From 024c4d9573313869c5dd713f0914eeb71977b53a Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 9 Nov 2018 14:58:38 -0800 Subject: [PATCH 001/211] Add Comparison with related projects --- README.rst | 215 +++++++++++++++++++++++++++++++++++++++- docs/_static/custom.css | 5 + tests/timings.py | 48 +++++++++ 3 files changed, 267 insertions(+), 1 deletion(-) create mode 100644 tests/timings.py diff --git a/README.rst b/README.rst index ae0b086..6aad03c 100644 --- a/README.rst +++ b/README.rst @@ -52,6 +52,7 @@ Does your company or website use `DiskCache`_? Send us a `message Features -------- +- TODO: update with Comparison below - Pure-Python - Fully Documented - Benchmark comparisons (alternatives, Django cache backends) @@ -88,12 +89,15 @@ function:: >>> help(Cache) >>> help(FanoutCache) >>> help(DjangoCache) + >>> from diskcache import Deque, Index + >>> help(Deque) + >>> help(Index) User Guide ---------- For those wanting more details, this part of the documentation describes -introduction, benchmarks, development, and API. +tutorial, benchmarks, API, and development. * `DiskCache Tutorial`_ * `DiskCache Cache Benchmarks`_ @@ -111,6 +115,215 @@ introduction, benchmarks, development, and API. .. _`DiskCache API Reference`: http://www.grantjenks.com/docs/diskcache/api.html .. _`DiskCache Development`: http://www.grantjenks.com/docs/diskcache/development.html +Comparisons +----------- + +Comparisons to popular projects related to `DiskCache`_. + +Key-Value Stores +................ + +`DiskCache`_ is mostly a simple key-value store. Feature comparisons with four +other projects are shown in the tables below. + +* `dbm`_ is part of Python's standard library and implements a generic + interface to variants of the DBM database — dbm.gnu or dbm.ndbm. If none of + these modules is installed, the slow-but-simple dbm.dumb is used. +* `shelve`_ is part of Python's standard library and implements a “shelf” as a + persistent, dictionary-like object. The difference with “dbm” databases is + that the values can be anything that the pickle module can handle. +* `sqlitedict`_ is a lightweight wrapper around Python's sqlite3 database with + a simple, Pythonic dict-like interface and support for multi-thread + access. Keys are arbitrary strings, values arbitrary pickle-able objects. +* `pickleDB`_ is a lightweight and simple key-value store. It is built upon + Python's simplejson module and was inspired by Redis. It is licensed with the + BSD three-caluse license. + +.. _`dbm`: https://docs.python.org/3/library/dbm.html +.. _`shelve`: https://docs.python.org/3/library/shelve.html +.. _`sqlitedict`: https://github.com/RaRe-Technologies/sqlitedict +.. _`pickleDB`: https://pythonhosted.org/pickleDB/ + +**Features** + +================ ================ ======= ======= ============ ============ +Feature diskcache dbm shelve sqlitedict pickleDB +================ ================ ======= ======= ============ ============ +Atomic? Always Maybe Maybe Maybe No +Persistent? Yes Yes Yes Yes Yes +Thread-safe? Yes No No Yes No +Process-safe? Yes No No Maybe No +Backend? SQLite DBM DBM SQLite File +Serialization? Customizable None Pickle Customizable JSON +Data Types? Mapping/Deque Mapping Mapping Mapping Mapping +Ordering? Insertion/Sorted None None None None +Eviction? None/LRS/LRU/LFU None None None None +Vacuum? Automatic Maybe Maybe Manual Automatic +Transactions? Yes No No Maybe No +Multiprocessing? Yes No No No No +Forkable? Yes No No No No +Metadata? Yes No No No No +================ ================ ======= ======= ============ ============ + +**Quality** + +================ ================ ======= ======= ============ ============ +Project diskcache dbm shelve sqlitedict pickleDB +================ ================ ======= ======= ============ ============ +Tests? Yes Yes Yes Yes Yes +Coverage? Yes Yes Yes Yes No +Stress? Yes No No No No +CI Tests? Travis/AppVeyor Yes Yes Travis No +Python? 2/3/PyPy All All 2/3 2/3 +License? Apache2 Python Python Apache2 3-Clause BSD +Docs? Extensive Summary Summary Readme Summary +Benchmarks? Yes No No No No +Sources? GitHub GitHub GitHub GitHub GitHub +Pure-Python? Yes Yes Yes Yes Yes +Server? No No No No No +Integrations? Django None None None None +================ ================ ======= ======= ============ ============ + +**Timings** + +These are very rough measurements. See `DiskCache Cache Benchmarks`_ for more +rigorous data. + +================ ================ ======= ======= ============ ============ +Project diskcache dbm shelve sqlitedict pickleDB +================ ================ ======= ======= ============ ============ +get 25 µs 36 µs 41 µs 513 µs 92 µs +set 198 µs 900 µs 928 µs 697 µs 1,020 µs +delete 248 µs 740 µs 702 µs 1,717 µs 1,020 µs +================ ================ ======= ======= ============ ============ + +Caching Libraries +................. + +* `joblib.Memory`_ provides caching functions and works by explicitly saving + the inputs and outputs to files. It is designed to work with non-hashable and + potentially large input and output data types such as numpy arrays. +* `klepto`_ extends Python’s `lru_cache` to utilize different keymaps and + alternate caching algorithms, such as `lfu_cache` and `mru_cache`. Klepto + uses a simple dictionary-sytle interface for all caches and archives. + +.. _`klepto`: https://pypi.org/project/klepto/ +.. _`joblib.Memory`: https://joblib.readthedocs.io/en/latest/memory.html + +Data Structures +............... + +* `dict`_ is a mapping object that maps hashable keys to arbitrary + values. Mappings are mutable objects. There is currently only one standard + Python mapping type, the dictionary. +* `pandas`_ is a Python package providing fast, flexible, and expressive data + structures designed to make working with “relational” or “labeled” data both + easy and intuitive. +* `Sorted Containers`_ is an Apache2 licensed sorted collections library, + written in pure-Python, and fast as C-extensions. Sorted Containers + implements sorted list, sorted dictionary, and sorted set data types. + +.. _`dict`: https://docs.python.org/3/library/stdtypes.html#typesmapping +.. _`pandas`: https://pandas.pydata.org/ +.. _`Sorted Containers`: http://www.grantjenks.com/docs/sortedcontainers/ + +Pure-Python Databases +..................... + +* `ZODB`_ supports an isomorphic interface for database operations which means + there's very little impact on your code to make objects persistent and + there's no database mapper that partially hides the datbase. +* `CodernityDB`_ is an open source, pure-Python, multi-platform, schema-less, + NoSQL database and includes an HTTP server version, and a Python client + library that aims to be 100% compatible with the embedded version. +* `TinyDB`_ is a tiny, document oriented database optimized for your + happiness. If you need a simple database with a clean API that just works + without lots of configuration, TinyDB might be the right choice for you. + +.. _`ZODB`: http://www.zodb.org/ +.. _`CodernityDB`: https://pypi.org/project/CodernityDB/ +.. _`TinyDB`: https://tinydb.readthedocs.io/ + +Object Relational Mappings (ORM) +................................ + +* `Django ORM`_ provides models that are the single, definitive source of + information about data and contains the essential fields and behaviors of the + stored data. Generally, each model maps to a single SQL database table. +* `SQLAlchemy`_ is the Python SQL toolkit and Object Relational Mapper that + gives application developers the full power and flexibility of SQL. It + provides a full suite of well known enterprise-level persistence patterns. +* `Peewee`_ is a simple and small ORM. It has few (but expressive) concepts, + making it easy to learn and intuitive to use. Peewee supports Sqlite, MySQL, + and PostgreSQL with tons of extensions. +* `SQLObject`_ is a popular Object Relational Manager for providing an object + interface to your database, with tables as classes, rows as instances, and + columns as attributes. +* `Pony ORM`_ is a Python ORM with beautiful query syntax. Use Python syntax + for interacting with the database. Pony translates such queries into SQL and + executes them in the database in the most efficient way. + +.. _`Django ORM`: https://docs.djangoproject.com/en/dev/topics/db/ +.. _`SQLAlchemy`: https://www.sqlalchemy.org/ +.. _`Peewee`: http://docs.peewee-orm.com/ +.. _`dataset`: https://dataset.readthedocs.io/ +.. _`SQLObject`: http://sqlobject.org/ +.. _`Pony ORM`: https://ponyorm.com/ + +SQL Databases +............. + +* `SQLite`_ is part of Python's standard library and provides a lightweight + disk-based database that doesn’t require a separate server process and allows + accessing the database using a nonstandard variant of the SQL query language. +* `MySQL`_ is one of the world’s most popular open source databases and has + become a leading database choice for web-based applications. MySQL includes a + standardized database driver for Python platforms and development. +* `PostgreSQL`_ is a powerful, open source object-relational database system + with over 30 years of active development. Psycopg is the most popular + PostgreSQL adapter for the Python programming language. +* `Oracle DB`_ is a relational database management system (RDBMS) from the + Oracle Corporation. Originally developed in 1977, Oracle DB is one of the + most trusted and widely-used enterprise relational database engines. +* `Microsoft SQL Server`_ is a relational database management system developed + by Microsoft. As a database server, it stores and retrieves data as requested + by other software applications. + +.. _`SQLite`: https://docs.python.org/3/library/sqlite3.html +.. _`MySQL`: https://dev.mysql.com/downloads/connector/python/ +.. _`PostgreSQL`: http://initd.org/psycopg/ +.. _`Oracle DB`: https://pypi.org/project/cx_Oracle/ +.. _`Microsoft SQL Server`: https://pypi.org/project/pyodbc/ + +Other Databases +............... + +* `Memcached`_ is free and open source, high-performance, distributed memory + object caching system, generic in nature, but intended for use in speeding up + dynamic web applications by alleviating database load. +* `Redis`_ is an open source, in-memory data structure store, used as a + database, cache and message broker. It supports data structures such as + strings, hashes, lists, sets, sorted sets with range queries, and more. +* `MongoDB`_ is a cross-platform document-oriented database program. Classified + as a NoSQL database program, MongoDB uses JSON-like documents with + schema. PyMongo is the recommended way to work with MongoDB from Python. +* `LMDB`_ is a lightning-fast, memory-mapped database. With memory-mapped + files, it has the read performance of a pure in-memory database while + retaining the persistence of standard disk-based databases. +* `BerkeleyDB`_ is a software library intended to provide a high-performance + embedded database for key/value data. Berkeley DB is a programmatic toolkit + that provides built-in database support for desktop and server applications. +* `LevelDB`_ is a fast key-value storage library written at Google that + provides an ordered mapping from string keys to string values. Data is stored + sorted by key and users can provide a custom comparison function. + +.. _`Memcached`: https://pypi.org/project/python-memcached/ +.. _`MongoDB`: https://api.mongodb.com/python/current/ +.. _`Redis`: https://redis.io/clients#python +.. _`LMDB`: https://lmdb.readthedocs.io/ +.. _`BerkeleyDB`: https://pypi.org/project/bsddb3/ +.. _`LevelDB`: https://plyvel.readthedocs.io/ + Reference --------- diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 998ebea..1a8e2a0 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -3,6 +3,11 @@ table { width: 100%; } +#comparison table { + display: block; + overflow: scroll; +} + th.head { text-align: center; } diff --git a/tests/timings.py b/tests/timings.py new file mode 100644 index 0000000..d1459e7 --- /dev/null +++ b/tests/timings.py @@ -0,0 +1,48 @@ +import dbm +import diskcache +import pickledb +import shelve +import sqlitedict +import timeit + +value = 'value' + +print('diskcache set') +dc = diskcache.FanoutCache('/tmp/diskcache') +%timeit -n 100 -r 7 dc['key'] = value +print('diskcache get') +%timeit -n 100 -r 7 dc['key'] +print('diskcache set/delete') +%timeit -n 100 -r 7 dc['key'] = value; del dc['key'] + +print('dbm set') +d = dbm.open('/tmp/dbm', 'c') +%timeit -n 100 -r 7 d['key'] = value; d.sync() +print('dbm get') +%timeit -n 100 -r 7 d['key'] +print('dbm set/delete') +%timeit -n 100 -r 7 d['key'] = value; del d['key']; d.sync() + +print('shelve set') +s = shelve.open('/tmp/shelve') +%timeit -n 100 -r 7 s['key'] = value; s.sync() +print('shelve get') +%timeit -n 100 -r 7 s['key'] +print('shelve set/delete') +%timeit -n 100 -r 7 s['key'] = value; del s['key']; s.sync() + +print('sqlitedict set') +sd = sqlitedict.SqliteDict('/tmp/sqlitedict', autocommit=True) +%timeit -n 100 -r 7 sd['key'] = value +print('sqlitedict get') +%timeit -n 100 -r 7 sd['key'] +print('sqlitedict set/delete') +%timeit -n 100 -r 7 sd['key'] = value; del sd['key'] + +print('pickledb set') +p = pickledb.load('/tmp/pickledb', True) +%timeit -n 100 -r 7 p['key'] = value +print('pickledb get') +%timeit -n 100 -r 7 p = pickledb.load('/tmp/pickledb', True); p['key'] +print('pickledb set/delete') +%timeit -n 100 -r 7 p['key'] = value; del p['key'] From 05aa1000f7103705ef4ec1c7dd3d1b670628a39f Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 12 Jun 2019 13:50:41 -0700 Subject: [PATCH 002/211] Use time.time for Python 2 compatibility --- diskcache/recipes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 8ca2c09..52f606a 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -236,7 +236,7 @@ def __exit__(self, *exc_info): def throttle(cache, count, seconds, name=None, expire=None, tag=None, - time_func=time.monotonic, sleep_func=time.sleep): + time_func=time.time, sleep_func=time.sleep): """Decorator to throttle calls to function. >>> import diskcache, time From 509b0986c3bae7a584a1517c3498819813ca6cfc Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 12 Jun 2019 13:50:57 -0700 Subject: [PATCH 003/211] Open README in utf-8 for Windows --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 64da622..90dc280 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +from io import open from setuptools import setup from setuptools.command.test import test as TestCommand @@ -15,7 +16,7 @@ def run_tests(self): exit(errno) -with open('README.rst') as reader: +with open('README.rst', encoding='utf-8') as reader: readme = reader.read() setup( From 48f2c5a088b445ec885a0937a9483947933774cd Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 12 Jun 2019 13:51:13 -0700 Subject: [PATCH 004/211] Remove unused imports for pylint --- diskcache/djangocache.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index fbafeaa..d171408 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -1,9 +1,6 @@ "Django-compatible disk and file backed cache." from functools import wraps -from math import log -from random import random -from time import time from django.core.cache.backends.base import BaseCache try: From 012f311653c107d76c44ab60cb4bac7debc49034 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 12 Jun 2019 14:02:08 -0700 Subject: [PATCH 005/211] Add testimonials for #110 --- README.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/README.rst b/README.rst index 6aad03c..f1cac23 100644 --- a/README.rst +++ b/README.rst @@ -46,9 +46,24 @@ testing has 100% coverage with unit tests and hours of stress. Testimonials ------------ +`Daren Hasenkamp`_, Founder -- + + "It's a useful, simple API, just like I love about Redis. It has reduced + the amount of queries hitting my Elasticsearch cluster by over 25% for a + website that gets over a million users/day (100+ hits/second)." + +`Mathias Petermann`_, Senior Linux System Engineer -- + + "I implemented it into a wrapper for our Ansible lookup modules and we were + able to speed up some Ansible runs by almost 3 times. DiskCache is saving + us a ton of time." + Does your company or website use `DiskCache`_? Send us a `message `_ and let us know. +.. _`Daren Hasenkamp`: https://www.linkedin.com/in/daren-hasenkamp-93006438/ +.. _`Mathias Petermann`: https://www.linkedin.com/in/mathias-petermann-a8aa273b/ + Features -------- From 5e69d5b66e52373e9b5fbe4a51eb8811bb766df7 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 12 Jun 2019 15:23:19 -0700 Subject: [PATCH 006/211] Add comment blocks for Python 2/3 shims --- diskcache/core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/diskcache/core.py b/diskcache/core.py index 10b8e0b..89c83a0 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -19,6 +19,10 @@ import warnings import zlib +############################################################################ +# BEGIN Python 2/3 Shims +############################################################################ + if sys.hexversion < 0x03000000: import cPickle as pickle # pylint: disable=import-error # ISSUE #25 Fix for http://bugs.python.org/issue10211 @@ -48,6 +52,10 @@ def full_name(func): name = func.__name__ return func.__module__ + '.' + name +############################################################################ +# END Python 2/3 Shims +############################################################################ + try: WindowsError except NameError: From c8f125d93af209743a39bf71ad15b6e471bfdcb8 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 12 Jun 2019 15:23:34 -0700 Subject: [PATCH 007/211] Add FanoutCache.memoize using monkey-patching for Python 2 --- diskcache/fanout.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index dc31593..a520f25 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -355,9 +355,6 @@ def __delitem__(self, key): del shard[key] - memoize = Cache.memoize - - def check(self, fix=False, retry=False): """Check database and file system consistency. @@ -656,3 +653,21 @@ def index(self, name): temp = Index(directory) _indexes[name] = temp return temp + + +############################################################################ +# BEGIN Python 2/3 Shims +############################################################################ + +import sys # pylint: disable=wrong-import-position,wrong-import-order + +if sys.hexversion < 0x03000000: + import types + memoize_func = Cache.__dict__['memoize'] # pylint: disable=invalid-name + FanoutCache.memoize = types.MethodType(memoize_func, None, FanoutCache) +else: + FanoutCache.memoize = Cache.memoize + +############################################################################ +# END Python 2/3 Shims +############################################################################ From 54744b946263d410372020bfe8c86b6c26e701aa Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 12 Jun 2019 15:41:15 -0700 Subject: [PATCH 008/211] Simplify recipes and make more robust --- diskcache/recipes.py | 42 ++++++++++-------------------------------- 1 file changed, 10 insertions(+), 32 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 52f606a..53cc589 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -1,31 +1,5 @@ """Disk Cache Recipes ->>> import diskcache as dc, time ->>> cache = dc.Cache() ->>> @cache.memoize(expire=0) -... @barrier(cache, dc.Lock) -... @cache.memoize() -... def work(num): -... time.sleep(1) -... return num ->>> from concurrent.futures import ThreadPoolExecutor ->>> with ThreadPoolExecutor(5) as executor: -... start = time.time() -... nums = list(executor.map(work, range(5))) -... end = time.time() ->>> nums -[0, 1, 2, 3, 4] ->>> int(end - start) -5 ->>> with ThreadPoolExecutor(5) as executor: -... start = time.time() -... nums = list(executor.map(work, range(5))) -... end = time.time() ->>> nums -[0, 1, 2, 3, 4] ->>> int(end - start) -0 - """ import functools @@ -241,12 +215,16 @@ def throttle(cache, count, seconds, name=None, expire=None, tag=None, >>> import diskcache, time >>> cache = diskcache.Cache() - >>> @throttle(cache, 1, 1) - ... def int_time(): - ... return int(time.time()) - >>> times = [int_time() for _ in range(4)] - >>> [times[i] - times[i - 1] for i in range(1, 4)] - [1, 1, 1] + >>> count = 0 + >>> @throttle(cache, 5, 1) + ... def increment(): + ... global count + ... count += 1 + >>> start = time.time() + >>> while (time.time() - start) <= 4: + ... increment() + >>> count + 25 """ def decorator(func): From 912f96c6895e48f78e6e4d9020aeedac02fd4d35 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 12 Jun 2019 15:45:52 -0700 Subject: [PATCH 009/211] Close Cache before removing directory --- docs/tutorial.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 8a67ab8..f2db508 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -311,6 +311,8 @@ The third is :meth:`check ` which verifies cache consistency. It can also fix inconsistencies and reclaim unused space. The return value is a list of warnings. + >>> _ = cache.check(fix=True) + >>> cache.close() >>> import shutil >>> shutil.rmtree(cache.directory) From a60b54108ddd6c71f3cbc0fa83bc03c7429a35bf Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 09:59:26 -0700 Subject: [PATCH 010/211] Suppress PermissionError for Windows wonkiness --- docs/tutorial.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index f2db508..5ce890d 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -311,10 +311,11 @@ The third is :meth:`check ` which verifies cache consistency. It can also fix inconsistencies and reclaim unused space. The return value is a list of warnings. - >>> _ = cache.check(fix=True) + >>> warnings = cache.check() >>> cache.close() - >>> import shutil - >>> shutil.rmtree(cache.directory) + >>> import contextlib, shutil + >>> with contextlib.suppress(PermissionError): # Windows wonkiness + ... shutil.rmtree(cache.directory) .. _tutorial-fanoutcache: From 56927b51098395cb1ff9c45a4fbfb5d109bb8806 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 10:13:13 -0700 Subject: [PATCH 011/211] Update DjangoCache tests with touch() test cases --- tests/test_djangocache.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index ea163bc..f70b87c 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -430,6 +430,11 @@ def test_forever_timeout(self): self.assertEqual(cache.get('key3'), 'sausage') self.assertEqual(cache.get('key4'), 'lobster bisque') + cache.set('key5', 'belgian fries', timeout=1) + cache.touch('key5', timeout=None) + time.sleep(2) + self.assertEqual(cache.get('key5'), 'belgian fries') + def test_zero_timeout(self): """ Passing in zero into timeout results in a value that is not cached @@ -444,6 +449,10 @@ def test_zero_timeout(self): self.assertIsNone(cache.get('key3')) self.assertIsNone(cache.get('key4')) + cache.set('key5', 'belgian fries', timeout=5) + cache.touch('key5', timeout=0) + self.assertIsNone(cache.get('key5')) + def test_float_timeout(self): # Make sure a timeout given as a float doesn't crash anything. cache.set("key1", "spam", 100.2) From bce5a77b607781b30702a22b31730604e08fcda4 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 10:38:50 -0700 Subject: [PATCH 012/211] Fix recipes doctest for Python 2 support: get_ident and ThreadPool --- diskcache/recipes.py | 36 +++++++++++++++++++++++++++--------- tests/test_doctest.py | 10 ++++------ 2 files changed, 31 insertions(+), 15 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 53cc589..fbd5fa8 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -6,11 +6,25 @@ import math import os import random +import sys import threading import time from .core import ENOVAL, args_to_key, full_name +############################################################################ +# BEGIN Python 2/3 Shims +############################################################################ + +if sys.hexversion < 0x03000000: + from thread import get_ident +else: + from threading import get_ident + +############################################################################ +# END Python 2/3 Shims +############################################################################ + class Averager(object): """Recipe for calculating a running average. @@ -120,7 +134,7 @@ def __init__(self, cache, key, expire=None, tag=None): self._expire = expire self._tag = tag pid = os.getpid() - tid = threading.get_ident() + tid = get_ident() self._value = '{}-{}'.format(pid, tid) def acquire(self): @@ -221,10 +235,10 @@ def throttle(cache, count, seconds, name=None, expire=None, tag=None, ... global count ... count += 1 >>> start = time.time() - >>> while (time.time() - start) <= 4: + >>> while (time.time() - start) <= 2: ... increment() >>> count - 25 + 15 """ def decorator(func): @@ -280,13 +294,17 @@ def barrier(cache, lock_factory, name=None, expire=None, tag=None): >>> cache = diskcache.Cache() >>> @barrier(cache, Lock) ... def work(num): + ... print('worker started') ... time.sleep(1) - ... return int(time.time()) - >>> from concurrent.futures import ThreadPoolExecutor - >>> with ThreadPoolExecutor(4) as executor: - ... times = sorted(executor.map(work, range(4))) - >>> [times[i] - times[i - 1] for i in range(1, 4)] - [1, 1, 1] + ... print('worker finished') + >>> import multiprocessing.pool + >>> pool = multiprocessing.pool.ThreadPool(2) + >>> _ = pool.map(work, range(2)) + worker started + worker finished + worker started + worker finished + >>> pool.terminate() """ def decorator(func): diff --git a/tests/test_doctest.py b/tests/test_doctest.py index e398391..981d00c 100644 --- a/tests/test_doctest.py +++ b/tests/test_doctest.py @@ -29,15 +29,13 @@ def test_persistent(): assert failures == 0 -def test_tutorial(): - if sys.hexversion < 0x03000000: - return - failures, _ = doctest.testfile('../docs/tutorial.rst') +def test_recipes(): + failures, _ = doctest.testmod(diskcache.recipes) assert failures == 0 -def test_recipes(): +def test_tutorial(): if sys.hexversion < 0x03000000: return - failures, _ = doctest.testmod(diskcache.recipes) + failures, _ = doctest.testfile('../docs/tutorial.rst') assert failures == 0 From 8701cf901e07f145d5307eb8c9a9dd0cb6fb8e65 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 10:54:32 -0700 Subject: [PATCH 013/211] Update tutorial doctests to work on Python 2/3 --- docs/tutorial.rst | 64 ++++++++++++++++++++++++------------------- tests/test_doctest.py | 2 -- 2 files changed, 36 insertions(+), 30 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 5ce890d..deea9cc 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -92,26 +92,26 @@ Closed Cache objects will automatically re-open when accessed. But opening Cache objects is relatively slow, and since all operations are atomic, you can safely leave Cache objects open. - >>> cache.set(b'key', b'value') + >>> cache.set('key', 'value') True >>> cache.close() - >>> cache.get(b'key') # Automatically opens, but slower. - b'value' + >>> cache.get('key') # Automatically opens, but slower. + 'value' Set an item, get a value, and delete a key using the usual operators: - >>> cache[b'key'] = b'value' - >>> cache[b'key'] - b'value' - >>> b'key' in cache + >>> cache['key'] = 'value' + >>> cache['key'] + 'value' + >>> 'key' in cache True - >>> del cache[b'key'] + >>> del cache['key'] There's also a :meth:`set ` method with additional keyword parameters: `expire`, `read`, and `tag`. >>> from io import BytesIO - >>> cache.set(b'key', BytesIO(b'value'), expire=5, read=True, tag='data') + >>> cache.set('key', BytesIO(b'value'), expire=5, read=True, tag='data') True In the example above: the key expires in 5 seconds, the value is read as a @@ -119,14 +119,14 @@ file-like object, and tag metadata is stored with the key. Another method, :meth:`get ` supports querying extra information with `default`, `read`, `expire_time`, and `tag` keyword parameters. - >>> result = cache.get(b'key', default=b'', read=True, expire_time=True, tag=True) + >>> result = cache.get('key', read=True, expire_time=True, tag=True) >>> reader, timestamp, tag = result - >>> type(reader) - - >>> type(timestamp) - - >>> tag - 'data' + >>> print(reader.read().decode()) + value + >>> type(timestamp).__name__ + 'float' + >>> print(tag) + data The return value is a tuple containing the value, expire time (seconds from epoch), and tag. Because we passed ``read=True`` the value is returned as a @@ -180,8 +180,14 @@ used to delete an item in the cache and return its value. 'does not exist' >>> cache.set('dave', 0, expire=None, tag='admin') True - >>> cache.pop('dave', expire_time=True, tag=True) - (0, None, 'admin') + >>> result = cache.pop('dave', expire_time=True, tag=True) + >>> value, timestamp, tag = result + >>> value + 0 + >>> print(timestamp) + None + >>> print(tag) + admin The :meth:`pop ` operation is atomic and using :meth:`incr ` together is an accurate method for counting and dumping @@ -313,9 +319,11 @@ return value is a list of warnings. >>> warnings = cache.check() >>> cache.close() - >>> import contextlib, shutil - >>> with contextlib.suppress(PermissionError): # Windows wonkiness + >>> import shutil + >>> try: ... shutil.rmtree(cache.directory) + ... except PermissionError: # Windows wonkiness + ... pass .. _tutorial-fanoutcache: @@ -475,8 +483,8 @@ access and editing at both front and back sides. :class:`Deque >>> deque.appendleft('foo') >>> len(deque) 4 - >>> type(deque.directory) - + >>> type(deque.directory).__name__ + 'str' >>> other = Deque(directory=deque.directory) >>> len(other) 4 @@ -619,13 +627,13 @@ All clients accessing the cache are expected to use the same eviction policy. The policy can be set during initialization using a keyword argument. >>> cache = Cache() - >>> cache.eviction_policy - 'least-recently-stored' + >>> print(cache.eviction_policy) + least-recently-stored >>> cache = Cache(eviction_policy='least-frequently-used') - >>> cache.eviction_policy - 'least-frequently-used' - >>> cache.reset('eviction_policy', 'least-recently-used') - 'least-recently-used' + >>> print(cache.eviction_policy) + least-frequently-used + >>> print(cache.reset('eviction_policy', 'least-recently-used')) + least-recently-used Though the eviction policy is changed, the previously created indexes will not be dropped. Prefer to always specify the eviction policy as a keyword argument diff --git a/tests/test_doctest.py b/tests/test_doctest.py index 981d00c..70fa61c 100644 --- a/tests/test_doctest.py +++ b/tests/test_doctest.py @@ -35,7 +35,5 @@ def test_recipes(): def test_tutorial(): - if sys.hexversion < 0x03000000: - return failures, _ = doctest.testfile('../docs/tutorial.rst') assert failures == 0 From f5422b4f774bfe98510b120c007b72816064b407 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 11:05:01 -0700 Subject: [PATCH 014/211] Ignore old long-integer "L"-suffix --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index deea9cc..aabf9ff 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -561,7 +561,7 @@ are updated lazily. Prefer idioms like :meth:`len ` directly. >>> cache = Cache(size_limit=int(4e9)) - >>> cache.size_limit + >>> print(cache.size_limit) 4000000000 >>> cache.disk_min_file_size 32768 From 00dced81389745759b455e813ff7f41a6c6e1f54 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 11:05:36 -0700 Subject: [PATCH 015/211] Change PermissionError to OSError for Python 2 --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index aabf9ff..d09fa3b 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -322,7 +322,7 @@ return value is a list of warnings. >>> import shutil >>> try: ... shutil.rmtree(cache.directory) - ... except PermissionError: # Windows wonkiness + ... except OSError: # Windows wonkiness ... pass .. _tutorial-fanoutcache: From 03a5f67de75d925f6c2bf4ac3f8c78fad94549e6 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 11:05:54 -0700 Subject: [PATCH 016/211] Decrease throttle rate for clock skew on AppVeyor --- diskcache/recipes.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index fbd5fa8..90ca376 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -230,7 +230,7 @@ def throttle(cache, count, seconds, name=None, expire=None, tag=None, >>> import diskcache, time >>> cache = diskcache.Cache() >>> count = 0 - >>> @throttle(cache, 5, 1) + >>> @throttle(cache, 2, 1) ... def increment(): ... global count ... count += 1 @@ -238,7 +238,7 @@ def throttle(cache, count, seconds, name=None, expire=None, tag=None, >>> while (time.time() - start) <= 2: ... increment() >>> count - 15 + 6 """ def decorator(func): From 665683c2f35021ee4b03146cc75112012634c1e2 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 11:35:14 -0700 Subject: [PATCH 017/211] Disable pylint import-error for Python 2 import --- diskcache/recipes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 90ca376..43edf4b 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -17,7 +17,7 @@ ############################################################################ if sys.hexversion < 0x03000000: - from thread import get_ident + from thread import get_ident # pylint: disable=import-error else: from threading import get_ident From 325a525145fb511aad8b6c570c44a51af2c2774d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 11:51:39 -0700 Subject: [PATCH 018/211] Use full_name() for throttle key and accept 6 or 7 calls --- diskcache/recipes.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 43edf4b..3223096 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -230,30 +230,20 @@ def throttle(cache, count, seconds, name=None, expire=None, tag=None, >>> import diskcache, time >>> cache = diskcache.Cache() >>> count = 0 - >>> @throttle(cache, 2, 1) + >>> @throttle(cache, 2, 1) # 2 calls per 1 second ... def increment(): ... global count ... count += 1 >>> start = time.time() >>> while (time.time() - start) <= 2: ... increment() - >>> count - 6 + >>> count in (6, 7) # 6 or 7 calls depending on processor load + True """ def decorator(func): rate = count / float(seconds) - - if name is None: - try: - key = func.__qualname__ - except AttributeError: - key = func.__name__ - - key = func.__module__ + '.' + key - else: - key = name - + key = full_name(func) if name is None else name now = time_func() cache.set(key, (now, count), expire=expire, tag=tag, retry=True) From 2be5bac6cf9a640829bd8e75ace8f5ccbc24c012 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 12:00:45 -0700 Subject: [PATCH 019/211] Update docstrings regarding Timeout errors --- diskcache/djangocache.py | 2 +- diskcache/fanout.py | 3 +-- diskcache/persistent.py | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index d171408..a2d863c 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -295,7 +295,7 @@ def stats(self, enable=True, reset=False): def create_tag_index(self): """Create tag index on cache database. - It is better to initialize cache with `tag_index=True` than use this. + Better to initialize cache with `tag_index=True` than use this. :raises Timeout: if database timeout occurs diff --git a/diskcache/fanout.py b/diskcache/fanout.py index a520f25..d3cd413 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -395,7 +395,7 @@ def expire(self, retry=False): def create_tag_index(self): """Create tag index on cache database. - It is better to initialize cache with `tag_index=True` than use this. + Better to initialize cache with `tag_index=True` than use this. :raises Timeout: if database timeout occurs @@ -550,7 +550,6 @@ def reset(self, key, value=ENOVAL): :param str key: Settings key for item :param value: value for item (optional) :return: updated value for item - :raises Timeout: if database timeout occurs """ for shard in self._shards: diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 5f2dbde..5350fbf 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -648,7 +648,6 @@ def transact(self): [4, 0, 1, 2, 3] :return: context manager for use in `with` statement - :raises Timeout: if database timeout occurs """ with self._cache.transact(retry=True): @@ -1385,7 +1384,6 @@ def transact(self): 123.4 :return: context manager for use in `with` statement - :raises Timeout: if database timeout occurs """ with self._cache.transact(retry=True): From a1feac48c11bb9f199324298a7b91c1f2a44a6ec Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 13 Jun 2019 16:09:00 -0700 Subject: [PATCH 020/211] Add caveat about full disk --- docs/tutorial.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index d09fa3b..1d70e55 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -711,6 +711,12 @@ this reason, :doc:`DiskCache ` currently `performs poorly`_ on `Python Anywhere`_. Users have also reported issues running inside of `Parallels`_ shared folders. +:doc:`DiskCache ` uses transactions when writing data to disk using +SQLite. When the disk or database is full, a :exc:`sqlite3.OperationalError` +will be raised from any method that attempts to write data. Read operations +will still succeed so long as they do not cause any write (as might occur if +cache statistics are being recorded). + .. _`hash protocol`: https://docs.python.org/library/functions.html#hash .. _`not recommended`: https://www.sqlite.org/faq.html#q5 .. _`performs poorly`: https://www.pythonanywhere.com/forums/topic/1847/ From f3866d21b74d506bf86bf83d480c4e826a820162 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 11:39:45 -0700 Subject: [PATCH 021/211] Audit and add retry=True as needed to recipes --- diskcache/recipes.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 3223096..b6ac6ac 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -52,7 +52,7 @@ def __init__(self, cache, key, expire=None, tag=None): def add(self, value): "Add `value` to average." - with self._cache.transact(): + with self._cache.transact(retry=True): total, count = self._cache.get(self._key, default=(0.0, 0)) total += value count += 1 @@ -140,7 +140,7 @@ def __init__(self, cache, key, expire=None, tag=None): def acquire(self): "Acquire lock by incrementing count using spin-lock algorithm." while True: - with self._cache.transact(): + with self._cache.transact(retry=True): value, count = self._cache.get(self._key, default=(None, 0)) if self._value == value or count == 0: self._cache.set( @@ -152,7 +152,7 @@ def acquire(self): def release(self): "Release lock by decrementing count." - with self._cache.transact(): + with self._cache.transact(retry=True): value, count = self._cache.get(self._key, default=(None, 0)) is_owned = self._value == value and count > 0 assert is_owned, 'cannot release un-acquired lock' @@ -196,7 +196,7 @@ def __init__(self, cache, key, value=1, expire=None, tag=None): def acquire(self): "Acquire semaphore by decrementing value using spin-lock algorithm." while True: - with self._cache.transact(): + with self._cache.transact(retry=True): value = self._cache.get(self._key, default=self._value) if value > 0: self._cache.set( @@ -208,7 +208,7 @@ def acquire(self): def release(self): "Release semaphore by incrementing value." - with self._cache.transact(): + with self._cache.transact(retry=True): value = self._cache.get(self._key, default=self._value) assert self._value > value, 'cannot release un-acquired semaphore' value += 1 @@ -250,16 +250,16 @@ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): while True: - with cache.transact(): - last, tally = cache.get(key, retry=True) + with cache.transact(retry=True): + last, tally = cache.get(key) now = time_func() tally += (now - last) * rate delay = 0 if tally > count: - cache.set(key, (now, count - 1), expire, retry=True) + cache.set(key, (now, count - 1), expire) elif tally >= 1: - cache.set(key, (now, tally - 1), expire, retry=True) + cache.set(key, (now, tally - 1), expire) else: delay = (1 - tally) / rate @@ -399,7 +399,9 @@ def recompute(): # Check whether a thread has started for early recomputation. thread_key = key + (ENOVAL,) - thread_added = cache.add(thread_key, None, expire=delta) + thread_added = cache.add( + thread_key, None, expire=delta, retry=True, + ) if thread_added: # Start thread for early recomputation. From c61a7c6fa32a0010385a13767d1bbfe6c4c345fe Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 11:41:05 -0700 Subject: [PATCH 022/211] Fix formatting for keyword arguments --- diskcache/recipes.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index b6ac6ac..b23be1b 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -157,8 +157,8 @@ def release(self): is_owned = self._value == value and count > 0 assert is_owned, 'cannot release un-acquired lock' self._cache.set( - self._key, (value, count - 1), expire=self._expire, - tag=self._tag, + self._key, (value, count - 1), + expire=self._expire, tag=self._tag, ) def __enter__(self): @@ -200,8 +200,8 @@ def acquire(self): value = self._cache.get(self._key, default=self._value) if value > 0: self._cache.set( - self._key, value - 1, expire=self._expire, - tag=self._tag, + self._key, value - 1, + expire=self._expire, tag=self._tag, ) return time.sleep(0.001) From 24a6299b4a8bd03d84cc0ff4d85d23c4440e9d09 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 11:41:26 -0700 Subject: [PATCH 023/211] Group Python 2/3 shims together --- diskcache/fanout.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index d3cd413..085345f 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -4,17 +4,24 @@ import operator import os.path as op import sqlite3 +import sys import tempfile import time -try: - from functools import reduce -except ImportError: - reduce # pylint: disable=pointless-statement - from .core import ENOVAL, DEFAULT_SETTINGS, Cache, Disk, Timeout from .persistent import Deque, Index +############################################################################ +# BEGIN Python 2/3 Shims +############################################################################ + +if sys.hexversion >= 0x03000000: + from functools import reduce + +############################################################################ +# END Python 2/3 Shims +############################################################################ + class FanoutCache(object): "Cache that shards keys and values." From 7c9b6f73e67d4a288d6ca8d6c92e7c6a4db7163b Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 11:42:07 -0700 Subject: [PATCH 024/211] Doctest and formatting fixes --- diskcache/persistent.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 5350fbf..bff2df0 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -173,7 +173,6 @@ def _index(self, index, func): raise IndexError('deque index out of range') - def __getitem__(self, index): """deque.__getitem__(index) <==> deque[index] @@ -360,6 +359,13 @@ def appendleft(self, value): def clear(self): """Remove all elements from deque. + >>> deque = Deque('abc') + >>> len(deque) + 3 + >>> deque.clear() + >>> list(deque) + [] + """ self._cache.clear(retry=True) @@ -902,7 +908,7 @@ def popitem(self, last=True): >>> index.popitem() Traceback (most recent call last): ... - KeyError + KeyError: 'dictionary is empty' :param bool last: pop last item pair (default True) :return: key and value item pair @@ -1005,6 +1011,13 @@ def pull(self, prefix=None, default=(None, None), side='front'): def clear(self): """Remove all items from index. + >>> index = Index({'a': 0, 'b': 1, 'c': 2}) + >>> len(index) + 3 + >>> index.clear() + >>> dict(index) + {} + """ self._cache.clear(retry=True) From c66f0b21214cda18bf6d4b62e85351a3f5cc984c Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 11:42:55 -0700 Subject: [PATCH 025/211] Change Index.popitem to use Cache.peekitem and transaction --- diskcache/persistent.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/diskcache/persistent.py b/diskcache/persistent.py index bff2df0..961f773 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -918,21 +918,11 @@ def popitem(self, last=True): # pylint: disable=arguments-differ _cache = self._cache - while True: - try: - if last: - key = next(reversed(_cache)) - else: - key = next(iter(_cache)) - except StopIteration: - raise KeyError + with _cache.transact(retry=True): + key, value = _cache.peekitem(last=last) + del _cache[key] - try: - value = _cache.pop(key, retry=True) - except KeyError: - continue - else: - return key, value + return key, value def push(self, value, prefix=None, side='back'): From 892b53e186abe479fa36446f065fc2ec1014d988 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 11:43:34 -0700 Subject: [PATCH 026/211] Add tests for Index and Deque coverage --- tests/test_deque.py | 57 +++++++++++++++++++++++++++++++++++++++++++++ tests/test_index.py | 4 ++++ 2 files changed, 61 insertions(+) diff --git a/tests/test_deque.py b/tests/test_deque.py index a3ac018..640cee2 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -76,12 +76,56 @@ def test_getsetdel(deque): assert len(deque) == 0 +def test_index_positive(deque): + cache = mock.MagicMock() + cache.__len__.return_value = 3 + cache.iterkeys.return_value = ['a', 'b', 'c'] + cache.__getitem__.side_effect = [KeyError, 101, 102] + with mock.patch.object(deque, '_cache', cache): + assert deque[0] == 101 + + +def test_index_negative(deque): + cache = mock.MagicMock() + cache.__len__.return_value = 3 + cache.iterkeys.return_value = ['c', 'b', 'a'] + cache.__getitem__.side_effect = [KeyError, 101, 100] + with mock.patch.object(deque, '_cache', cache): + assert deque[-1] == 101 + + +def test_index_out_of_range(deque): + cache = mock.MagicMock() + cache.__len__.return_value = 3 + cache.iterkeys.return_value = ['a', 'b', 'c'] + cache.__getitem__.side_effect = [KeyError] * 3 + with mock.patch.object(deque, '_cache', cache): + with pytest.raises(IndexError): + deque[0] + + +def test_iter_keyerror(deque): + cache = mock.MagicMock() + cache.iterkeys.return_value = ['a', 'b', 'c'] + cache.__getitem__.side_effect = [KeyError, 101, 102] + with mock.patch.object(deque, '_cache', cache): + assert list(iter(deque)) == [101, 102] + + def test_reversed(deque): sequence = list('abcde') deque += sequence assert list(reversed(deque)) == list(reversed(sequence)) +def test_reversed_keyerror(deque): + cache = mock.MagicMock() + cache.iterkeys.return_value = ['c', 'b', 'a'] + cache.__getitem__.side_effect = [KeyError, 101, 100] + with mock.patch.object(deque, '_cache', cache): + assert list(reversed(deque)) == [101, 100] + + def test_state(deque): sequence = list('abcde') deque.extend(sequence) @@ -178,6 +222,15 @@ def test_remove_valueerror(deque): deque.remove(0) +def test_remove_keyerror(deque): + cache = mock.MagicMock() + cache.iterkeys.return_value = ['a', 'b', 'c'] + cache.__getitem__.side_effect = [KeyError, 100, 100] + cache.__delitem__.side_effect = [KeyError, None] + with mock.patch.object(deque, '_cache', cache): + deque.remove(100) + + def test_reverse(deque): deque += 'abcde' deque.reverse() @@ -223,3 +276,7 @@ def test_rotate_indexerror_negative(deque): with mock.patch.object(deque, '_cache', cache): deque.rotate(-1) + + +def test_repr(deque): + assert repr(deque).startswith('Deque(') diff --git a/tests/test_index.py b/tests/test_index.py index 83ba5cf..f4232c8 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -179,3 +179,7 @@ def fibrec(num): assert hits2 == (hits1 + count) assert misses2 == misses1 + + +def test_repr(index): + assert repr(index).startswith('Index(') From 0bf521c6a6cf2268fa4856b454b95e48ea10ec82 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 13:08:49 -0700 Subject: [PATCH 027/211] Rename test_early_recompute.py to plot_early_recompute.py --- tests/{test_early_recompute.py => plot_early_recompute.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{test_early_recompute.py => plot_early_recompute.py} (100%) diff --git a/tests/test_early_recompute.py b/tests/plot_early_recompute.py similarity index 100% rename from tests/test_early_recompute.py rename to tests/plot_early_recompute.py From 13a59228f2c918d200acfeafa703b7805dcd9d0a Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 13:09:10 -0700 Subject: [PATCH 028/211] Add test cases for recipes --- tests/test_recipes.py | 78 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 tests/test_recipes.py diff --git a/tests/test_recipes.py b/tests/test_recipes.py new file mode 100644 index 0000000..35de332 --- /dev/null +++ b/tests/test_recipes.py @@ -0,0 +1,78 @@ +"Test diskcache.recipes." + +import diskcache as dc +import mock +import pytest +import shutil +import threading +import time + + +@pytest.fixture +def cache(): + with dc.Cache() as cache: + yield cache + shutil.rmtree(cache.directory, ignore_errors=True) + + +def test_averager(cache): + nums = dc.Averager(cache, 'nums') + for i in range(10): + nums.add(i) + assert nums.get() == 4.5 + assert nums.pop() == 4.5 + for i in range(20): + nums.add(i) + assert nums.get() == 9.5 + assert nums.pop() == 9.5 + + +def test_rlock(cache): + state = {'num': 0} + rlock = dc.RLock(cache, 'demo') + def worker(): + state['num'] += 1 + with rlock: + state['num'] += 1 + time.sleep(0.1) + with rlock: + thread = threading.Thread(target=worker) + thread.start() + time.sleep(0.1) + assert state['num'] == 1 + thread.join() + assert state['num'] == 2 + + +def test_semaphore(cache): + state = {'num': 0} + semaphore = dc.BoundedSemaphore(cache, 'demo', value=3) + def worker(): + state['num'] += 1 + with semaphore: + state['num'] += 1 + time.sleep(0.1) + semaphore.acquire() + semaphore.acquire() + with semaphore: + thread = threading.Thread(target=worker) + thread.start() + time.sleep(0.1) + assert state['num'] == 1 + thread.join() + assert state['num'] == 2 + semaphore.release() + semaphore.release() + + +def test_memoize_stampede(cache): + state = {'num': 0} + @dc.memoize_stampede(cache, 0.1) + def worker(num): + time.sleep(0.01) + state['num'] += 1 + return num + start = time.time() + while (time.time() - start) < 1: + worker(100) + assert state['num'] > 0 From bb750fbf629c1d93f0f0ac7bab931669ec45a98c Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 13:09:24 -0700 Subject: [PATCH 029/211] Create connection in __enter__ --- diskcache/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/diskcache/core.py b/diskcache/core.py index 89c83a0..3c0b023 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -2332,6 +2332,7 @@ def close(self): def __enter__(self): + connection = self._con # Create connection in thread. return self From fb1bc6738a9cf1510e08bdc3db092730c2d9df44 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 13:10:03 -0700 Subject: [PATCH 030/211] Fix rlock value calculation (must be function local) --- diskcache/recipes.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index b23be1b..069269e 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -133,18 +133,19 @@ def __init__(self, cache, key, expire=None, tag=None): self._key = key self._expire = expire self._tag = tag - pid = os.getpid() - tid = get_ident() - self._value = '{}-{}'.format(pid, tid) def acquire(self): "Acquire lock by incrementing count using spin-lock algorithm." + pid = os.getpid() + tid = get_ident() + pid_tid = '{}-{}'.format(pid, tid) + while True: with self._cache.transact(retry=True): value, count = self._cache.get(self._key, default=(None, 0)) - if self._value == value or count == 0: + if pid_tid == value or count == 0: self._cache.set( - self._key, (self._value, count + 1), + self._key, (pid_tid, count + 1), expire=self._expire, tag=self._tag, ) return @@ -152,9 +153,13 @@ def acquire(self): def release(self): "Release lock by decrementing count." + pid = os.getpid() + tid = get_ident() + pid_tid = '{}-{}'.format(pid, tid) + with self._cache.transact(retry=True): value, count = self._cache.get(self._key, default=(None, 0)) - is_owned = self._value == value and count > 0 + is_owned = pid_tid == value and count > 0 assert is_owned, 'cannot release un-acquired lock' self._cache.set( self._key, (value, count - 1), From ac3a3b619c0e5b6eaca429e93cd0d3678fb1ec0c Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 13:10:37 -0700 Subject: [PATCH 031/211] Refactor early recomputation to close cache in thread --- diskcache/recipes.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 069269e..48b8241 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -377,6 +377,13 @@ def decorator(func): "Decorator created by memoize call for callable." base = (full_name(func),) if name is None else (name,) + def timer(*args, **kwargs): + "Time execution of `func` and return result and time delta." + start = time.time() + result = func(*args, **kwargs) + delta = time.time() - start + return result, delta + @functools.wraps(func) def wrapper(*args, **kwargs): "Wrapper for callable to cache arguments and return values." @@ -385,14 +392,6 @@ def wrapper(*args, **kwargs): key, default=ENOVAL, expire_time=True, retry=True, ) - def recompute(): - start = time.time() - result = func(*args, **kwargs) - delta = time.time() - start - pair = result, delta - cache.set(key, pair, expire=expire, tag=tag, retry=True) - return result - if pair is not ENOVAL: result, delta = pair now = time.time() @@ -410,13 +409,21 @@ def recompute(): if thread_added: # Start thread for early recomputation. + def recompute(): + with cache: + pair = timer(*args, **kwargs) + cache.set( + key, pair, expire=expire, tag=tag, retry=True, + ) thread = threading.Thread(target=recompute) thread.daemon = True thread.start() return result - return recompute() # Cache miss. + pair = timer(*args, **kwargs) + cache.set(key, pair, expire=expire, tag=tag, retry=True) + return pair[0] def __cache_key__(*args, **kwargs): "Make key for cache given function arguments." From 713600acd72d86cabb18d16361ebef27bb72a55d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Jun 2019 13:18:57 -0700 Subject: [PATCH 032/211] Pylint fixes --- diskcache/core.py | 3 ++- diskcache/fanout.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 3c0b023..c74241e 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -2332,7 +2332,8 @@ def close(self): def __enter__(self): - connection = self._con # Create connection in thread. + # Create connection in thread. + connection = self._con # pylint: disable=unused-variable return self diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 085345f..8a0a722 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -665,8 +665,6 @@ def index(self, name): # BEGIN Python 2/3 Shims ############################################################################ -import sys # pylint: disable=wrong-import-position,wrong-import-order - if sys.hexversion < 0x03000000: import types memoize_func = Cache.__dict__['memoize'] # pylint: disable=invalid-name From 291a1cfb8ed552cc93a4982f926d581449ef5149 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 19 Jun 2019 17:39:11 -0700 Subject: [PATCH 033/211] Update benchmarks for v4 --- docs/_static/core-p1-delete.png | Bin 45852 -> 44624 bytes docs/_static/core-p1-get.png | Bin 45803 -> 46320 bytes docs/_static/core-p1-set.png | Bin 46824 -> 45292 bytes docs/_static/core-p8-delete.png | Bin 47424 -> 48531 bytes docs/_static/core-p8-get.png | Bin 46972 -> 46379 bytes docs/_static/core-p8-set.png | Bin 47227 -> 48240 bytes docs/_static/djangocache-delete.png | Bin 30412 -> 33750 bytes docs/_static/djangocache-get.png | Bin 33536 -> 29965 bytes docs/_static/djangocache-set.png | Bin 33357 -> 33364 bytes tests/{timings.py => benchmark_kv_store.py} | 0 tests/timings_core_p1.txt | 40 ++++++++++---------- tests/timings_core_p8.txt | 40 ++++++++++---------- tests/timings_djangocache.txt | 40 ++++++++++---------- 13 files changed, 60 insertions(+), 60 deletions(-) rename tests/{timings.py => benchmark_kv_store.py} (100%) diff --git a/docs/_static/core-p1-delete.png b/docs/_static/core-p1-delete.png index 6cf196f17d253df0244b374281281f2a1fb667fb..25907e5c3c59521586661bf0a5ed3cef4937458c 100644 GIT binary patch literal 44624 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 JZDQ;5{{VSWOq~D# literal 45803 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 J($!3S{|n=Q5_bRq diff --git a/docs/_static/core-p8-delete.png b/docs/_static/core-p8-delete.png index f717e0eeef7388d6285ee795a1d092c09205a9e1..c6cb29968237ca40fad85bd4ccab59ecbd961dde 100644 GIT binary patch literal 48531 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 literal 47424 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 diff --git a/docs/_static/core-p8-get.png b/docs/_static/core-p8-get.png index 0c4451b5f5318c5edfe3ca0a793c2a0fd546229e..889c032d38df1bd4bb53f3c0fbdce413759b43cd 100644 GIT binary patch literal 46379 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

gCDD=n$A2lKusJMq^ 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 diff --git a/docs/_static/core-p8-set.png b/docs/_static/core-p8-set.png index 5be3b6cbd0d46c359469ba49baf43466971a30f0..f653fe1bd23e834bef7c62fee5260d8c70861fd8 100644 GIT binary patch literal 48240 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 literal 47227 zcmeFa2UL~mwk=2*O2tyD%B&<6M1o)fC7W@BfRd49MnHn(AfT2x0B%84l1k2!BnL$W zML~&@RdSXrAkcGdPkOie-FJJuJGyU=zK(HDDeV34KYZU>bIm!|+Sd=NC@f%K$O#0u;xxeBkAuHH*@t;{XyBHdC@#oy!6W8$Xc~<*%ZP?iUw59+3(r}8$ z5kHi)-K%4(Zh6Yq;i&aVHuIylR%a}2&zKzJwLfWXV`6E+yIFLT=q3@~)3&x&QetBN ze1WK?wXqmmtEwLx8!sDU&rS_T|L%I1b0v1u1^sOfr{>(NKAp7b>a{%&lRo*Vw``OQ zn|UmabC-%_is-VN5uEG>qMbrEY5N)8d*9~Snk+;Y6Jk;h_UYW&#( z%b+he3nuMrdpalFa&!Aey9Vo1^;`^tO4=q9s(b7+arSI%RquMVma+bpy6WO$`pfb) z`+vcI&aeL8{O9K@e%sGJIV0is>`b+Fw9<2~V@3BiBvzk!h@YyvB%OMHBB|O!`4C&L z&@4f=v|l{9q+5p+3~gy}F?i z6yWFQr+7~!^7ZXiT|Cp@uZkLGIYujGMOsgDeU+Y-r9nxQzB{ z7I<*YmvK3mVQVt=IeTCJRSxY}H zLwPef>N+>G)xAnf=3ujUY+n1Uuj zqIT`xt(oO`cI^B2_B#sP9}`a&`M=(~XHSIFU@J2uB4T}b(JPAzS;g7)YfmOsm&fRC z36Qc=bR8c$bmYjR8_UGvaLdeUpP4g5l8j4ChuS|z9ejLIMpjlcc;5ooAN>Wh<5JF4 zZx**~(C1gW=k4q38>Jl|_vWsEu&{8~LsRqmj1)Z5k=}Yg%W2#>e8g7M^7RU}AA=Pt za^2lEBh~gcG&Bs1j#lmFkPW#obB>UtY1t=ZygYZjQ&@Ja`7Zn1 zxnAMn;k$P2Du29tj(J_$rV{_Is(5W&jwWuple2S_b<5k8vd+n+rKJP6Vs#Rp2x-N% ze|NnnW@H!`7#O7%dZWRyX#rM$!Kdqs1n_S~X2|9F%cNYd1_uZ88%z#V{J38}pG`Y| z1=i%=%2>VdZ{MtZ)*sr{Q=6(?op{oR$$awH91hdoy7Y$|k6t`}=J4T%hm%h~PD|Tf z5vi^~w^>ESqcYwo-l9IkWO{1yp^Q@^U*z!u52on!FEz;~>f!R<(~e>JnjxW~k&L_R zGTH)NhU#oe1o`>*^0<9}d^p84^#1+j3zl!(At)$lS``xU)hDH>m2#-kV#G zzrH0iIe(d`_t3O)8f3v0P>CyaT^ID0^ zSFh4Rh15c>1zWzjG|zdsLc67@DHuCkh>vfNfNEel{+fK^&7ZO(pN@MZ96EG}V#1Hf z$%;4w>DjYpoo>u=eJJOeQ5k0tjeCLH|H7oFyZcOvX@w%*t$|By?(L1!jMg%#OFJv& zK9$uZ-H3%A8X9U+8Kdj`{mWIZMiYjUC{MOlmAV+4{v9qhD<#|Bx@yrkImRV{GOotO z{%4)*qqW5{`6gCbW_>9SeDJ`;vLec-{!~(R)7P(@J$~CPpD87(-rxA;N_n7>`TV6K z>#g2hnzyK%*^OV>oBE#cJ;Af_$oa|AP$iF%y*HOu^H)#)_|e@{lage7>Xg)Y&RFZJ zOgM)PW#Ea*k76GcAGF5v^OG}*kw#IYEQ)_!h zyuBB@AawPFD*7b$HRd*&{P2{YIxS|Hm6mPaQ=`(8o*Vx8^N|Vc(ZQHpw@GK57hc+p zhv+n}>c9QV_qO=lsT$EYQ-y0iKdGy$2fS{X;=aPc6WG_EVp8_HV~eC+r+0l-RaI|S zmszgWmy5x&a&ir#ZyKVtakA3v4;}o59{itrsWy+8FqIwGc(oH z)e*KuoSd8_$EOB-b0ZM+Ym$sT%Vdx@n0SHQ_s@UH1a*AxZ_d~<*7WhS5gxCU!UP)JCTL+<;dvI(=wm`J=xP*hZ9&O*+@Ez|cm9q06Y zKl#yL(uR+*<37|dK-t=kE%WBi zJ$Q8?*U&^?Zkr3^*wLqIVr+{qv2IvLLw2TCoPH#BZ|4K!z(a$r?<+zWi^An~_&f0O zs!%0g_ADMgX=yzs($I6iL-voO52r-+rnhm2Kf|(Fzj5P1tnLce(R$xD*^@>0DvAT7 zx#CL#q{FjaMx$&yN}VTqQU?tR%E|^G$_L}b=qwMXSv`J{pU>5U(4g`0$zBS@3VIVG zJxf-shmCHd;)K0U`}belcY9^%?#L)@4LPOY0soo@mGPwg-iMi8-AW(Pm=%c;vHkgDh_|pO=R*b35knqK6vn;Dmf=8s8LhCi+zcZj`W9J)|nOa zKacDH?pbya7$oKN$6dIG8YfPKi0Y?363hLeR@$PJP*+#SS$o0gW?jH1i}LCGKGSpO z&S?N%^fcxgA*o&7j8!Vz(%KqEnb&^4h(ShjS*V+PD`Gm9hiUhx1P}8L?bySb>xX|F z>b1DMuJO9rgEEU$Gen1~tr9pj+!3+l;pv!-x%u0I4cQ!cfK}_(G@cv0UDjfft`Z={ z-!ZW6x6&5B{!E9yxKE#Slpk&tY?K#qA8T0#a3^ipW*9isI6XDG&%2ko&x11(OE5w; zNN!AS*?Jsa{>Qg`@seeK{`q9-hQsZdeK}<@x=FUJ?^f7#RVpLa+pSD>8MT<4n&@+C zm~Lx4ezdbbGnL?IL_~y4$vtL>zdvV_@s;fFUvDx)d<~r$*RNl98lV0oaqQ(>h8iNU z?LhMznd7(%(S11+#iu9b2L}i5zCR`G-S_Q%#In5lt1cmkpev+oBmP{ph=PD-jLso$ zZtj|Fm(-l8aWl<`gF8#)CSs0vOZR03188>jxld;<6*G)^`gG&$Uw`dvesfpK@!KQX z2OIRBy-Qf7hvcd8?2N7}lB3Y!?AGFd=(j$?(MYXA zMmbq(YHEvlc;YQ{#`YnCRAxC1#yJ%mW;>fSy}o_p)-Bttsiapomzhod7-OG5U%ln6 zPfJ@{IMD8195o%7QvBTaZ@Z*l>m%8e-dG}B9;q%MZd$${XQTl*h;U&0_2s-*FJG>g zl+U=xC;84C0j4dml-^$Us#H6*JHLtyH9_vuN~ z+Gnvsno%LQZvDn3@20PpYN4s4^GQ6QUg(;qr{_wEGZBCykpSmW2$eS$3qBd?sg2f( zJ-pAR{+9DEb*^*O*sN~u*|)E=OjTYJyF@r8-^wRP8{<&^52erbIS8CPt*^Q^Ia6X021F!rtE75N#afswI{^#54uq(Ye|df^hFK=WU!( zt|7qiU7+fVfBjYQWbb^GGp`(r0W>?PSm>@!N;&m@Wq_=U)-S*O0%RZPJks5<)gD{U zv^3=Usgl50+-ftx#)08sO(ZqK^x**u;;X*Cx+#v;OHdGH+}(N$ZImdt{wjYTh2{F{ z)VB1&rBpoT$)b#AV>@z#cQ*a^3T`j`^V+}v&p87Pe7G4k8FtaY_9%RuqKl{o?KRca z)s;H?^%4L=6r$3~+#p`NyJzY%G`Xbhvno!~vA_R?#TvQ$>cZE3YxmdltzNwvNc%~Y zMx^!4Xx1?-6g)T&0fuq8hx@I;E7E-j3Bh6^roF15BY4@?r`A8Mo<)EE&nkZZKXWMS z+zLD?k%y-?=bKD0v&F`_It36Ak+|EWTv9ak36#u&v z!Lg)yY&#yCZ9O_k?>b+TnlXbdFW}Oa|Ik|a_gBh){}wCjKe(&@L-$HwYX8X-Cr+bG zGev2S8VnWvVlJ+pt?L8?6h40ZXxf;QjXkM>Y+n(htA1BNwQ_8*Z7^UIVNV-z0$4s2 z;MB;b_1zl>{bR?T1iFoz_=xDL@hf^0(6t4_@IvkY?XA=mz6;fICrA7YU57QfWE>B2 z$p5g)IgW@I4)oCl1h{g$)uRTt33Kt&A9gHScYtE$iPYMF*Cj#nfJ9wJ#8JF|utmXB zX5iYDgnh4ok=775DU~I&XzL;fRlg#jx9l6gZxMhoa zeAd7XVC0lzuhpXiU@@&S%%gi7^g4q*TXpcBe1&8v#1I063f_S7gsBm4 zC>x#toZ;bo57px2c&X!8ylC<7~7{68ecRG$}hsS)iB44qp#;K8Q z!#%ayVp-oVst2W}rUG_)^-X}g5JN>=u$}Q1e;n#~3YrCYd3j58#pvA*9@3rMkqcHy zL;!cPb8u7v#|#Y3nLQieTS>}wJXw7LK-AmcKL$lIgPEI~3+UqG?d@&sQTMDafez6t z=WxKQCfz1n%C0j6nv?c?=zbweL^bF{4R%`8SK z7Vd%q?N?4phv#dbbNvyrkXt4Kq!J-I)RltY5;FlZ3mY4arhl}_2h^a7c8@|$j1zT8 zmK`ov9|z0~cu`&=KOKW-Embn9$~q%aQFqUBLVwB0TMIm#332`T=bwBGv7GNu0Km(m zG=)RM!ieBgDY?iD0jlc)0=Q#4mJQMS(AeL(KHkQ$pn?Mb5BZDb6;PdVN!_Hd;h(AoX6394Vss}uLG-FNE) zP@m7;yTPa4KkSH2Z74-GrPp3C4&(_|bcdxIxz2jBa#!5}(8~C;aSy5${Mr5A|NQT}@n3W?G?(raTDx{PrM#}n*p|LN zF_dQQ?d|6ZxREB2*zTd0L2e!fMNWCr=`|uv`}=K;8mO$Ak%6&|O@P+#wGxQ6ayU!b z?u}*cWSF**t7c1)Z&f~rd<@F2W@*vYt9N?JjYohF5kL8MG3Ea8p^owX0-hAJDp4m# z$L+6k2HpxgPYzc>vXCd)0wkYSszp7@wIL8BhE@Kwoq9z@xk4^pyofLv0+nLDgoM^` zS5=8aowuO6?MSz7PeZmIKG>I!ayi(@v?syAR7^0`fDtG?Ql1NNXt={SIUMhI*`}dIpDvG#+d|87I|CiVV>p7ZjM>F5R0nK@jk`4&4bh2Ml>z zTwIB;#c3|#r%cHKp>Xk{MaN|XaGdj*o=QP?GM1tUr)Up0#5X*C=U=}b&a{thveVc1 zCshPLZCl}6%d>RpsjbFtDnW7qZH7^66+H9@H?fkfd8(_IqM{-=t<8Dz^V!+e6cukI zCMJ?d6B5FQ*!cETQp*Yf2D0PUz%BAW^I-Wf4=Wb=+SksBeE04E2_+D5$b!p(XuVbDof_>}-ApY9Oa^q`1DVuWM^39u^pM z`HvBexMN42IL}PFz}lNimJ(+-Sc~NrWpnm<0^?91-)^&b@!}8wCr$fI*^=!H_Js>2 z0)O7vwr3n_L=YmtMt}PBNyZiTRqgcYh$cJy9|1CG98$Myk??{WZB_6QYz{%Sa{C!- zzM=-nR6uS&`{Ls>&{JGewuK#rp$oaBB2IsNLODM8J&2lyEGHqoXE7_cS)BR%BPeQI zJ6tT?w|>Kh_X)ub;2q*FU7E!xy(yNn-pI&kFDh`5cUs^)#m{`&NXrspiF5notL2#v zmIeA0<-dK&7dNljdeYc98qf_(-shQlZJ0X%jmOn~#f(--dQf$)`f~e~gX9bfJb4_l z453kgQWa!$ehj{fCBFoe4z`nNbKxDFo4<^V%wlftxD{JYHK+S_3|AQuqH8S)if?Z} zP2|Gi#1q%SNVRS)KJoJ=Ec4}1Ln)aVb{6I12PB|)sp9wVl|g(Zf{$hrLyg#tCGzZzh;#4YM{fOIvIdZeAx|Lz4i^`1Rii{5TEZ{JQKF#ceoWV2p;9F=lf3i4faq(#EwzvilxA}4 zq6Xl*?Uxs`NfiW5mE2h#Y4eI0(n=z$2&W?zNZEJa_c=^kDUW~o?8w5x{ScQ6Niuj{ z1(qF^MMZRcPSouAOZBxc(2+lz-nv+R{+F3?W`A&Scy_t5Fn8L+Rf&^hfYrza)vGp% z(+lgY?_Dgv%HtLR3VVCX-o1O@6ci{xWG3Oo`_7%Ckvr&BR^oWotrc7Ri)D!NLODn3 z!@gU~ALFAtQmPD-^!ey@Y0NG+4{&M&MPd}un}z}AHBs5Qg0{DIaL|SfD>v4>NI5{N z!um12;j@rPz#WtaOlaCo!$^#w(Dn=KC#~gNzCEAg|I*qSury0;=;ac#HS0X0oXJI=o*#jLG;dZgBp ziq)dMtd|NWEDkX|*S{BfmI(yHD81CU*B)HLC|MYkN8BcdwIQH9J;=@?&9sjSBGQ5- zE{`|LmFbz9n!0iOc8ZiNom`s#s(~GbtiRn|5pc)!63Ba(mFLT(Zs@IXO8QAe#9Y{r&xlN=n4kA`+@ORrA=u6q_p6$Up6QL!yJyTpLN)SP+oq9>%@p=ykYj@RjZ<#3OtpV zpu`{HvL)^NSxHs;dZ8P%7@bW%g6jJ~O=|Ajw=ZxEZ~;uK&g?mJifU{1P>+^F>9eT( zLYHcF^fJ?)LPM~ECuuLp*sAW(2j(AtFaybU*MgN>z2f8Jul%-Rjk~)$D05U+EI-;TPDwfL&o?Ax>FGAIp zn>K9%oBVQt+#348xOb&1A_0=MK=eEJzgjjwIQfMh-6(>VN_JI#{%%kJV4Q4{U%h%2iCCp_L5au~-i`tOi(sQ?SK5pZ znFL7MypHTCyt7t|gCN5Bn^>xfzM>n9j2d4=wPd7N)E~aPPPr7|d8pbrNV3Mrsbz_B zpo}gP_?oX{KsSmoXDRRwXZ&3zlbw?VG_#J${=}KyW2{J=wn2woZRVVX<)F&9I(|E{ zL{Kf{scDAj5@D@!5EfCQ1{o0=k!nOOZ3XscElyZHz`^jnDJ$Dj;*y<}#W~6Je(*r| zb6VY$@Cp!88TLJ$<@U{me<1aI{GLYlEsa}gXA~}!q;+n$??;C_$`o*+W4}gW-|OkN zM#=s7wtKj*(FpXP8dL&KvsTEBkQkg%>NofFZ0Hzx*6MCQ^}|Uam(R(?W!ZMo)M>>T zGiEIQ{dcSS0@{3eDZ1=xB{GAznu2T`QYW^c9D>b77sn=r3L#c{mqr|v9APP`!)MQ) z^(Y#!7VDn~CL}&*jH@KIX!!i5uoZfK!i}+nAopWr%+x-e5sNhty|kMuq9&i=FR@SCeEFa;agqDZL=qO zGd;GL1LKoP1dNE|E3OIpl)5pI*^%;6R-ey--(q2mGsfq$SpE=YB=6Qso&70N1Tua5`!kfAhO#dpxpNB0)bRvat#7xAH=S# zC=$z!DGOJPMpPvu0DL)NqmAyAot^8o~UU*1+`G`73Hj9&T=#9Z! zwg>d2A0l=W1+1Z+zK+iQ9QZq;Jq8B`N-5xg$$hb&MOksZRP%z3fk~f5s-R>4SC0{_ zBC|rT)g)kQXp}3@V~G@2Nol%HgE_7fJYlfmpROCW4$h1!D~|E8qS}@B3_x zbqZM^>P&Uw?2$Qh<_K@v^zmPQg$2=pYESg(^ylY)QGWPGZf93RDOL#;b;+`2zTtg9 zh94lcM+24V=+afN>3f9sln5w43<2_sM8Qs41Z5xwIWvaF+Jc06d3PXe3VH9MXOgFh zUH<&}bD*V)w6jgg&cmhz2ALs$@+(F_0e-moy;KTaPRBt>(Gz9eMg8mW zc)@Uk4BnM@VJ1#hF>tE&y`LGP&c8e$^0biG00i}5wiw;=aa66b|6GzJ?U%KjbH8{0 z{(3-FVW@S;Hbuangr9J_&9x3-XEx|O-9eVEf38>|n4J&-i5^E75oFMULMU;MgpX0m zru7!QQbzxPSwoy;QZfaUb^daBS$UGNDgzZqHXkEOKRuCLdnnFz<9}Y*wU#*p&)^fG zm5o|}%nHI#CE%&mf}O)@Bb5v1rI{_fKEhkKY9NEMSQmoUGTZQQBz=e0VtLTzZq;xpP@W&!ZG#lp@m(!*O&tnx!A;-@*e{fzFBu@_(-?{#4r8i3q3-|08ESU{EyBh(4azJwDl~ z?@dzH&DT=cS&Quz78~Rba#`?U|1+qJTBIuTrM;xE(96#+^zR6^CM6!pf&8rGu!N2B zZCRN*EVom;!ig(33 zkM_pq`1ycr~j#OClPs<85iL%W%_pYwj%00l7bEK+4+;&1fZ zw8dD+Fa=4EvZVU7F08Ia0xBXQo9VZoM~-%Iv*eMqRX|$q6Sa+tSFSvK#2s6D$&w`} z|8XBLmXmJ>hV>Z}@RdY^(fya_FboHkHa0Ty1){u^+mEz=iS2T7a@xNs0>A&8%&41m z>Y==Q4t!06LsZ#NCP!^%@sE#uCLe?xO$i+#E5fod$79aW6g;ovpa0?j+aYdJ8nR1} zMXTyx4&?<4ERI~ROtM#lbJvFO90Dc8gMj7u$AeSzVulb$js-E^liCPT`Vga(#5s@# zdZH|XME?##P&7=EmxwcFbC?~>3wzrdM(JRQJm~mVM_01eZH;H$U48FEUcduh?@yJX zg+Lg}C`c0hVOa^k+bq@nv&XT;Azz!C_!%Vj#?#LGY=Ix4#I$-ZP8tzcEzqVG$8F3Hi}`gw=wNWDVfH?XRQ!M85e+CV?D(nC3d!Z8>* z!2}!-7MWnoL;6i#D3%An{YqG!;gSre1c_o@`}&{!{%&dAELaur8Z|!Au){TG@sY?$ z@D*`CCZLmN@EnM1n`I!0=cW}x>FNDvjOefqbCf#Tuo01N1b(K~XdT5JSbSwDaoD3- zRw7n414-lqs{XB~Kk$;G0N?f@Ag;ilOvhcF3s{*L})-r~wY8Alg?M4=L7gxA`Kem+y zT|O!yYGZk&%tv;7Fhf+|kg$UG3z22>=}2*T?c9+B&>$<5FpBIgpsK1$t_c8iCYa@J zdongJfujzcAPhLZDM}836eo1Dt#dgY<#X6(b~Rv*`b-CHJTgF|aMT!raMm`ujt0O+ z%FiIcL3RKj@+hR1DE08-1%j-6XyMbo%# zHhf>ckiN0q!+=~(k*ZaW!x#1#hC)I)C*OL1{P1}9plhKO+@)Zzq9K+eK1UzTzfuF^ z|9dIcmK5#IaOklSH*y{Jp;iGH`t?xz_(Hw{J-&>ek8k^Hm>U$x0f_xcvNC?gq8(wO z$oVm#6h5|Q&6;EwEf(;|=_3*6l5X_OT#q~wM~@!eHt~(_QkvJUd1{D;<1j1OqP&L7 zZ7)d#NYv!4E`o;`DPHi_Np|nRz_>tJ7a?fbFjIK>=}igLtXaF3XbYS~1dI;M5L|PV zQLfu~XLS6~%SEdDnS{V$PB{owstk+|AA?M_|JRI#9Bjm}P*5jHC;Z#wCB-vgFtMIA z5{~VKTP6G;do~j%hWwz6*1!F;iVx~3@@^TRGgs!2U2mQ4GEw~x zxWHjNQz1Bbm=Nk7v`ykhdblxQ_7}0zx0v?%ZROVe*eQ0V(!g~fzE9g9p-a+d0*r%; zcr|g^zONw`m6o3F*N2WSE=tTmmtM*65$f$hxVdrt`XR9RdN64y-_*699v;Our&{;TUAXWb zX^2uOZC4(YmunQ9bV~RAK6yw@jd^?_c%*Z)Z%;I_kV3Btg13f-H?o{k=#Cw? zw>2C@-3!Gm7|&R6^X9~-rW+0dgzy_QT$%K=?mL+Qr@v;>DP_=^!V(gdCht)7Bpa7- zgW`~?*|mGO9jZzhUw+)|vX2zhKlh##+&?xx-bv7mq1(Ep{JiLT!`BR`KBIu;SPrkXe@U!Ffxc&u(pFoWX@_RZle#{`rs90nP-3m{_%cSUnrgC)DDoENpEh*#g%!iFm~270-XYPQmv`!RQzVr@_Slv0As5_CMKp z3*L4&wmryLGYnReT+|s(gJIYY0@Qyb*P;58DG%QpcCq~0A8R#o&N*S@D`H!}k7B_G zUcI7nb+Tv0%ejvK{)Fy(^ZEy@DjlVohSpE0t9t3cPi%wxh4rhcRl!PF)rMiccVOgY zB5g-N@p=QU6;;q({88q)`RWPWZ2amhfn|q1Gm>}s%T*rJ&v_TDZEVzc?AUQdm6i6+ zhb!acLKt_^Bw=p;>FEU)4CgHVYy&&1k^G zKV_maSVyegJo)rG)$pjUJ)f}fmgAcH3)QY1mAN5BulRiZc~c49g}jif>}qrgUG(Gg z0i7TV?YcWx zjedOO&z-E|_`u&3+ zB-4!yvCyl4;?|&T3$(r&E!E(2^q{Nj)R%=6`%4CcrbJZ8x2T?)oT!L6wALWk&4AkR z_UyTYcpQ#UoSF0G%a@0nj$a|uF+n926%}GOlff((Y+`N0>ibE-^9xM@+bZUZWl`1$ zkaur5RSgcwN8a6y3UA;BW(ZUjR5pf$IiX~!GfLrF9!AD6L*8A9Rvo@bbf2(8rU4ut zLOhpm*vX3dT7gl*YJ}d=b`pvbu4Y4~b;}Ku6Hl;@bas2&3iG0K*%Z;F@)@u`E}WsT zYJ2M6y#P?BDkY$HWslUDk^HuttQyYVPDciO;A(q$wD%8hZ^QaGlZS=cf$Jbc~$ z_W{z{urrl|+}xtiy=<8PV_q88lYSP_~{tV;t3HpKcMU`u$A>@>V_ccmhRK z_j(~%UVMFPMKGF9iwX(^^5p>6nS9#X+LN>Pe+?;~$kZWvp5nRc07Z6A&W)MFnXY;cO_<3K;gdTVaL-rnRi8ZwD(X%iPC-!yCU;A>z?(1@ z59!}_-QCm(nm_u-spdrC_I8{KWg!SnRCl2R7+{qT+7ardl2enbwppA8M5o5`lvs!; zH#|Hlffmk;Bl~(*w$Qa$#|g;o$66=l-`)sBn-KbPDeYGox+tSp-n1?);hv~L6l`{f ztEjn`tOz!$>VV8>MvOw6pKdL+&?5O@7c>rDcV*onH`O2IIiR0Te*gXIPzf``wHhi6 zF=Ll6U0M$xJWAz{sMfLR==e!OIG4e$u(NyrN4SBQMatCHBYA9O2qm}*stko0 zIEs1Q7>CjuLFYx2Whd-lsOY z=@Hus<3cE6E0qL96o@Rn2`t_m@tCf$@Ylc_yu+_oS)N8$qQp+}qY?sda2+)uDvY_+ zq?8X8vodulP)`txyr%}$4NwJ;saU-baFx1rHs#?x83k=rlMt;ucK0vxRxkfvb`&H@nb$pR~P;d z){zJVPy5MIU#c|<(*tirx~|ZDcIE-{0rmed)SwUXy(uXviHk>;AfG^U^NI9~3?X#c zpqRY(>R3XT0K*6#+GKj?T-MFJ*>wa5%OTVyY*-#(@FI@{G|lN_cv{eBfXd znfe9>J>!|_>ADkX85xJCUEdBC#eobT?4bo#Uh zNf!ceEPS!3p>~20Ox86_K79yu6%FgK@9Kq?g|7XCoH%4cbfH=dz63act2~y~$l0uq zy#dvlCgh}mcA#Dn5W;VtHVVgQJ%JUf9e$!Ol&Ag(UE&EB)mP;?)02(U736yWNFg!1lH;>71|=+GDS#>HN+H1zW@fEjyDkzz zjuagUfKWLFv{i#ZE`$cTSX6UgF1Y zj!S3ElHPk<75`y-n!n_KEV%w3jIPBVqDc3`&mwYzm7c0^9Z3!*Sz3}!`)|b@YP}Vw z2lpz0e`qUuW+aKzWn=r{uFpaT|5H>TbA}bL*6`$Jm4D>_WYW?;BT(&s@t%KEbpD%r z@N&+;i!X-DGplj9e%FSNOV|qMegCY&=4kzIBmMv4I87xs56>=D(Du5Zj7!I#jhBW0 zLltU&x!mV@{ryEslx4J7@6FqpwuTntf5B4jzfjd(zZ;Pe6KmpN`74B(3t+b<@#M|m zB3dJ#kIag6nG0tFY{f$$gsEp9J`A!fkTp>m;RUy!v5UwgA6_05Vu_y)}8hY~DE$i*=T@h9W z>s{ACW(RDaV3W;j;`Gy>AX(6Gp8WGy!qqWJ4cvY-RN7sTLhQjiCbG4Oe;fLeRs4Wv zqd>z!-6B&yJ}WZw+u)!&N-zT#%D|EP@2qh!z~~`LN%lRIG%{xN?-7LDjk#f52EU#c zdFix3#&vtVP2kn^KYcu|D%b5r*@hnGSYhqBPs!8&EX3B~>TTOU?2L?zBvCSD1IA9! z)EgL%(OmWSM|RA#(O?#AOaNQdl31N&OtZzv1S_E8|KZ~7`J4tUGu0exw2;(oS6Ft( zfb5$nZx3hKMxwA2hVkY-I+iiw!rIov@giD`BTy-UAGER+WJ{d;^SZC-F3sQgo5Hf6 zeAl17|9-HWCHQ&Mddw373m6-pYS}0X{DT}K9kULrNJi%3Vry@t$2Z3k3c>Zlwqjqj zAm0me9>5(K)t2+@zsRVhP9J!xBSIJx9Gy5o>LdV4r5l!4=ZD9CQPPre8i=5Z4SL&^ z+8+?g%d?zpXsX(d3p1!&6fD7L+Lc+&j-Fq$0^w+(AtI>gskPHGtN#i*cSq8$Tsm`u zqj|4KGd2VS(r6q{qK8a^U_P2VJID8r0>8Ne3IUY1-07W@_tygqQbS%w8s8=IT5}|45=%U z29JqVo2&%hizA)Tj>sHF`C4int_jB{g&)Z~k# z&Y;;-p#WfAuxVAfPYv>P*+Zib0r}KH-WqhC!P<$2ahk4#2Z&HI(9f~3@NweTERUNv z%Mh@L5C${+GNT7lGZ8)Th-3qcO@5zq^~{Ax*TOqfZSGSFq<5h|JjNVr@&oo}Ipn6y zDcH6jO4@{Br_i_^2=39a!jNxQSw4RwOL>MBqh_phQL3gre9^#uIP zLvyFl_Q<0?T<9@Zt?&+w*};3nI)%(S4ZlJSVd{BPp=0w%q-Y)avf)It?Vqg7~2AajA`R7br;XuZr5 ztBK4qc(A8zQlgN8sDPvyVc3aXf13wM;1fHc=vcyjW&?&Np#1B|89wk}^Q+c5c9_fr zn|Ul0YLcHN`m6-_`Dq#<%}C^M;e)g_mpO&udPT^Db>Oc!!|W)O04KJ`T4M7JjErc((`gHPc$YTT z2!vY`*B2$g_jXOM7towBEE*Fyb~!XkdR8MUnb8113`*RPzj5<9flOSJM0{68bhtby zxCMMb%>LGX;+2yDq$%Q|F~xzRkc$8I#}GWCuL};{m2~TR=lBFfkB71@&&UHs zov};}BrRqC?S~)|-i6aQWIAUBQ*KGSJ*#+RlX1qgQA+J~g~S?+89;&z0z?>v_!wGO zJD6`z_alW8*l!=nWsN5O4$K}N0W<&hbyqZHrKrlxX^kuBrGfnWPFBW z{}V!CxT{5=KYkiYwJ{@8*r)7u+^hne)y0N6S(Aaoodi3WMlSWMz_mF1gdW(hW* zMg~#GFtcH_o3u|)^-XW?UiAC#k07W-|`VnO$u=E?vGXh*dHDy+FSE zUVWC6y04hgJzFzTMBQ|8#GMsaj`nQ9IF*UYJ8qXdMuvaO|A6UZ2X4>FYwAbBHbZb@ z*+_bN%+1|fe6*8~=KmTX=UP)Uf`m;xHRu43^-J<*X4E>lhGRMe_SONXf#!rvqZN|# zwL`p`?D`re1B-TJiYj_*smp{0%+bIUX$K2x7XxlW*i51st6j#D^aA8y7Fq^#LLnC| zh8b;gZ+*q$&!1-_(>NY9OlhTXFQ>Q7EBsX@P$m+-Au(RIz^~SzNT^^eM-qEkVz(;PG1H#O0|a{47SX zZrM+J=K23rsZvrpgN;`p9a23x1)!#_xbmgl8m!PEz(Md&nAyV*%OWH=1>Z{o_g{UE zm;9Wt@weR&do<9U@9*#5-2H9>b~G6kY;?#ccleg9Kez(`{F0!|)+y#p?z<2xgf?wD z1o;A*P}3aD-dJdb@J!BFurMTrBjyAvy;ubH@_a!vGA#3DCaizky+D_s{f2LH)M3lF zjdRuVj-y+B4fK?-p4Ds##1jmRqxGxV2N3@3oQ(PSC-UjEt8qVo(71m6 z`ofPV!IkuU`@WG^dbzK1tBGe*SZz0g;&Czsl12yLx;oB^SWq~j7q3_$2qI3wcP`rw zkDXpKzUg36&86^Va+fGQV&_@g*+I@^Rg9z!A%PPpfxTumSy4WooWbUG{Pz$x87&|y z6l>RQAlp+~!nmXluw`3z&0#32K~Y))Jq(9@dtnLQG5GD-#`7Xc7|;Nbb2sEO3_|_@ zL-v`fwqlnM*rm3*{cxmqF8qqXXlG??G-#|k8zby>)J#7VE%l0&Xz!u0S7qTU4Y( zP3#EOu|*F5GsKta`U3(jnq%(bD4-lQPu2!u8}pwyXN8X55H(4f1@DpX+Qf13gTdNm5ogX6HkZLS-c z^w7>gw*WZ|38GT)2Txuz%urFO!~>$LtSf;_;>>a$$9ZgyLf~k2+}jS^kMckWB^YwE z3UttNdTE-~7neAy)+m?(vD0>>5*2n$@`MxM*sR|MNlOjjkGisvFp8ZmKLC?ccLUgb z^8X;<1pz1^TCF3C88aFDA}P)|isti+v#G=2eV{CvR6kG^xcQg7TP<&HOM_s;f zQv%lcP`V~o6!?J|f^0!So2EekIZ{I`QX_ejv~6vZG3@y!39aV^F2DM`HsC_RSCE`u ze6kbWNh9-vE?!lJrmliUa{=oW6zKTLmeb8t~N%qEXJ*pv(83i0%^0o5V`7Wq;AZFtD!{r8w%-+|cu^E(3I6K$M41G*j{Kpmz}n#JKa5le9j z5xEm|bmF|(%%1S7M(&L`u`rAch=!SU^KaBg&!o}09pnds#m)S+P200lv~Cu(+TmW) zSO9}`Ys@~z5oz`@;5h~vq`I;26`&2y&c13>xaC^QhOAK;iPTKjTL7p8y)hkb2(f6r zoSb3lUp!)r0Ss(GA3{t#c@JPY?BZEn8Y}FI5xNIaPMg2RDiTvHl`WVHPWW1+E8AxD z$Jrja^{-i3S>&`| zaplgJ{yQP~{6@Q4md68kQSRUdW-`XRQqMaI(S%HGVO$zw9LKpVzDU%|_FshDr+%;+ zuj0{n9EFBpWuX2Q7ERcq9QyLGFUq zs~`%JXak@xlDs-x;pX`FfOO67+t1&;5zHSM>bQm6oTwvGlXmv_71zGuqxrZ1Lc*XP zzVAq(uOt8grG{HBe*E=s*g-|FUhT!5O=*BQc6DXbRRbehT}BXFvw;MLTYQab^aV3*OzZ#R6x)JE;UPR+>cn56;y#P*vq?UN zj&6Pr+YEL^*)pSZk3se5*1AvhY$J6gu*)#b>UnDl00~OrfG=y4urO(vQ!b?rL;%a! zBI1;g6-_ai7Yzo9x?+=W8RI@aweca#uRO+vtR5PLbs8=Q@`e_jl5vQecvO>WDJCEw z0QR)O!Zm;68IR{^=o^$ClSGD^d2mmT)!%{Il)BZK&8r@?EMlcuM1+EUqZSR1miE2F1Ip)2g z>FUt1FkE{kJVVNOM%0{|3`QD-6>t6&SXrvAiH@Nu%^=_oV)`N0{ScO*Fj5L~|3Rb` zayZcpjSRQR44NMdI6z&M;DTrxE?!SjIMXGRuj4NcIkFUMq6)^e9+InRGAlLtLn3P| zPryi`4>XSsI#xTf7m1Mrt?y~Hf*Fc3j0-LYPsGnaW%ChZ+d&^mbS+6m{jk9(M<0Pw z14TtSW{;CQ6>)nUr%C!4k0t^;obXh1FM!<4W&l7^>jA*R!; z#2`6W`UV2}Dh80eB*4Q1BgvcRSU5_J)<4m{3xFXhD0APOV z?!q45hsSc;IMC_!cITmplu0brtzak2(=szxVTbO+8oiCt5$`g7pd)l2Hq8Pwi3A^f zylWq3QQSi9|H|uxh<@6KQ#_arxfScdxVgx;#LC8I3nHKKEpFMG3>numcydX)mRLtu z_?mDxC3+CJBmn!&a2m@|y6(fizYSB|Ds16|+EzsGZVyZ3@Q>(c4nFaQc?B^14y?t& z2$$56aT|P(-|>U#)+h5J3lP)12@?XEjHbu6uZY+8If9&D4(pPAifvy*{d#oqD%ax5 zZ^7?RKfV$j9eoE2iQMO}aXPn=vop=v&aZ8`)L7)03_4X;*U{*br8oDY?k*lM z--gu%04JWJf}{m@BS$3u)h&GM1gaxA9?Bov$*{xL=kw~=Ma^xG2o%Jqf(nevx2kGEilpXu%!LubQ>IxW=o4AF>DVRc@7%{j zAEcr=n7+KA5|%+ugSU%XP&@&3d7(}tv~{%sCPy1sznLO2A}+L_*B!X;6-!>eYu=)DG$TU794U8URO=TEXV-v2Kn+m!iMTB^RDft5 zw98|&H3MfNr)W~xMtHaV{_W@-jY%s~+Ch3&yOvm4(gaET2}{MGcuwjhLJUbVamTz3 zAM_MxYgZlUTPuwY_P=QE783V~Y6;Y#F<_g)waJ&ST8H&EEFS+}H6H8JG{CLNvKUqiZ-8 zYPCoZz5@cM+WM&T9kv5fupg{&GZcK8kAZ{h)6P<*4MYd362}{>4l%8meEI~%#$Xeyu za}uQ+mj3F+Pt8WK;}oKC7BjiWAy;kbc4(7TLubrNdABUEjceilTq2@#5I~^DvMLOb}D4(;D5m=F6V2+L|dM4-aE{L#(U)z zhaRQ}9QUIyRUrrn40#Vk5^yq|}x`;jIW$UIl6FEp8xID2w9^LO6`ToT#@bKK%yF{ zFAzh~c}w4El9|I{_TkY^m^j~mYdpV#94y$kLty<5PsfJ3i)}_XB&rSaqC#Pghw?4%9~B%TBIqgl(eCOHxld$yu7CngkWR}(p{FS8f(8;F{tZG=FQ zfocp-Ivj21x{x>CSOri6CDlNX;2^$AA+joE*ohs&DKA;RoCb`o$5V&sQi@GYQW&EY zoA&P_4`!E@1N0HuN*z3)JCl2x(9n$sHtY=!l;90wE}b!blM^hmHRX$zE)7HMHUT~y zhe$8(K6MW5QKs0Pm2zkbDhDOK1TVpts=ynqLNXGtB3h}3cW^AjHxZ<|Jl*?v*X5Y$ zX?VS8h84+S;PC0>hkNVwt{kN%b%`z-&4jx^raA0F3W<24k$9A}Ls3?0fps}5R z2hk9A$wElq=tABD`UaGkNm@OaZCF%Mp$RvH7WxG-OY`m*SAGg$1ivv^0kb$b&!6nc%7!HPr~W@n&N zdKlloKwr3nnk8{WiNQF8a7=vxG|G{f+c1@YD6kiRIfj*PAZgU7l!uq269|mPKOoA+ z1Cfx`Sz=-e7JZrzqUWiLFJ1A0!4gA7PGfOU7bVfwK!ee+f!P;if%qlQHr66_NzzSY z^^{^;ko?I6CGl-X*?_*jeoTQ-JRD@`%21M`BGh@^d!PUBzs~yq*O}H?>zuXDTF;VljJoOW^4~vH5_$#Hxmy6+dFcC+CnP;n(?PtHu1rUpVHEgG|{<`z%1D_M_r77=4!{^WG zZT;`0e*Kb2fA;t~{Qu`ihcLS%wGMaUuC=vV_Edsrk9Mxe(2-2(L*yMx<+`WxY=ut3 z+3QaV1(>gV`*zX(DSm%HUq_ZNq)Q?DOR)ueBMR@I!vdq0vNh8w$?J1|@n=;aXr!xh z5s*n9-^&qL@ab3xFWiGJjHb-$hd*aJdFW0nMQmw1EdUo^Hk0;O!0;xtLTs(7x|8ta zc->u3LU3u!!ml5nJv{y=v{V%A;rs<8?gB4wZ(SP0mOQNI*+rf^ch2d@fj*-hmzbA) zu3e`ReVX!cIz9)_=kSKh%ynU2JUz+sW4+(q^^{jLEPFm42ZZTg8MqObJ0N;ml0XL2 zvgtokbk3UowHixG!Y@_}JpPfk$maynvEm2P?s9ye^*{IH=8{w);>d5i?gmOLdTxvP zzVOA17ipO2awV7BLvveCnUh>(2fE&KmGcG@#~cw<%exF<0`Wt z>-0Kz?%a=q+GtSR%+~JrR}^vc*?HuHXDbud58T8LAHlRbouhHu6L=6gL_CIX>{-v& zGnhnU7BUN^@VFS_pzwQ<7`UMv>AHXN`nxd;bKBc~bhrNTD-Ue2@SHQC!4me(onZoz zu0LB|-KEZ`&$$wSRI&eM@XA-sb?u{TnWtf{i!ATzIT;>GIk+&p`EH2Z?1 zci{!`?y>@^p!|-htvSJ0&WM2=>kYP(^NNII)Jbqbg$_P8K06?2#4gu z+Xg>Y!l6<7+p?6Rg$Hir`o$G&-;4fD$wJY&r2i}H-lZ$QoStCSV3wFAk#mo$G8x5+ zhyweo&-94(3}i{a6Zfgi_9E?#Pjl0_Pen9#kWg;Gt_!4!^oeS)B5l}z@Myn`nmYY2S{@~Jekk;mt>T-ue^|YQ|4;$Jgb6Cec%I-aTPjh&mRMMfv{V{7uIoBW#Y-InUKvfCY*ge_BDeMCkHEW?{nw*)O+I(7)^JUIkfy<9 z+u@;4Qs*7<+M9K@q}bNVD(vsStr$B@cYQ|!;hRqfrHpCuctCE9$aKu{j&NAV6cO&o zi(lWuA#|sb0wNi&TQ~mW$B)ZatoNJ}{4=ihK(c~I{gH$ORb}@ZH*SQ+$E$oQzr2kS zUp(v?gk|3sPnv^%vZ4(?zpr^;Q{!Ej+gV;YKS1ry?c3NfW*t3oVvS1cK*K=UQ`X7p z-M5&9F$oYjT>QW_VHIeh8Tno2R#s!ZF~1Jn83lwwup3~p{|rTCV1m*;aOuYR$9 z+Mu1EdumLaIMMjeKf7C<+C5>ygU641OS126wa_diQYrTDFCTAFbZI<@Bxd*SaIO@> zqWzk;)E+yw5V0^@w_m@0ag%(sm?9-8gouOJF2W+rIPEXH{9+#)OPfL6zqd3d6nb>( z*kMS|E(X)XevYtIpC)^1S3!YEu5W|MQpJVKmpizti;hzI)s1^D~WoSuFQ&5PWUne&GW6gw#>pm(o^nlTaT_W`74;?x**SO}+ z_**ZWzVFA{n8+5FtNvJ5*Fgf-Ka*uVb7Y@4z^x0Ti6nZr4;iVEYf~D7QtR|I>|Xvj znUb=ttjzJoLfvB>dP*XUV|0$sSp;g6m6s2{a6ymCDgtmd7{xUy+TY(_n_i?P_Y#WX zNSuM&$}ks&Wby3klCWl!9%--{0zSz2A)X5&>`EU zOQk}k&)T)&+|pSPoV@GMArWBvps-ar9jSNV0V{P}!8MB9tmNpbBKdSVJD_DWU9Vvr zTQ|bt%-Xz=cEixEUp(F2kE9ozP%fJI3?Y2;QisMCO4FinDwe! zu%+X06Z4J8n8v}uhE%VjZTi~v>#kH)_E&-p>YojrKY#w`rlx5#XUfqkcK^hmYOyUg z&d%ManUs3>b|qJ&pF6kFd`JO7M%%l}JYvhw3qzIUVn^?7kJ)o`p`xs`Id1s*nT7+$ z@87jciX1s{(xlgL^QM&2NvBS20qUxdF64@B=O)-#KJK3Q{Pvv~vYhFnMVb)EbKf}b zIeS)@nFI_ylm>1_9yl;yt&fl8oH?Ns(YX&>EQcu#!e;P6$FAEyv%eI-N{kUEeV+uZ zSTRya(R`ld4%EN0I}#@v7>b;!`6!&f*w(buMfl3S4T;bpzJ z{`U5P$$e+-8F>`|l&z}9W}4Y+ZSL>0W^LsJO2ry)azxX}3cICI3{!BR~8eVXa1y4Otl+IB+{7v!Flw&}%wi?}3$q%06e5C;A;6OY_6R zIxbwic!a@;MvYCBg_hqV3xCv9YU}Amm=6iF(42eY?{Au2yLQEDIqYk`bxVA*MeIE# ziIDU(jvUnm*IpC!_((fd>=e@i1E;WlzOUA!x>c0g*|O^z8@LJ4>GA+` z=xmRVYr1GY#Om8>SqZ%&ksCKkfh14H3{*K8XF9Y!>BL8S4_y_eulT-*+qds9D%xgl z7&5=pHoo2+X*KEiaod!Z3`7S;Ia+52m*_<+A z*OXXK(KMMoJGv#lV`}XPc=l8>J69!C8N==m)?|0p_;>N2}^>-PEU*YI=a#zsX&c>$&gC#ye%y*hg2$VMo;Bxv>bFE34Cwyzu-%I52C zBNycLr}T~3wyi6J;p9wBO@Xf6xcaFW&dX)-VwqFcTQMAzUG(YdmZ1y?oVm$`LCE

+BK5N!2$4l!JFg?uRQmC2lW@yE_QE$eLUEGcQ9?AD&bG<=Ld3rfu0>7|> z*OTP)XLeBZ*RN}xcR0)@`6nhNb-?@+r8=P-CuJ(}lAxI5YL(0szitdJt9~nysEv<4(f6}@i&R?XiWMsq`4$im0?h>!J*39Xz%hc!z!f|$2?e4W4^w(gv(!~! zu%8dPqwv`?mB`4*iGGbknDK4IwkF9 zz6fv7DKj(Eoclz-Sq3+}p7yooaaWXmV3Aa~i3XBgL&a4oc@^F85iR4_9Id))>d{#K z8w?~;)vYhSS=WD@QY~+KqD7!oeW{SI=4Duv`I+dJ7nM_x$? zGfCZ^;b`&ZL)=ISBgc)~EqFW)y>aeQ2aG}vvX(p6N5kTI*H(YHN73*1!X~yfh!r80 zMYkA6Mnp8EnHDL3X=?I7xX<;#cq*J{jt*?LXN-AZ-1%_3MFhXw(Aky!xk1@ZZXmzg<>d#(l~qamr~LYk9Z0EWVe0F1;v#_KXU^Q?$*StONly-H9aryIekJ4( zl;xIZ&!6kEvhwos_OL7SAVdD1RGXaq^&OR`yhPP7&^OwrEH5YlGBANkrok%BF19Tf zj!e#H*pF0eA4AZ{E%=TbbgtYTH*TC*Kqef!;kU6GHUWfL3Tb`()Tx`a2HXk$Rw)w^ z^oqB}xuomly=uI7BjHdE zu#$PLgBGn^d3T(iUi_|Iz3Kz9z4d$b>eUBC@^1W<==Rq3TU9T6X*c(IqbjSRwQF%$ z%F>mcG^(y>7#Iw|D8X!)qyt3s2cFT^t37(Qc*&8vl>la2;=e*^%^5L3}w_@%Qi||hCh<2**U1n zAZhY~a7B|Fn>MyB(Da!`GE(FP!W9n>k~=E?mp_t^fX|iM81VrGNi2{N!eU|Gz(B*`w2c`NH)U*49eW9XqU=hYea_`q&y3eeWS( zzkaRB@Mtm(4(vW~%98iTV`iz-#^UBp+alIp9b)_sF<;@4G#X@!?l zV!@c|nwrN;ntYu~2Y?(B`&C%za`#!ap9uBePzbd7yU3L#GH00Z zPPsRYZIj3^aM$~{@82t7`-5vI5%i-)zwJPW@2=3PYP1E+sc*vC)Po>(zqB+mI$D9k zW^Sz~-1-*AiB~YGE{ys~qy=GuNpb`aAkiqym@(tzsZ&?M+9i`fPH0{oIp|B4FBbzQ z1zZ_60m4}%n)LMa(z>Blo~I_Pm6Fvkeq|7>@KRcmUmp6aK2VL=GiUCRfQhTzan}K8 zsz$Bx_Fk4)ntK3V6w<&IGF+|}b{V&B1YD>q>PVcnYsW{AAFrl;;9619prJ!M;on^m zYJ24X9OPBRno5JgTMd7F9Lu|_4;dm&jGMlCwGKQ#j@a%$(Z>k!wmEl+^+&LMh{|&m zM{lxou5X7?qfWOj3*IQ!hP0)P3CKTX4J4Cn*RF-I1OkCGDP4W!NO?+xYxoMC-!#MP zng9O#RRl}`VGlL69eC+0c_{B6o$6)BWn3K~H4Cs$MIlt!I=rLq{mi{y>f^>Kkv;@A z?%aK#yq=z(ys~!ah`vfn;WU`IqhmGs{~>D`$!@(7(6r!t!{3TnBcj0$6V{F`-vZSm z-MRB?cAOvw{fsgtF60<{TsBe{{_w%5)?OmiyAPw4_J2d_U%Qm zA+9bibE=!UeQAc3RZngq-;P!$CBYXCXRdpYC5iswAsk7bZyRd##y^HB9S&)4U&9Wl8Xfz2I zVkofd{DliFaRzSqW7XyR2iwe_zm*O|dj|(o2M1MTg)q1!ltIV3HfA%rtXLtBB0nXl zb+^O8efzeO(@hpEPy<+!2cG5yP0niLyFfFllQNQy9g855zJdEB(T-7SUJXuWcAzZD z=l+8S8&01dC4A;{K2g9a6B0-O#CdH9%PApkj!BLtCLKY6Nk@)I4IVrgSR@T(JlQ(7Ob3ULGYF`^d0*1IpM0lV`cPQ`i6!u-g5{kEd~8PRyP? zMI(Ck?JMF4c@*wMzwdRBFrE=JD2oZ>EDd-6rlZq?dj+8J=ExqF(S3!>rJu}*8&tO;a@f$#UmV+EO7;;}nUML`{p%v{VYPKk_+%#x)` zD}k>aBv34?i#?Yt*$lHxziY8vT~5%C@mw*`0V;`~O2SVEV_`gX>IRc}Ho5*ya!_n0 zn0$S2&7M75Ls2#n9?AOvN7odBQ(V9EY5uM&eM{b&1AvZD;6iR1^RW|CQo1f*zWn3! z3tc5K2M>0mPem+CdAaik?F%fs8)Gn+{C%_wfTp6VDje5Kgg3aPkn^2$Zz+$Ci&$-) z;UkyfA1)=IKrTj~lnN=c@0F?Q5vI*$cA5_EHdMhohm* zbulaBiLi_(h@hWo;{>x75pyFWr$>7-_o91ANr|s#Pg&`>iK_=;zurzXYb*%}2pDW+ zq{gGyE1hlbG-rpB#?iar+y@lb%Bre^wY22KagB(>h#LRGVf$V^x61cj<>6g-9NV>Uo-{={-?pX;l0#z52$$BqpZ zpM^J9A3eGo$HM;NT7Vd_i%pyl11lDUNsw-cR)Pqi!ESl1W8%@HW*QmOfHOHEXPhqe z5d;P5WD*8@K5}?uWSCAQc7XDA^lB?l@zrK{?Ka4(LA(W{ed+fySmEi}!TM`*c1}(u zaX(JiLrz{nfneR8a)`aCN=81NaVHjowi?sL@)1Qvkd{ckHZ^sSfJi&d(HU}bJe6TX zWA!YHgHu$v@8=snzzhs*fI3wk-C(=X&_NJX_y7LeWXTekf{{!MJh9gKcNhfXRs+|F zRFWm0o?qIFX3&CwD{N6wmLeOv(2tM}e+dKO1%CTx3=R#0j;MrwVJpg@Z=Y9Ugl#wxFz@?Y)&7&#%ChzJQd3j4QQp(a(8ha^ zS}@v+R3O41#7Ycdk!01-90d8vC)hZ7F-`6Tl@#yRmtBhJ-{B0HN3m2wXsZ63BGi zLcT^fmM`Z4m#r>&*Q0dY+K;5M&n+#*xCeFU-6cbIhNPKJP(A+Vq3wwCZk#G|n+ifh zD21W%ke(e*eoN(bP2fl79=Djvi+Uk$VEdATOrB5r%!${Zrw4->g}BAks|K`)%+D(C8?ha6P&h%*@jf$>2*AVd2|l>D#YB~@3H^2vg&0TS~8K^<1#bHUkd&;DeEK7FpFVIXD3`)#MXHV zgPQ#WzQGl(;Bl!D?W3AKwy`Mj$<}24v*>cw1 zDceh=j6{)kMQU^Pm*Tat7#z?YJ5KYpB}#PK>qRRopL&whU#A9~2foSizy>cb z;kQ5H7s_pq5y>eq#CK=6!8w_E%qwo;;Z$Mlbf* z6_u21<)T*WOIK>1K7X$Dag$EJ<>7oK_zr|=oc+i}?B>&FqoZ^%D5a#=6XSSJ zu?MBTFTQ;I3ZzYA%|npj{6k4e8Wfn>fBSgD+T_;$iGHv~UP4_w_o9={D}_d%Rlz%S zY#V~4Loq7t$a2)&rIR%021BQ;JRMSggv7@@PaKFIasb-;4SjiJ){0Lvr9v-XHh?VS zWNlcMwby(IDsV@J3%GiEI(a^WOyhwZ!3e@5m7$Kz@!8Z}t%#$gjq4`jd-vGl8+HRV z(e=k7jCCaacXpTOrz3K5^zjxp5Io&6hvmBapBtTnC-JlMdU6MMY!ngVH2Q4IfZ0NU z0)%aTa)c?Wa-?Z=j3-)7^|Wg%eY#<5`~4}5=A8NS<-mDnW@Z&6i?Ww%B#gb%6Fsf> z_aM`H-C{ykC_C!Qr>|SDy9ve*Gv$rDcac+^qU+M4!Le&D&%KcJF=*|YHE%xoCd9{A zLiPzd*|7cR8hkTRSwWt!iiKPQRk5wn^aE_iHq1ZHiau1fv$&kYSz z0V2TVikh0OP&oN~CGzy^5-2nmiZA9?7XciU{Cn2A!D?Kn(MSy(mFL?K*79*%=+y@U^_r6C#v+Fxl!H0;YB~GQy8-nX;~sr zZh&^c^%TL7`SUAZ?z8)ZafG=b8(|DNJH6j_sCxHK>ZJZQr>Rpr;F8)wN+d}7mu3x|k4ZtWQ< zM~{a0HNX69k>8~ehjt;B2upd=e)hPef-U<{kQ9TzTZ zRwW0h`S|$IZ`w89;`lh@j){u0x1T(Lr_dG7bIHiv|Xv3&ZB8JpIRvLT(^>h~_y z=ECJEi2(`vt0qmFgvgpU(?_>x*aK<5cYFV9Y%`s& zxofJrqB?Ts(r2x{Prd&5qZ^$GqM&5lRyTsyrsKyo1T{KeS?SlrYg4Lj!Vh17Defi_njHEf68UA{#*ik)^rd94YJ+Zv24I&>|x{LkZv-IGSGf;%DgT zkyEDhr-pk9Mn{y|OizIun$|?`D%tgKV8M>s8gzq4bWGgGD+!m@loJGn4e9Cp~Qmo*$u5Se` z1Myr*iIAL`2BDx8^fn=I-s~54Ht)cDHno#f+=mSxUU^4jXq8hU{Fl{ljZy138FIDU zYO&mA6BQVh#`W2^n^C?g493JzTL}G<6R?RftO&}zk%`Ov{l-j1u8K814j$OKYu85N z+~1PJn2kxd;%w*g zOY6IbHmYM1SE7!(V0f^k+-_Akd@NCUp>Dj%o}vl!?CsUL_I7dJ z^WA}W_#53Q%g8ZhpU3#+TYdko&5F&OduE&2pa`{15^eB2ejFFvBXXyrHf+_mZdCIx z*>0n`OG8sLGAo)jB~?e^MTmIxan%&O3pYHQh={Z$_?%N}HRtI_&c6G_R`n*(S2Cr1y?3 zLhubbE*Ump(y*Re<~Tdw4Ui8n!%w-aDcJhc@8HugvO+-~ejgbg%~u$I2hH}pM9a0g)jobCU~>e9{*be+MB(qPUZcV0_~#h zyD?B<_|?WPEkAIIGSZUBpxjEdfv$|kp3$lH866>H1qRIyB9CckYb!z?U89^JH)LQ3 zg?^e_={Jum{3#q|=+*=YL8|P_YwSrf)X0+F20joav{H=tT=RGY3cWiM8w9!+SJye0 ztWP}rTOPu}?$-Ue0rkJ>t0QvlZ`-d^gl$16`|;q8<+v?@ z?cVVFke&fyV?n1oQ=yC!9S*gWj(&D!`cjvqUAoj~?X z)R}^Ww%SFdGko^dI$C;SMo*eBVJ|61hq7C|A?-HLKm$sdvur>XHD#Wc4r8A~6a4o0ddK&`N#!1sxU zdlVfRf;}idOCg~RS(`Shxw&}-z$^9q`Q0PNqR%dJaY^*r4zx&#c{^$R_&vO_&ZtqN z7Oh%!Pc+fb`Qu<)Aa$plkFIUQJcd>&LyRn@UgD0Aumgrc`__-He|6=akWyK7v9f5~ zshq-*Tw}EKKr)k*RJLv1>W2Bn0@%z0dW)}#`m%V09cCoSqqwjHouZYm^6ntl+<^!8 z9sVR`W=J``L=lsA2iYc1I>Zt8Krmd$VlIX&73+$LECZl7e77p%!t%0gorjMeEj)e3 zb+nyC(s%#wW>c*lvKATr{JgOsPkcHw*LdpCvAVi$P~NZeQnVJlA?>dC`{?hf3|q5M zSG10obFyJaE(UN4ivp{VL2qA#ZsWbX2}_?JG77mXB(g=zmX)BY%md0lrGD+5xi_R} z_b*0NMcQSirbaOWZ*zP?!dR+P@nCU zh__mc`__a&96eP@qiKWxQCYt~-{0+vpxaoPD1O}%{`+73_rIp@{x6k{W4doQRu=%e zWfBGen6}Faz?jGtv!;!OD66$jVa-_5RVLZLC(k@2qGNs_?64 zzl&WYq4#%JD!dGDKPrSpP0h|iSF^P2fx^Tg6qb1DPM!3YykJK~Gm{U0O(LjO;dviK z?5bDcEQW?!E;Y2^STGGLyi|(C7I7G@6is0G}-SJT_Q|mBMKr{MwT$XCmlPpkf#{=zllO9YbEuR zuq82iB&Ez561Jx1tKMv6^}x+1XHa!4Zp@iNpJ5S%zCcajb&G`Y3TUR%5~}Xr(3+MP zFC(e}Xp>VHJnKI^2W29OFD6tnJVv5pa_tL9btz(ZL`H@V%fZyqhSsL#R1va}`r6~h zg;V>(jC3hdKGQAUB9at&mu$ zw;u}E!Deq^M5wvLSN#khJ96GxX)Jo1w`{peD-Xt_d-Nnv!Or6QV*ex`IU!IOIyQZu zM=ldeh0t_iX^mMz*q3YK43_*6uiJ@#7GyaDL=RQfZCJhp*M_n$ZbTyDXV7iA;^oEI ztU7M)(&mzJY|;Ix39h3DB%MATeWkta((_EGyg(I7)ccH9{r)uhEG8_W@;!gvPuNEV zwJy{t1PkALI2gE8MkqB9brpB!hqxeyels})v zBmZglAg+gr`Q?oB=gqKNNnCKHDsH=Oe`(z)enPqU*J&TX#DtQOKU7Jat!T>;nkF0{ib)JR9T3XDY7v9hv@c0sA zXgAu*bhu?I)iLL6 zp-gEgc=YJ&>Dtq&A?Mb9JgdcmO)mF)uJa%W%@9evGoc2B&U`YdQTF18mdj zDV(1*^#85QFVBq9nZj+Fr9L-rzB_(7zCo;n#^WP`!oQs3-=x*rT(?cQP|#zrWpu=B zTM`h=^=NfM!v;DRl$Q_0QQP29f3K*hGfnSTF}vR9jt4;mWUsFu2$XYF+ax6sbp)PI zuI||gpCe45EUsWxsVEft^y!@F)+S~q`E|GpQGg9>3tX?5kt3|7bOdZb5g0UTl){y^ z))6A{)MnxW9XbEJtne?}1$+xx(9d9- z*w2>2#id5RY@h!8gE(Zks*6RyLk4CRA?m0_lB2n~EIHwO>F=6Vsi#j1eVUFrDWEa# zqHE&vz`DYqS4HqM59rxj@%(%jyN-t2`*deRG`c|j4b##JV-E`h z6jJk72FM=E6x1RoMHo<@%qr!CAacyDz9=!KM~S;HufjQKbHT*!*jQH*?Y}-XJw@$( zJ{C6uH2&0o$xzMCD}Vn5j5Jxhb{t{|Zj3kmZk=(=>B-j`4stjiGg~}UKee`7?PGSe zYbpc=*JODO`+sfMJvCw<$^Tz4^*`>P6A$VC zYs?EwCrabU@Iqx=8Gqu$Z(NU}nmYS}Pt}bcveF&Ju*jr@M%&#NLWI`>Gxx>|TX>=G zF>|o;eGf@VPgkUeZzK5#WlyvWVM=k~==rvO9d5T0Eu4W(*8`S5ByTA8?tN_S0b})w zUr_l1Xy6kM8RI#a4|b#dt^`}!+)M$*MA<)HClT}h&7j;ZKQT3iycXL796o? z+$+Bb9K6yL?3@?ah1ibe4$I9Y!+j;xP#XaYYtHZuyUWPPkO3PH$XHQM)SnOLDF6*4 zn}H=Gv4Vh3h3f}(9p*t?G9t-GYtey;QdUtB79PA`#o+YbHA3LSzS|9#A-b15qPf-c zA4w~=KZ5;&ihQGef#RxPrY5!S$ zE4m&+QIeq0krE#uGk@|GYu>Jly)^t$0(+$Z93;?`vb*@!44rdXv?vk<(R4^p4Qy2; zaBL6LN!^kP;*$u3P>k8$2n86zPOUzK58)aEm&G#vXKZ^7$w)Nd$q$%xzN8KfA7XGj zAD#Zr9yWUtCa)hY8Zf@Roh|wxB-t7v-t^pr!!H1n8lY#i0?y%Hi^9d92ul+;-m@P~ z39CQ+J@n&?tnM5EQxc1S>j>%Y*g^FMPL|N*pO~GkOsiwoPoCiWrx}CsrP7Zo+*d$1 zsP)dQ_}j?gXm%YTw=qn05N3CJu4JhZu8Se-Z}P}EQG$Hp2e|8ZRtE{rr~+YL;B!1q z{w0pjmEM?TWBmCTs?LAnYw3$?ZZ#t-(0n>bfJ@TstK0kZwsk#9C;V23+x-()cN7K* zNtCiq$I{YLy-#bGMSi<(-BEY=rx7fXV2jiTiR;J47OjVGzU8l1|SS{&}5Jc~?76!{oX%{gIE0jqA zNnmC)_KH}s{P?m&jzR$8BU{9Uim%M@0`O%7RRE-j5IPu3Bj10bqD1&+@F^ZIS_7O3 zrh7$T1e2unqDHYCAzc*e2D6b6uD#kvYiSj*kde#`tb~9O*i5u3k^Bk}>wxmvKDB*9 zw@P0zKC%^%gRoe4Y~>oAl5PV`E&BG=!an-xADJz)g2`Nf@{~L*(OzC z3D1>v+{~gcFBT9I50eh4-&wI!Vo_y4EcH|uoQm2yI#WdyqgrvJN%k9~9Y94jyfFdC^e(JqD)#^rUW$0^U>3MesmEh zMovVjrXmo~kM+*Z1C`w|YK01tr^KthEl84&{1@A7hylJd`=0Rp(f-Aw4+VH? z{&_Hp`Ksq^>-dbr;lnp!tx%slS(VO5u?(W&l|`QaxWnfim$Vx7EvUUNq}jzY3$2pr zSx>Ko7)_W^$HAI2_s6Rzek3v>p;HJ)tTCcCmi%0^%ERNB;_b(8%l_Oqt_xXNv>ag& zhy<1l#?Jdk@eB_BPlx;W4FN}|%0>+SQwbZdv&VS-5hGG2)jtFZfer+Bh|evCJ|59f z%GJ-5UuiZ2h@HHqkgE?JQWJmzeF`^hrtOO-i$>c8EiFw|L8p{RDEXE^+i>#kGvN$` z-QnA-95us0-`Fo#@asFh%Z!VQQ^y}B?3>G$Y0=9r$V=)m7g|ev(hmKT@q~HZ?>9#7 zbx9oLk@<5|h>b9j3G2s{$&)YH{^+^IOcae6hNI}?9SkBA+$eCv`j4sQG&mBRUvs+3 zbZsKbZKfTRAH-LD{w1jY6b`ksE zuQrRD1KtGF1AZgm^r=(y2FP$A@o7kW+kWaZRyK-N@Tx|Z-@s-b{zy#f1kc7yI6r$x z&m^?dqD<>|T*(*-qGvk623YKdQk#Q2hK4kkCU_(t2JbO^xj#Y5L=>D5;KDd8!ZjDy z7mQzZ@CQ7_;Nio!I5cBd#3WvtT|eR8W$K?N508xunk1@`1tvmISx>)@%H`Wl)jvnqGLu`NQ!L9l$SH0@LegwnBF9ElZcwc z@yd7aWP*Z%B)zX%3Ye^-vW0=Al6*G5*gcT?1vF*RAeO!ML(jbSwg6o7Zy1kT(OmiQ zW0b(Phk}|_@c#(=en?12>|Ig02`Py`h~`gRAST3NVNpgW=)hW5HFuH-qJ+8%y#Z^t zmFG+7q-)}IG1dvvh#ksA2mCXIG{hs=ZG>cD)PWaMP<}ES5R}_-&d#J&4BNpEf0Yxv zk*_MgHTwDKhB#rUgy9v26BewH^DdCJ_%P6~S_jkw;#jZ=jLPKaz&(c!y}EzL_c!Wp z6^X6A{mJyvMn;DiZXy`*<1~idzJ1#scYr<4lQU&3cdh0K<65pwa=xIb951@<5dq)GYXJDJ=Z#q&#GQuj0GP?zqlUIR&@jRhKSXri*ht_1LOGcdSJBM?6+)7Z1|APz3(4^&VVD#}IhpS8b5@os*9 zD!XGd9X4VFC1y=c0j48*BNfVY8&^%E#;e~+@S&g)g3XPEXL&hsV%xW3E`of#=B5Lz z`w*#ls+UMj|9o?^Y2%n+a diff --git a/docs/_static/djangocache-delete.png b/docs/_static/djangocache-delete.png index 59d60579d1ed7d7c4c4e05c1d86fbfd4031fc47d..7d4225c1a2b22953d2abc6450e1bb7a4b2f06e76 100644 GIT binary patch literal 33750 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�bmcN7 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 VrFrt#)8s0uISW6?n7v~AzX6S<>stT- literal 30412 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 HQ$iB}T{U8w diff --git a/docs/_static/djangocache-get.png b/docs/_static/djangocache-get.png index e92d4a7b9d6dc2b01636f2d492c94ecdf1470ce5..4805d2558ef3741b17b713fe247057b37e75d201 100644 GIT binary patch literal 29965 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 literal 33536 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 C+|f<| diff --git a/docs/_static/djangocache-set.png b/docs/_static/djangocache-set.png index bd51a62ded3ce07f59076702d91ca83253856536..a2a241a169bdd9b8dc63c32629f3f3d3e5de4166 100644 GIT binary patch literal 33364 zcmeFa2UOMhmNj_QvMe!Rz${<@B?y$6AYg_IN{*7s07yonf&?vd0K6&~NJ<6)$r;Qj zh)B+$0xDUMjKJ)3t=F&CeBboEneJKZ`=;B~uS?|ekH6nJ`|Q2X;j)U7+#HVO91I3y z4pV-w8iO&7{`cayU-2)Y%Q$rLpI_{EGc|t0pVPk`yM(W2+RE$NGZ+gT=zmihO?Xe@ zA0-_2={Ts{m^e5ewlii}9d@v_v~jRBJF?2j*v{U}#(LFe(ceWkZ&-EQ!NFEiOzfW@ z5Vf&06$_XZ6T)DuVlema(m3hg*>LK#vi3xN@6dMnvl@I`epxLTw!EVHNNnu2_F$;+TG{5ZilYhd+xM{$H|tl?^Z`7 ztY2@>4)m7eIbAF<6;^c=IUPd|}ZV_BPh%3jIu?8O(Ki|!jI)oih@7mpacFlR-9NmAS-SLVe~!Jq zz52&z=QSggxW~tPJgQPGs_Zgf%n)k!*z;rd6kE+x_PDE^-6bg&+FLEF-$z-s7Tm7Q za*jWgVjkw>GcO|dTJiYUP*tH=-lNhmMcGI-``BH7{u*Wb@mZ$(c;=C#M+Yjjjm+}B zxNFl-g!qaX1(vmDTzmgs<>iYPk;h9ON*Hi>%5m-D!1GJYl(hZyVrExEmUgUOT5fB_ zpAYw4?VHtOuzbhK$5W?G;|`MZ=80Xt)%^I&D~oCqjjt}>a@@!MWrokx=^S69A_5EA zHZgWQtFE2FkT~8^89VwtX!4O-n8KUq&-c8%xEuMCy7+ZAuJIz{k9D>5^@&sIU5vKtnr^;QgX-Jm&V!nxyu|oUAL0 zgu~BHpB1eXAX%H~7{k|w`(D2B$hm@Bf@?WBIj7Hg;?c=0+MHOFeNq>5MiiMwglgH$k5#FZelqpl}oSn53j=U^? zuxG~jkMG5IKfkHYJ%xi6`AewZ7S`6n!8RKl{xOs{87n(EQr!3b`^}3NFHW60wLDr= z*sA*R>N5Y1s`#o|V%foX>IHaa#)ZBWm*%g%g+FGNfBX?lZ(uSoOwrF5KUi>EIAW;L zRoN-4VWSgPCkF=yi?1k1wyDijeA6bi(txyhlb4}}jP!+Z@>6!s)lIVwc=r6cW~9pM z6w4Ylennr6-Mh~oIdWu+>!9UzZmG(R+XMxf*H>)Ua&SnF){2epZpadrmNvj8XK+Zvt7hwGNbj<;&@y;H;xa)duZaoxx=4k zZMCR8Am!Al($ko8+s7w(vB)6-5s`yemxxs=isgiQdNL}aH4Dw+a3YJw>{CLT(YI<- zEFzwrpVg7i>rsB+CN~O~(_Uwjr`edDHmPjnCu)%FKJFr-^LVFvgi=wttjEM@=A?(C zsEmx^R`c@rSU&^LW{E`_WI1KpwO+x(PqF>Dd!VPuXx|mCgnsFQ-u>ki%%f3<(_-s5;1eFFoUR~L)4eEAaLHa2L6 zg?T8;Db}g8`oZq=91^hszP{lmMfck9)8>uY8GWCNVz5N8R8{H9n@w{MKfj=$pitW1 zRyNR?R4nNp&Tm{R-)KU2`{frF&atq^v%ThP*Yu(QS`U_5f)hD zuywTt@%MRGLIwOeBPufLiX z2TCh_KM*8iaO(T#iijZhdu^_xpYPj5%imZtig(_v;~fzZG12Fj_Z63W2)C);gt*`^ zKI-V$R?1&?LU&wh@dlmW3kT~)zCGvMid2*{^7(#ydDKm;qX)Ncaiy4*?MH(2=QG2j z!)j97yZ2n6`>0t&v#@4#XrQ#Kkfh`x?CpXrlO6GS-PPNilkRRdRbYiH1w^y>6;0a3 zuqB&&Hk-Wl;dA@Zn=hoF{v`wH*18F7uwOM=l~Ach~nX#oD>2M<^i6=@t5lY&Chi6zf}=LRykZkw%)e zf%JHP*-*bsNLyKiVrIhi;=)2j3e^!x0p$_Oy!m&yrJUqhgWV1HTZ>*E(*l9dB4M^8JhB$7j4*0sNw7j+ym%oM=O)&jf}PBLmJ5$5)vC_C=-$?Ql8(_bRywU&o%*)kzf&W}o`rnA-DsNQi&kx&rI8 zjvpi6b!8%-YXnF-YUG|iHQr$~sdnVZ!_%iv_Xkb>ki%wEL1GSwTk0#QzCX*Ut2F&Y zb21`g{@Q|RQ>Sh@J#0H?rTF{fUDegqQjLYay_8Ku^m+O&4; zpz?*Ro9_aogtuBBjAq|ByEFq+g+-vsxH{?w*v#?ux;CJD$BgN zu-U=)M6T;lcTQJ^iDpM^9BYxNz79^Wz+`5`47lM2hI2&K!>^?*ELxWlYsko$2K=MH z6Z@nh{lrn^fP+V0T`Q@G(H zcIox|y-2E&k&yxi@BF^H&}ajqdN|@&{+DZA%)B3ch2sJZ{ByV^HLwUZhq@b*PK&8v z1xx^o)eoG;5;U(*PjLUyD|Zl~2Z5Ke`z#M z!(R7W3o;dymAAW_R(npF#>aeqVNMyg%;tfn`fbNv&ts|~mN|ZYd-uRmF@wxdY>blQ zu%*Zeu}; z<^4!J+WAN~$)V--{G5SGy+i4?4-lXuC=wu7Ub%d^L|Jw`l#pWJE3F5+r{CV7t7de% z_dH@k#3ti{*Ct7Ax3vvV9ZwT;>}fQhJ3{c(OtsV{FaqSHMW6?W7^x;kFU|ViX47{D z#WhHs0ARcD&QF}42*JWO@2<~?){YM>@D&l}<>f^fI1r;9Pq6D(P9A@&FLxdwN*_wK zyo)6pg7@1t@+C;y*7mXGw)?=OVfg%4;27lc0)yg_2-$I_$5>s{mJ^@$0)m$G@ z7Ms_lT19Kd_%<7-BXwjqx(yjz=aD|7EHe^nc=F?P{1KFp&|R+_SneyLrx9!X*I{TuBu0;EJMwRqfNieO=9d(wj}LX+6?;fQ3T$W^HXPWRRKs z>e{l9r+>~U%+Jr?n5yz%H-UJW(XVUWCPu!Dw#URfv=%H{B&>CSE<)d64#Hm@28#$!C5P;;<5n_WNJdUH`3d1sCM6#U0?HNrOjEdBSt{B%B>Qy$?m z)w1Rx!0xG&CtDmO@MBNbiXfOF#i;%H>l_W>oaW}4YMig}mVqfd{6(kHXAWw`=|^eB z9$LmL=i{;yMFL;L@I}fZbG7OFJ8Hdnq@xLqxhrqqhfi!u{f@+?^z`Xd3H>?v0E0Pn zx^52Vefle74VJ(d*%dQ`N7G(Uq@ZoVrna1DbtOLrBV(#|C_R8D^APRv zXZHO6nqT@T&WLJhIfklF3+M%<%IzW(b+8py-b_WsG9V2R6vl$;;m6tmZL7w5^5TPo zR{^_5Bc2_~K6%2|9@K%rh7AW0jquYI!tvP;0k%x@qiB1)eyJ}hKR#GzLzNP6T|dBV zW5q^9+H%BqyTXa>jVJf4mGjal3qQEk{_}y`q6UY!B^}-`F*^NmizrGlZ1<{?5V@^p zrF^K!^^H8nQ|@iEibeoFBq>JkEkm!-U-X01kMZ$Eyu9()xWYn03P6|XUBmWY-kJRv z?xlR&=+Y5;U_3%Os1h{+g&$xnMOHt8Tr_Y31*h6HoA@24`s7)t+(HpO%n*qvNb(78 z*`fgupn2>Vg^Jrnro=C;O0#VfR^#E}5i-mkHt^=%YJN*dBQhBA-nb^uyh11}EG){V zDc5To9$uB9>%kKzPShqGy%;hc6Q8SwT5#oR1=hVSW)G37I>6OrUreH>_3g23+bZKD zRdEJ{TV;NH+f!qix*>nBoLtNwPtQi{Bs^4iJ$^hg#3=JbYkPf$eY9bYOSr`DJ$tyD z=~3*sr|)9X)ByOj{P#aR31wHr8R)H7@_%iofBJMbh!S7H<0E5ZimZ4363t7U?ZI(zIxj^g^UbCoC#iI? znfR`B)MinrNpWE8rAwExWa%du847WnYD>fn%fFU{Y(4VgHx4=Y8J341l1IARbGS0R%Td+$cnc@teRh4mjvJrw{oQ`}&73{^R)ckMVsTN? z!4Ns`N-UEeab=*T%6KE2Y#F+ci+XW+a_pGQv*lk(uK#}O{qOJ6L;cra%72+>9NivU9XJOBZ=62;YgJ+%0V)tU~LvHUmgt_E*kh5AzY~$xHU%G$W0SC4(ACcR{YGg z8SObkjrPm^{QV#8zrNfI*SQ?ne$ARST-@BT*xXcV40ZWaBu&rFmN0nIH9R&Z3{n(j zlD3A%Es6nV*Rry*+=oBTsEAU}FR4s2RVE4wbS4$#$VzLNfZ!!7Y_cDK?-bEXeYm0(ezq*qffec+mnW|L*Ac__!lD!vHDg zcTq%k(i>2?C9s-FxDN&Gr{?C(MK?-(H!j98o`igPFjEawh6dPOjT{$8w~g5UCgR*qu$5y&FZ zvTC5pZ!8~A4*K3`;r|yv_uXi%iH9Qr=E6m=hr+Q|4K2ASo@t?8;b^DchkVb^pgeg1>eCXV)>rIP;l zq5l8yBwvU9sbOcA_{xhr=JDg-LFGW(7TmOHQ(?l!jT<>;&AJJ-7W-$QKsf#@*yg*h z!XhF%K&NQ~O7D=C*37Uo7BS4OE?qN}wCNvS0DoGvJR&H|J?log(wero|2RoyU$M;C zqir^g{;lch>EbBSjKI$z8L?P5ZxSr+YRWSLN6kVaIoe6N$+^i*TiSI{t#8N_M06HkklL>9VkPr;bX?Nfxt@mrfzJLmk6Z0UVq(Q$lpOhP11qv%%|WSw4+%l)D&8`~6 zXvGq#T@)x?S7MS@x7GCBN}`G#+siiwF2toi3CV9w8ze>m;Yv(NAA`jqrZ^x;`z*C=>fl(~ls2!yiw&movKd7dHkQ}WVa)Agt zr1JKpVj27FYucp-#Xnc%(WJVrL4VOtPEhY1p=xay1OL2e=~4lvJ&v zfVg<|@JYs#=cd*%`y>o@uglSP1nb54aOUyoSVp1I4apOqUKm`THFs_$q?sP^cmFKI zlB|^Iy?Oior=}Buy0hgdM{l9}Hz=d5=2=+a8b8CjF9$W2;r|mH_b+yK{4Mp`}Elc;*8qU(@A;#4ogM zn-;j0QgFN);Rlw@S+VT^935Ja{kp48(Y-yHc9biWyb_>~qQUWu78*@F1n=JH^aH^l z#i31&VuO14fs)G$*FP#}*Px~^=NUcyDt)SrvvfR?(r; zmxrvXfW+W>=tV=rc%V0%D%Q3);_+}FXxB6Vx^+Lm=3*t@srih4x+=B>T1iCerAxn^ zI(3Sq74x$2>j-rsi+-cS#l2GVp#@NOI|7hJc+HyKSe|7*tM`yTmjgo0_v$b7Ic>Y* zo*S&tqxoJ@xF4_LAdiWRD@(-UK-&x0G-gAmsX_@6=G=HH-|hkZzz3lX1~(KA9=r`b zx%qqEOquT3GQB!Bw!>5(S;3Pf=Qu#uF)X~V~atR@Lo2({A6aT1>p_ER|M zZ9aLaQ(IdFvjA-jh(~Jr`u+;Hg~DKg5FhnGi2;s6PjmsDS^sj_s;z%KVSqa{hg1k# z$-VtvE29ud6j=)vEa->q9F0n-MooY|oz9^MJ;7zLGYYWB#C!-A3P8<@Hugzd>;0DW z5w?8t{O#8(8sJW@FL?|@7Rs026ZF#`qOYCRz_x(|qh5!)!~xcuQ?9NZK&C!?FJHY9 zg1rTX8qJ+McMfW@{bHg|1msc3)U&G2tjoIxrNf8rcO_Szd-#@6$u#X5^!@l0zIxf{O za^1#O){P`C;Nl8}LrYJWO~31Vg{{(jxOb+AZsJ~?rm)-@ZZP_SU*Xo{MD{CAU#X}! zX6P~QIM7xmmttO_79``Q5vMP~EWNQ-ZVO^rS51-}G^BUFyV)nx)ZtLeaT!nx+;OV^ z)e4(PNPk}IN3lbQzk)EM-m|+1-Dd@2Dz1c zsowIu$V}x6AmI{4Empmj(}5Xu_o5C|}# z+cOj!$9P#V-y4&ppA7pBjSbVI8gpDNCMU+J>>a4LFMgAszdoy-{Y3nX1?9)_&s5g_ zI(w;)!2li5voNWDcOzM$pmb-M(~WxS<0S265#D6CfsAjeElr>N(6_;=OV}tkJI*LC z&NRqPt0Bu-d*@D1MBAHY`VgOD|GLDNYCbM)VSXz;cx6tX^gU|1ZH4wDIsp6nK=>MK zbJLeT7I>am-LK;$>C07BkGVpvoxZ+b4W3MmMRA6F12tQ>(s$Fj3U|TaqPbksA(D)L zf$HBr87V&2 zwq4{KCl76R7h-57Bp!kSa;~WQEAl)%AZ{topFbZsEJ^<#n?^enwPb{-L8kcm$l37I zX%HQ0(#T_hI>lJh=iewp1rUZ~U_}lD*dS1H*)g zRV@RBVN>JL`)MDRR8avtsC=-)Vgf5djA~kEJd9_ES0=`kZ5g%_zGq2A>qoS0Oq_hV z#Hbu5px6zxco?GjSfQxu6?`|GDx6rrukaQ2DlK377Q^qTp!_01wY@O$d>0uzdgO=( zjB&`FZ;#1aHt6@mXYs!15s}3X91e@}^Kon~ zdP?VJF8t>kKC1!tnrqRbFj7^qdJZKVInNgbK^ELa2+0w+ykm+F=+Y3r^L(lRYQY=% zHCR^xmhm&aWhc@R=~_S0C!Q?QovXGCU&08}(%QQ361|FVDoOp1XEL$L)pT_3aBv9( zui1CyIF=DBw8?$!Ak@%Om{?;)e?A)Q46?D&8-_F!2_6@D#JPWw$RUlq$%!m5z!n+u zbS_(dJmEVVk7^_w*$34|9;>;aya_Ogyg7e+K|$`LOjsLWag_5N+DF(cZ(=m&Ha{KX zNh|gj*k=3McSCXzXnc`d{`MUNyi(NhiJe*Chj5J%D4=!zxndX1pHHGTsD%!A1aI(> zTXpis7g~m*MO`o=$Cjb5pLQq=Yhy;pyIeMRzyn+ufkEeo(+RfIGwh z=Ef7J4dE`Nf(GFdDii(!qWw_JJoe>X3tQFN(?dKYQw?>3Q%_^8?0B29*91g?j11jF z>==x!bgK+LE^@14(P{vV5t9eOMSK*pSj_sslh5pKI5)q(vDO8g5b5NL;B-@#9=Js$ zCzuhb>veyvAh>}ePxgKykp%E&bCDl}emu-*=r{#IvU&7Ez`_ik@n@&uDicIU$UNm( z*d)l4Rp-Wy8=ze8@U?+SB%1|*Dh z1O@FQm^_&*0R9RPt>XX(PL_)G@C%bGmP&@_7did-Q1|J=4N(dZ6bHc9G-OXN;zMTQ zz^~zO?}dei*59cvI*5`ORi%-W=#4N~XBAk@<~qN6cz9%FWKhvSvRhh{dkkWs#O_-T zYgYkUwn1HVNCt^};8b6WD)`%#R)K+m?Ij@>$$yHi-SA~I`&0!RW<(jekL<%fCE5$2 zgCDLYvVY)lRsk9$g}jn7uYUZ1FABcz+9XqNoF)wM{44L|@Wo%}u8ag(ZjPLu>C|-( zcA0L6M<~^I@7@h!y6gB=FE1@@@_?y+#K|gTChYs>1ZC|f9oyT#n{M{wVKWP?;V~o1 zYpBG??uHlt^|;aFKh9{y8^$6vM&V)BsCCAkJAdBn*}0j-4-mU1QQLH~c@BA!ORq<8 zC~S{Y8@~aC5YCwP;g!6+9)%}Z4$$7}DmTO6yl?N`J3)kT*$4vu@>}2zi9Uz5`&CpA zGL;JqFeGHf>Lff&#mN;q-&4~gP}xZPTogt9ZivVSiS)7h7~(+d=g;9NEmDNG) zCvSa!cUF(&0Iv2pjzU67*wmg^^zZQfea-k1sE*aGi3Jd=o3y{_^!MwAZUdV6TG`lg zdQ`K*!BZZiEkI{varZ<+p zjp`jNtA}5|9v>g=i1!-WPcAs5ku?4KhG?U_+*F&UxD(BPWQi!9nkH51w|2<1E z%Ae{l;ND z6VglV6dC*T>{*At7UtieAI$#ND{g_bX>DsW`zOrQ0E{5@KS0J!QoRX@sUNE@3UKb< z4ohhb0vcF7Jj4pTn*3QVoGZnHQFLnn_2YZ5|5m>(64H1N^ho70i2_P)czjhvYXE2| zPN!Gq8H1h47X=dzx%S-0y0&x6y6dz0Uo2FnJ|5qz>^e;Qn1sBsz@cP;kA!NTtOb`r z9TTj65Vo*KO2{-Zfr5&J*&kkF%oU2@Ed(hPf}1YvnC<9O;`UCcvHa;7<6IQ7YSD@n z!b})2oJT1^GQ1artkMiWaJSB#n#(-p&V>C{n1{UXQy)X{3a1;)_vY1x?X=S=OFgg1 zqzuJhEZf<}5Pc4sW1H2z{nwVRrk)%W6UgH=nuM+x=^GSxnWJo@b`pet5gVJH&S*9| z)4vChC2ExW2)4%s59zZz)Bu1;5XBa)`B~Aoa3IpcCz=8x@ZjA|`)1CZS))mGA0vH> z;w~ZRF(8L3BUMBChnv00UjqY=PuY^X3HI?UQQd!vO=~Pg%SkL*HV~E1t)p*w|ANbc zng*?^3L&WV^Q$a3itIa$)gIv7s0!-7cMYKesh;ekr&rPm($|En8UQE~#{Gl2 ztpKF98Jl-GIe7~iLy0$lQ6%ML?~%fn>}4kWd#0RuQ?48BL!#kOW6^yJF?87`3n~bkcwnx{IB0Pt?cgFaM%@ui<-h zv5gaWeQI5ibnf=2<{|{OHauENYpq>fu`quf%ZEyVu(ALg8M&xX#p5Q+6OPJ}hx#HX zuP-0}`t>AY!oc?SZ~GPAn@ZG=Bpteqb*15Fy<~t(skO++ZQyn*(LfX4Ia1y+iEk%m zCuAOWp3(97ko)n%+`GGk%ocYa(t2ntddK4Pq42}HrF`MxX7}n2goNv??XM_!#k49f z*;4fD`n3y-`1xlq^3&gW{`=T-Ueg`iX^QWsU99@v`(gf<%=ocfnW2)pllBHfZj4|7 zK@}f1xe0?9$=0Bg=hi`xK7RiJy;X*`YVaxuCaVF9`|s7k0uRlj?d)v!D%z)X=B&m~ zE0jY~SCiXN09j3aR@2V~zs7;t1T=yjaB6X-NrDa^6RW-wz5^Jh)E~2H!Qe~y+!b$c zvOz;q1r89?-LRM#4ls!@G6XE&W*G^u!(uHqm!`tUB_I!JfNHoBz$MuCMG+2YMUv;4 z)E-26P(N9Z&1q+Ou6gi;_jN~WYZ!U!AdDY6T+Uwj`wY*Rux*^dd_v!@mBZYO{kH}e zstgz0<1rP(w!Y`<(N|G+cJ9y5?>}?4T#nZ*6d|bt6hFfLBRC1UM_XiH5fh3U-%X9S-1q5U4ga&U#h ztMvfrW3d)H2diwr1E;M_CIt{y-bv) z^Tvhmf{wa6RW_X9n)PZ|fq6=!7n{Hf zuC|TJ51RZUJ6S?1JQC;|fJj(hU3zkcI!7%bOkj<|@<#OvT5NwO0qvuXgoFe!l0dPj zjKUxlntY=3$!EUgY)R4CaNa=f49P~2o=V+@8X|zx-oV`e1}Ogf78DA@MS+nMi3^{MYWkrUEu58Kp0Z2SXIz>KYp50WxfAbKh%`=~2Yj zq5#`oQH6(wC7ZVC?LjHG(PXL`;9eBKLBbRa+#XuN&%^QCZ|@P|TEBc*&m^^Ebf_mD zdoc#G6)P0gR9myRY~?LMb@&zpn0~Uegkj?);7v)PgI!1n)LH|f%gG6E*^>O&BEuL= zaRtitaB|zDK9O`Cd<00;Ikf_d9FKhY^_3E(iywv-PN6tX#kz<*A|@JU4#K?nc;Gu$ zFp|kur%oN>i%{`q+OMZlELc7xjPw&5bZX?#i2%uO2L%O@GXe${*l9r#%R%lf&+5q) zMr;7Xqz(~R4cvd%pd*?APM%CxQ&Y3Ff~1L-tdv>$EVLJ|ZtGj{v}1bI+qVbE^Al%< zqfeJ+Z{U3|@Gu{uB$6iQ5s$}@6XXFQ>H$xTtjNUg^1AikfZWZ;1b;VoYzA71alX{5 zWy_XTb*^WE#OiSDxC{Fad^5A`6lg(Q`Q7g~HPd(gB^mZ#wj6)Is!n}0@N^%OdXs=R z%czkR)SV&=?A*%1q5l44E5A_ugHe*v#9+rVbXLP8-C2t+vlx`e_XC#Vke+Ma@Sml= zXuh_)rhy3-xQ8ra!cf$cI$7F^a!}RDDYa|qNPd=9o^XDaFD%;dpWY210m1Xgb8-3B z*473uNe(_VS;H-qiC*EZ+V9WbLgpf?6*Ez!H%o2*{tH;%mhpf7`6mLfdxiT*Gdp}_ z=zp4a3cWz?OuOG?M}{_mu@%BXK?6-CxaO`=ecT(h_Tml1ioo{?4O9{f(x3?nXl*LR zS%D1wHS*LM&FX(TBXHd?nDC!-0eGuZCaFmnm}3B(e<6rXRC$LIk6m`39Cy|WLiN)D z2X3bgp`%dI@wi`Ut(E^FO(kUCen!N0A8{fTqmpW@mSpfef zevVD<^Ls1)BFMZa*C$p#Lk*O1-pvR~6v4kG>cncrBwZDliAr@5>Lp(H;a#|RXD11~ z|34a=75xp){&^;7(AH@t!&Z)~|BF3tnayi<^ZmC;7LV}%rUB=_jI;GWVKLafS+x~@ zIUu(4=g&)6)@7ecf_bI|7#WliwPcZSvM0~qefEMXCni15&|$eF1Gql@}TM zK19|)*hP)d{MZUu-vvc6`CF)?lDst_LC9kNemfX&Y|Ap3C1TJ<{2Ivuh^-1~vipS@ z4#p68rXZE&`D`1qlr&_af})}dV8_l>>RADsL+#LTKJqakPig`zq__;2qo3dqiyFPy zO~B|TKyAp+rfEsM0TTnQcjR3lwQhN?c^PH`4z9;T7`d}7LfN^RDg`2Wq+R+SpBHGe z+ewWrp#Ckb;Qg?(w=YBEqa~Y1MGphDbfKJUXk19qQYAFw?KKT+>o_uVxQ~xUp}k~o zopX0R>GM&51x{TktWi5^!x9=R0^h@f2M?UiD6-%nfU5AkkzMCwFzdHi!*dO zp``#a=1yE@zl6m&M9yHX1%jVHZqB7h1|2^vQQ68L-ib*`R(0RU3}c#Z=V$(vd>S;m z=*O-E9pPo`At3ZEP-HkMcH`5I5b?OhZFizKBh~oSh~kg9TQ_bzfUC5wQIOJ(I)@*bTpU;Zj88X*qQY7kdr`u zlA|jt7iD;g6?=8sEgD-zK^8VosncXNBXt#Py8@goS*g$pPtIH9JnK5>jW21vH%K3Z zi$fJ}_H%KNjYZ*`Hzgpvxt1+E)v$h$jaA)Vx|=mg57paOToiBd@GZYxx9-55U@T*5 zZYGN=5nh1i(SToQ_>7G;!k$68=UTEP0sw`$Npi11%|w>ut)I9_r5fstv1Nh_0sBi) zv*H{RWqzB2cn=+j*@q?V|`P#KL)449djnM`HP7D1B#7{tTIgXS@LICN7 z$c0Q6ZDC+<0;;IIDK0dg`oc}c2w3c!s9(G8kXy9TeawnPNzNVLJZX#r^`9@kx{n-I zNlJ^@r~I&BS35}*DB-%!s-#&rzz{KwoLz)|*}VoZT2;X08Ud1#ogQei22uE^q4Hh+L83`b0SJ(W4T5qx5}neFt(4ll zx$I+Z47k_Ez6`oPfkXP+T0jOekD`3jgD3L*iRE;tE35owtI3Y{-=wB_w2bb>S$z4(2f%6p1@;~> zudFg332(5eV<0OK^pNnnb+6b}A=)XEM&7Wm$4*~Vo?F6hFDg_Yh=>TXR-EU*_4Tp% zT3cGgJ8uRASXJd07IuV>8YqGt0Yhj#&PPAVWhUDD`0{D$=7tnT%E34WEojKgU$WAh zNX-7h@2*|&`n3wyrDc)|DoRqKA8Ezxk!3pNQx%}nmwv#f;N3eKIdTg_fsmD`!_%ow z7_FsGp6s-0%56B@gw-v$V@Lgw;ZE+qY{&#w~-$ zhein+MWmn7L}D5a*bYr4HITUjI>ie;BKI33bfWA05hqG`!^ zA+08s)6bF_>)Z^Xi-}i~%K4$F3MvM(6w=*ooCN1GhZ&A`1mu8+aA`!s%$GU=TB{rq z+7?j9w5Di7(EI?P6`_qAZ*Vr#eWQH)O$`+!Cd<&|0Wi(nsb6M9!AEEYUB#vM0z9nQU80nRJp-Ue4L9hX4F8m%&jh&}IF2dh%+LaQYPw$tt`~aaLf4!nGK&IUQyt8UpDUh|E9IlKu$@?i+BWpV%Rv) zbfJdEsS{4RbZ`FGRylG*XC>(@&}~qcY74Kg~1L5KzbvDti z1U_{OkIj`;bLsM*Ox2U5O>|# z@~xqvp#o@$2XuGpg8Psa3ROouWCI;#A4730ZZ*O;(PdpDRz#3(9V&YJwj3?|U9~i5 zhFJ=#eO5w&o=;o-$N10w1D;r#-^Rr9qyT#I0%bZo7px2o50}jPMP9#Z`{j<&&$b3j z=Hstn0)|!kdtS;-6t{wlO-q*D)xI3knHWa=U33B=ST|_#7ZVqs8H>*U^XkEh%UU|8 zUO7Kd`xOVxUbgs4=K_^uCk&cq$60WR;gD+&9DXM{1+|Q1D9XcSJ#s#ozlw>)$Ap)4 zc3R?i|3=4AgC`WSt;gSgh@LVhJrHRn8mAzi9{fvfopubasc{xh25_ljxH$Ag`+$5p zp*XfMG;TF=7@A7wc7Zfxg+k-D=w1O+C>XkngNV#rhGztBkO>N`qg2*x#`#Mf%Wb9g zt6t{EzTMl}e0X8iJPb_2$ssM*#<`*HNoChn3iK*4SLJ^~iiLs>Z+X#9FGPGHy%4M& zwCfO=Z(!;{(T8KYL{)<#JVpUkLG(;MJz@v8uRY#l zJch@uU)9v_!h+Y}Vd2_;3{k)z!y^o7mymVz@H@lGdcZQ8z zxeGTj4EvRXF5m(Zbv95HUuKRU7cl!x?TH8t``NJgD*lgF^y3Ee6fz;;bL5F!DD;bM zg#Jj+nU_aypmN0kgspJX)0l}=cMN*c1ToAtZ{8{>aQpV{GeauWmi~;M{qj^RJsL-& zpJ7KvZS=ypL@3P^=(Wywlm>>Xh}F|3X;bCFS#s9}fNfR^SirFi+n>t^r@=h0Xp~P` zgFug8zkPh(_w8GHZ1Z;5=kd7wva-gkxMw`MPIK3&Mn#p2nQQAvWT76%{^y*sU(pmy zjX|&*^}}O8st?hac&9X!GYWmbsm&{8PV0adr1=KmPGDMEMjj4w{6S2AH88h+*Lg<+ zZ_qB(?vMMVDP7|bu&F~B)Zsv@pOIwm>=QA+sqLIX3IcpoBy4D8_D_mNfwFDKXtpY_ zC)ORqVG6fd5fNXkxTifSG9yiHyEU9_Rq;XHxntHu(!jPEg{L2iQ?*~@byorFz|Vwt zT^lL~nc*RHIHxv)Q>LMGqcG2Cpx=v0ljUgc0ZmSN;yJ~6qQ!GT^zU&U*RIW`gA#|2 zgB_Q0f)|2F67+cLG9dR6ji!!W#f|!vhb1_;BT* zIBbfQDWj0+L=I%sd8PP-# z#15M@8@MjfzeeN2K(|vr>X|cV$PosTXV&mL#>ti&a(v9Op`I#`CXUcTYuu|@d{|e~ zgVjez$@+`isjihBX<3MNoRY;4`2@iofYE{UQ4=6Fg)!H04H&XRF|n<-TW2I* z7LZFyLz-jZE)IcU?1E_29pw77w4#m}5CkhHp56)t4bYC4sfBLUEk|F?Ci6eMJJcm2 zYM6bzV+a-@3(S^e+l3?&*D2Dib`<0mTjocS*2I_(O9$z@uZl z>x;N1pvwNLya`R*iBpm$1Pl$7O#~m&e$xTrm-?y@J%edYpk87RXc1by`s0Ox1NXMP z>E`V~M^+@1J!;&C%(^72$BE@otcf;gc4x$HCeP&^*^2BpI0>1O|uTv!bB(EC8D0E z&}Mk5brFq>WN9Rs9Hf2-u%o{5@$sRP<7@al1mEAr+wB@tlb%u$&=S zQHDQx-l`eAr&qQ0m1TuainJ9PeR2*%AjAssGIo2yt+BK!g^yfM*psEuf@#zpoXd(J zCbdyE8pv|-{vsIcv!nOLLNZVj4M9_(^UQ+~q>kgdk={^>z2h1==#2^E|%~4-ZEo+N4&Iyoyb5Z>z<+wmwchyxr8WUD~y& zR`+4o=+TK~Siid-*CC8+DNOy;f)TtLPzQ0-JsMF0dr>2CEif`NqTJS;@16JAbQgv{ zYC@i)A-bK9yRb?mPxXa{7Iq{Q-VzMKqy*^iwWFACI)1^irTBM)jGzj08pU?)-Haz) zX{WxMj5ke=2VoL|Fx<>~WB;>Sm3lt+M=`8RQ{p;g3knO%5H4Wb>9URAbY+G~sORo- zbl-JY+mt*!Pe}nX8~KaKcni-rg=qooLyRv~LDIAgYNh#m2-&Jo8igBG$cuI!93@DqKP@osh7wBE0qx zSICn>XRt?{Ha>Pt0N((k1$xoZiOH}n9M?zX9zesh$l~Q5QYLn1}d;^&F~uArh=0^I{}}0?);C~JN*$X zL@^*kfLT=#Sco4(jwpT5gjH0L>*k6vT?Fij7sa-;tUC&aD~6p@LjcOVTZptY-i*ei zu#(XiCf)aXEk!~YIFivddvT*u;EpJ2kH?yzQ&7JP@(JXMn$eZed63I{vcG#ZdQ7^Z zyWIefmNw>G?xh2JP|*JL0!Z@3R7A^S7&^^418^W)rm(PZ$F^*HhUbcp7;)jLNK@qC zwIBl_#z7j=NKbzW`+iIjU}t2!T|OqtWMVZJK@-D9jKTOVVmqlL8(-FW`Q5)CgP|aV zwh3*}FlN*aSTekG^ae2xey89Aj<(skF1=irA>YG0MzvtoEI=`MK*Lz zW2F02Zx!e`(xXK7GS#5Q!h94#IEw|y_z=Z|1yGI?80QU(^COlHc2pG)x*_+X8`28z zAQE{gl}2^PVA>pp!v)2PAZl|;_xk`#IH2LZP8;)n0L}+`JSOX`{n5sx2UZ(lkg4X) zBW<}_C~q96N4wf~7Y_zwdj2&oG?=XfBq?oGvFC@03~#j|bsUSwmk)$~!?!z#rW~_p zCtG|U8q!9i5ch5>z0kXP*fj^YPkzz4V_yp2zD;&DKml>FnVEP$B{OlnAOYL-SCE9B z_dk68Y*OytTKXhAdj|wb1wfNr^QQOj-|t=lmB--kNEr&KHjL3@tF+^z;hSI8YsXpSH>h7D7; zA|n_94wP!7>jHTxKe#rXk-izr+q3XhR;AlEAO-C|{s;zoCkc~^aMDCk zt^~cBT$tgA3P?yyOc_a71|)egWCexbLOoX~9t9BSVZ{`Zk!kFn>BjK<^z=yU zTup|+{65}%w^r#ro}0fp&-WDm@N+EAfA*-;C+A6pK}f4hv&ri^3g=!lUP7ANN`}Bc zLN;3#b%jnzU% zm-j|nCwvVwx*Ow2Dy%0_lI8TiT0zbv*vDYM<0ldi3C3w0JPA$G#=Kql`;;Nf6VHGb z0>2V)1W=+NC^G3E-=0$&#~y?kYns`C5ofC)paH?C02w{DLS@negMSR#*{Hw<3auhT z72cfGO8Wf|H0lG+7#R>WwbzgS4O!@?zjxXh#i8iXg;>!`ok>dLMuvw(a~9 zLmxg0v`A{~36*FEquGgY2a^q!TG89kX60wq1o9ygkPZl-5>$^S-T~0IB4&1%<4Y?y zYO)5di$rz-&rks?3k@)ae6^olwxT=~0yHA?GOQvR_+>88>&RXL!G=^qt9ta#QwxGk zcJGC`XagaSF#2{z(H>~skp%nwKzD;4?TJw4P0Bq4wIQD=FqsL`@<-4_Ozlz_1T9TB zL+uqb6aZ~|b$zBBjD7PFmp-VWOsB`jETv1p%*Y%BcbYZ@R(UOxhIMhFbCt|;^c0EQ zLhv>Rctn?QA~#t<(a=O~!4;Uqt~Z7XeE`#7YtHUNGd&?}f}+rnGCSh3_?TqP!=dR` zkWxs1qP|hY4RT_@IaBLpmU76di4)J7l%$4JWaOc1NCrnH-T!_MD->agiZNQ6Fj-o; zp~LE6R`X9X0*ktCw+`qZnu7weNC5>kNlj?>4hO0+gC8Pw+-0ac7O4Y+aGqAmpp&ct zt>|d0y9q-#KNA?6h&D}-UiT-Grh0z5Mh0cSQjCJF8zSx;gU|scQG!@lh`|L;6)2WF zlZ8L@%KwCw4u!$fE|A$y}Zt z0&RU8s?m8l@OE=?bbeAPkX5MbmK?!w>|RF3P9k-QeFPg?YgHJutRP5Yw0Q%yeRLW6 z327=I=u`q%Q2VabHDk$Hm6&yij!bI5z+E_02Tn%z!`y^cbqn+>rW#);ZFPM_l1RwL zDZ%1YdZ>IF=&Vsk`Yxl{2pIH&AasDLYIx(5Eo(NBhNS{jGUP0Qt*ig|B|hLISq>v7dXS-1Fp-6jrm?ZH84v^YDB`Nr zKMc)eT$zUA2YqG-Pp>fb`>+MOj>cS2gJnCAPB~sFwcH@jSU@y}Ut1(Ofu`qUTM1%T zB^V`5Y<7Cf=#8x#-C&TZ0gGJ)SRyeL38+~tz)S$LCP-Vrqk9z)%B3*3Knr-G%^FoHF4m+tVb?tL2L2PMiYHYh^v4JuLLRh zc0fQZk!+MMK;_vC2~K19d#;*}mJttd;+-aEVQHcHb7U4mN$SH#c^)a0x;RAG13J+~ zQGum|5fZDJ?G0IJ0CA4bH|C)&pDck8PSQSJgQhuO5uIkD#-Nr{dy*0p`yqxYQe{V^ zF%Uu<3=op;KhkOW63Yab#3J|09!{CzV5)(Dd7;tPtnkvsZ=S$JG966Zu=*G@52`dd z!1NVRGgvnDz-*}%Lzzz_l|U+SUj`9Eqq*SH$f^^dO{l9`4|QlV)wnh;VtiAiR~N@YqhNi`(VQD=n|wY8#iGe>fPOe22zr?_=t4=;D5PimEQ^rk zmy^zR@1?5nOQU;Sn4d)rb(*ivhZPah(Oz;Fi{K1VtVNPwI6k<@1|CqDBM*7i1zRT! zz!KVdz^@-YoDGx9GB%)~84wz;ab^#3)It`;Y$uP+Nl-S3S{wx}voIVMDryK^Lp0k% z15a$K3kgP}%vvN0tPoO*cdgOi#TNfBD;1oRaNI*|4dkw{<9ot!nqj1r2_!8EjhH5K$UTT~PZXQG_CNgw^4ou}l1;^*I0;pp$_8N~{)*k!8lu?MOAv@2 zG=nXLKo>dGzU(#=Requ~B`&w%ljUB^rWtyLr_Opxb3fmBJYD4vM}EUI$)KW5$Q%#x)KJRRItcTo?;koMfUst5pTy^XMPALr0P&3t++Jb!jk zP*l8LRTT%rYrsap+8+A<2#t5k%TRgzc z*Y|=|d(HS@$eQ-f&Rf7?r_P=YcyNmlvUt&=VY;cKagHRww{c+iPxaztzt+`l@9D8; zM3K$XrMV|ho^*VKxKJ#SC;%?NhGpZ|b~4%Gs;pmCIpt>9lBHs?Yf^hVq@<@ox*ca; ztu1j)H{GPg4)DF`?&_+^OX!-Lrz6vVPr3)FTZ?KUaqr&1^OWjarOC-p;j28!6qv+6 ztgkKMOGDsnkfC&AKQ+L$=gpV)wY0b~i)HM>l`GYZjg9Sc=BKk5=P33Kjf@7#?QY+> z6Y|E<*;yPJX-3NO&D5zQ!QRm~*5VUa5rAqU$rPToR#@^d-faFe?q!F#`GK6A-ZT9E ztI&>Gr2jjp_yPZHyuEc1gK*=qv}vEd+^eH=Hf85Qsq}(6P}+^QAO{y037ubZkw6aW zhF+559wAQw;I5%JIn?^Y^Oj#yp`}%@DDs%Dtm%BR;{Wg-ITjsS9de;QtJNwmB1ib zg1nW?Rfd0Q%_);g-&>BH#$zfAD;^Lq6WS*E4QF(nlL?Bx=?F%rf@FX7-7W+A*NEww zjaPb0cGSXac3fM3RYN2U0qu36%qw$%juwrGrE?D#UFF?qjhxdcQMH$h!G!9-;1_y z{}w23>Xa#QOvlK?MmD(PWHc-y^l{u@nVKFRr4?D(Q_j#e`n|Hgjn3etheTXFZF3tS zcWmtJt_NQFvC*6sj*HbjUpRf%6Ja4Aw<9YnOV{ty9!nE_omW4e)SjxNGjmauQ%^=l z&Sr-AR0OnM-MX2AO~N+3d^v;Rim;zXBc5lrnmuX}{iU7>$H{a#@ba6-RJQ=H8IW?Rs2kcCJ+3v1?Bi(QTEd&ViJ zs2%6&u#NK&Sgd4ki^uZii@;ur1F$&vKek3iDJv;0jE;`Z`<*|%KReqVH$z&W4Q?IL zyC*U6I2@|J$CV?OFV8z1@x|s{n!WPw@A^h3R9zznbMLa(9mk%!LKvIX85-3*V}N>- zkwdHX)okG3wN=WlFz$^jdD`y0L`bdcBt1qG%0V@j=tLGe_T+AQ<_ zn2WpwWjE_*6&5xZsCUGO5q;Frzo7^}q`&G{5F4M?@&!N~p5P&(3!7SVK*bCh6Gb0I zQbNM0ee+)_utsa?yPg%TICS8sCs;fE#YttApkX+d{CD>F`2RlIny zyxN|TA}T1{dgQa?358w4%cNZkHtruT7C$-J(5G6utHe~tboaM=bndHcOy(_xD^vA^ zc<5?5Ay_n5yRNX!h0AoBBys3Nr8ZVX4X*3 zTQybDiK$^4I0-k5Y5bP&^bLP7G%rEefd_}aE{ERk&3Os2n+9L}@aPR5W|Boj66*EC z?0t~{3Q|=UHTCfVS;Fg{ur>>BA0Z-E)dc{o<+)o#HzrDN5By}z%Y`)9+Sz?=Xs8m4 zoP4c;x^(yM%k2YJxVQBbZEpt$>zT*^At2$b&A>~htB$6mSn+lAy?)JqI)E^|aea)V zqcU%%sBR!4KR~N?#4K1REiG+U_=*)P;GriI>wvE%;24BeA-v5BN3-n-O#_vkhSyfs z(5OZeG%?u^#}&V8S9@FA4w*mbwQ((&gQ8VZIs4%lc-}QnNYgE?MTncZL zp*ZpI>HK_@cL!d*3PI*4CiV^De~u8KG2Jr^mijxu)qwMg6)yoDF{oQ?d5r~IgehgO zyBa@_#kB-e3E|E9UcPKYt#<_-7+hK`c37>^E66_4W?hG~DMk1OyXnPqBKJ06_le3s zyjcw&q%eB)T1M1@vF*a`#Io0%2*X5F5qIJ2$u)-Q5?k9U-x5Qz^v)v~={rNSNn>s$ zYsC|5`Z?iY7z(c887(ozMJa;7tF$v`D~UrpQ#(d|+~S#i!?W#v(LC0Y{=l~ssd6rC za9XF<^137B<>z&?UN?aMI8sOt!6-V4dfX@jD5nujnwloW5_ybJr+C*Mxke>2W+6e1 zoN3md&85Lps4c#xi#a|aVR(%FciR9wT|hBYJD&uZshb69(*~gpm8wzc>*vQQ%(~jz zvG3n+6KO>~QHCHgkL=SX$sWlObnyg4BKq6e(T_eo+TbXD;(cr=UKQY-l(n>$QO=lg z22a1B?^;_d)6>%n$_L5byh|^Z1dP!%*BGJuv?anvT5$UGp8#Sllmwz#4R0uVWhH(S zV$e*bu4}G{v7}8s=}XI;J9n0L{sZA!CH01_m6cpX+2qW-i#tSww^hDzCcV`7yXNd$WlQJ;{WDfq}q%%vvf;9!mavy^GVkx_q&m z@)-r2N^Qf0GjQ8pycjEF6mRL=JLjTbn{9DP79m1Yt7~kW20#FQcMS(3*obA};o*5N1#(=r z!QWpWiF5~rVBH5$BKsU}>t^vNMMXJfWdIAcjdq3k&=3>=ED^-x1e(XassAwa;F_72 z%(^*8Ro&QEm9+0HjY zK5_gF&*Nlovj%9GKL%=xx7vxwQb|qiCgA=vpps&5cmliZd1Y@WtMyeWD{uRjEa#`Y-c>XLEgri>{3PSqs3RRu}vJ z&q-rOF=0*Pu|;^7@Rbqr%C1crr~JznA()?)R<#`~DA*0b9!g`ZeSd09^0hU`a9`@l ri%F7lMYDBG7yMsJ;PXFC9A3xV_6(_RNqG23z&35Hmx&7(t=ai+;BU$G literal 33357 zcmeFa2UwJ8mn~Xm#a7#Zvo_o$2pTB9l~Ez9d|M{X5x?M%%i{K>sj^+ha4G<1y1zeq{d?_ zPT-qco%ZNB9ke^<m#n+4Sn>#t#ZJ8__D{Ke;zSsVNd0j zcWKsLTu`pMF;c-!NwI8_&71=kcI(b+1uhlW?2QzP+_cp4rI?6@f*P~y?C%Q2eOF|+ z%0)3>W}H4f-0jwHU#V-B(C6nE5?s>Ykz|vso1irbC(pR>rqhLo{=slvdTJ5<{mX9u z$@KT>nYbSe#^IlL&^==Ov`T?~!FR`0Vfymd-}$G}-_z$u&|d}o|Ic6cy=_|>!A#K+ zt$pP7g)KhK^?cE7$=Alo=J>7)TTCw_;F^_OHli-J9q60ym2GC zzx8!fuR5bX9QVg})w+(Jf@G5yX|8>S&b{>y-=@xJeI3+3+iw14-mU5XWl`(v6>S+0>-$5z|e}@_M$_zcf;*JM7%@>;_Xt);R}@6^#15dYQHrP2)q7nz0&8 zadGij)v!}bC2iw7IyyW}Bn%%#OE#X^wQ}W3k%RXy)i*R$L@*bM89u7E)jxVPLO0z~ zka@w+FC$04$E6`$qgqCN@x?v3^zzQKsESNGqi-W4vHNdsu&hms$*^q@VP@NZIQ86* zFHS@=_T{r@d;a+258Z4hi!du3Q71t+ofQ=orEg%MnR@KNftwrl96EGpPweLH+x2l! zkKtai2R^S@f_Sn^Hy*o`!0vb#zVz3_zOkBdYa}Hl-8?+vT-dcsH<|kv-<8&maGt%8 zm)BZ)c9vC@cuwBL_;CJ(OP4g{<$dqmz1!N|9oO5C)7>88Q!J^{*m`^4mE{%jhf+17 zl=&V$eAp`e3SP>IwdtV{lsTunJ;fPa?PBn+Wh=7yREA(GpViUc9deENQ_lgA~r$!aFb7xwxQde zWs>Sg-(k?py1O!=?3#*fOiz#X7~9V0V7g^R zz@0nsxaHk(NqMFB58T#BGR*aKy!^}homjw{yLbNF@BXGL~hrZ!ez`ter9%PZszgTxI|yxR0KtYZcSoQiI& z-*shs|L^RLV!};!HiIvfV!zVRa6|Y~vt_|BY!|K?v`w4w@cKPXwDK5SBHMWWPZ5%Aesna+Y zPxEAV{&1s5l}zxPH~ZtxojX@iRaJHC?p>RzAQsD5Ozqxp>4v%P85uV>nl0Yux_9s1 zJYBAXk&4`L0#@qkEQ8VUvC#xuTU(rnUH(|5Wx8$x7K@gOii*|4d=jYphQ4y+Q2KZ;x(jg{Kk+ImglS7c+cm)SL` zFKmOCY&;fy^c8EQ<(f+Wn6i;uD|g^JZRHRw8Ru>XXXlDC6}g16C>1O0LKiQ3TV-WG z_u<~VeO}YXq_1N`77SUr_2mw4vM3eE?Jr!{Rg)UEbc=N~9))vf>7FaHBhdmDf1H}8 zsG_3n6yVqX6h)AcPpb(bdjqlfvDSLW)md6}iJNEg-ifM){9o{$P zvl(YTl*%%)4!93=gke*~sfF)#Z@Xz$9;>--vsLAr-q~>iW1pVPD}41zc);5qyO61F zZ2U1nD5qL8$8r z7M`2S#~Nri`)`%u0eQ z9y+%7h+7SO`lN~g(|>v1%$fcIk&%%GuIyzF;n>9Kgb}v5^}kx1eynKefe=}P5YPVe zZN+!jF=LJL^BPYy&q=qb(_&fQ=KhY`HS*!y3a+~nt5-L-@pvsfapJ_HYQvPHudId) z#`Gi8(l#AR(7G$*kx_7Y+a*egvD!(AK4aZRib_h?f`Wp)zklAxZpt^p;?0mY(ZTLx z*QP67TXUckS;1N_V8+y`=5KDSUn=XF6_$RD#Y*&$&}%gNh^3m{mUT*YA-9NzkiE6L zd-kJ8k1jY($D?q}2F!5wsampRiOU}?NoJ?}TN*O2U%TemkZ$+%a^-AB)#FGnhW)ZV zyLWfow#!q)4%lK_AC1M(S#WX+(>*x-`Z{$78O+h?F?WQ z@;sSe`&zm&*JIf5i_YxW^arU&gM)){8x9^jcI;~9aDRVxZ(lZYP21eG8}$~0le0GB zK@?015I(p!K`*mXy6;hVbhHpXMI)cF^aweLXUC5pPqCR{xX4X9xgmbyT+_R(oSb!1 zQaT7ts+N|P$!}`nv?aD~-D)xZ{aZ%EBTu!pD#356e4Oj>GzFLlBvmm7 zqxvuB0)WxMcXdw$V#$dvR!a6fYquwt(JNEW0U)0~JaO)CVIm0Z2aY`M;!=r>HCQCs z6phE>K2&37V}g@PcyexDcZIf5#p5%xxfd^9V`OARN%H*p^RxxhkZ3!20zvC7+hVP%p%K6yG!oO#Qgv)E*3{9t0pukjAt5o=|5`#f*F7CUX4CPO zm&Vf1x3?AsmH|VRM=J4bwyoE>A~SU3)alcv$vRFSpUz?oU+_z`%;uHJ&*tRDKc{>5^S{dRfZ?yuQ@BXq_JPgO!Mr%s&;xOz3L z`Nb8h*Vo0kX3t(%T9fb?OO66*ZMJh9#nrBgxVYWt7j#u8ha=aRm?!>i*+?y`O~47|Se*_K$ekD;wL~^;dvh~< z&ay2+GBO5@jg7HbU*^|q}+eyE~oMDZWX}N1vBc$aS&}St?Lo#sYI6gj3&wlXZMG4$=-Dl1xT$X$EtgoA5N?;gUvaqJ+5cfJQ4dAwlbSv#Aj>^H? z4;{F*>D`-~8!2=eWZCbKm6f%88+y`xu*+1^r9R66cl+kC;yW=mbr}|T>;6xlXt_-P z>6gQqsUI%uX4!}By}UFMXu{(2$7i^zSdAFg2Y%fwFe|IR_c&hQ>!FJ)SH2?^wpdK}Wm#F-r_ovJnJ=%4 z>6If_)EQ8j?i&}c|4<$4eZzOzQ{`d{e*T%iv&1LbvLd#j&60DBx~r@A;q_*9!%nEz zG8GzIJ#m2t?d?C=Byx@ru`G{$wrCAr{4dt9`1WD{T{B`XUHa+39VxXBA3pRC4PAd` ztfsGDzq#q!-Mb0rR(R{6DncRpx+>)M?YPV)Z@nEmb{uzh*2IZn1Ad&`uDE}{#)%VI zDB4=t?1c7U4`u9JF~40?aCrL}jvf4iqkZ|^=~adYjg9XucWb%2D%Y$s{?N!^mC|sbq2!Ovv*v@IsUc87q^5kq4uTR^XB?Qk=WZp6K8m#afL#QeT+S`2e z<W&nL%7dUhFY%EsJg^L%< zd?!u0@Xefff_h=zkxKz6%0x7xRa-t|e`;S|y15+4d-AAuf^H;$%$()ht1FKs>kK@e zEg`gZ>mg~Eo(Ji4wUq9}b~Dv)Z?VzHb$9mq{ESD^eiP?X7SHwCTll*{jtgav+6zWiRzt?Zxi2nLLD?7V(Bx(GM zyUFa?>ZfPS)5^4M=&AMN-Fgox*{EHCPNIHR?^k|jZ2SW^e%*m`Q&V2PVDpRYhYy9< zty^a_iuh{Un}5y5hi(C=z>Mr{Z0UL!i`Qn)$4(<_P=`d ziWLE%gMYutFrcUJJ<65@?8({MFWwr5g21M}g6+fC&1M^P)CkehT=S)66~;!olPLY$ z+dn{PMUflf6_you zwmP=-^(CRCtgS^c9E;S~K?O?2> zN$cugE8?_O2D>U!JyaSKy+CQWEZeRDE@kyTzjDCtKR<2!37d?c&NMxG)Er47)*vTiWT4aW+LfhvOW)OwU->uN)NbOF^R6Fq|JL8C zo9FpZAO_nQ!SdHlO~7ozNPtBkJRBSxC@-X&zgvZRromPpR0jpOcj5c2zJL9UM6QJz z`Myd>@t7J8T_96zJl0)2J4={21MoN&6XRoCbLZwYwBo0X;Z3PcC4IPF`r^IT>i;MS zhu``~sQr&Ic#4g^xZ7FUMeMW=mmvR=0!&-0eHHWBb|h*QJ~76u51(;mRz*-oA6 zC%>Q+dl7pm^*+eR!S|=<0jV6C<|pme9}(<5Vu2bv7LgX)^_t&u8QuDGvv7j%=Qh9H z(FIPd$|@=M{?n(sW%^&Pp62)P#OHh1(Ba5HloH&*0xVj(G!|4|tk>ue5`arHplBSJ zNJB)9Tbr$}-`Zqx@57UG-NX-Seo$`BoW}`;zz0bE1Z_Ie1 z-S5IdPB2L?~=ANGTzPwRoa7XFsgMCd#K=iky9PUw#hf+>~ zwPEANL4aoz<2&&Vsh5jTqP4ubw)t=Y!_Cc&$uc%EasK-02+Gjc8Mffh5Us?h2u9ME z9Q{O3QA+5X?CRE!AMbH-i|9LE0%2zGdm)fD5Ag-}5^Mlaz(wAfK5x0Tq{~bHl|%p& zR?w>>cn&@==hM*JUGqAVUo0;})#9-01)-Doz3<=dD7@6}~Arw0~9fGU0$ z6OfA%YuB#T^Rcx}1OpT0*(@NuX_H3&_x_6@c_+BDhd_Eby)T%Q6M_hQLzV-w{Qd}& zU~g<@XEtfoq?lIZd3osRA<-%Z2(4VPVnt4JadEMTcH$lI^ny&q0|zvL0l$C$-d}XP z%B8DZxB@j)LAr^F2{#{~f&Mwl1_giA2ksDAvt}<9Cf3&R+ht^I*f($9M4T1>B$Jq! zSO{+K^0o0tX%9jjw66%iL=HpAESZymlZr*hQn+|s2XCDE`t@szbT`Q$?|U7z8&(g7Onl!5 zE?6D$9WdisQd3EZ3M$=gj%}9=MnUdCA=s)JucO9lVY9`+Nk>LRwA@~uBxWZ{XW&s{ zurLCQVT`yzb~Ntr2r#}z%F*#_HX!|35h&hshVXx2nQKSxqE11I^la|0bbEhthEtQ* zaKmrmI10mlZPvO|8hGTZR;^m}%P-MLqKn*X3{Vab_M9jCwIFfq^gLPB&eBNk`SXuh zd341@4Lq4ASA8ZX#Ap0aljoNMd7~k8qnB&38o zsT_PP%4G`>KQ1MwSP>{$V!`CH#vdhF^tZes`iLMSP?rVXoCse-pD|~s19gp(Cq5na z8hE$w!tcMoy|LZ|Ia)aPOR;qS;9$i?iM$gR{g&hHeODj<*K#f^V&Ph41=jN`D?<9l zz+Gfa6YebWrNhvkm>8+bUj(YzwaH6+_1;SlJm}^`L3^?)k5;4N5(Roiim6iWXm1Xo zmWyLjw1Mik^2#b84E1E2KqCm-Iy&5!1hUo0RXI{!IrGye`-eF>uc5z`$PC{p4wkuXr;mW%=sjvP zZQ8U~6&0G!UFGk>BmoXSK8(-Hd-MU%vdp z`SWUf_nrlozmN24QI%+bfShsJ3+3Hl+ikm8pNVnH>IalkBSC9H74Co{kQ=s@vlI1j z#^1hu6BZQQ11eS3)AJExj?Vk68#jI-`U3Qu7C^ipGb}93vMwV5IARkLCA0L^wY8hQ zMx5p>*{~aF{M_ zzZ!vb91UWGg0Hm=Y65kfHYH7~R2@2tVDtP8Q$0##q?(L zxBNy%=H}6ri3V04cm4fCH<%VigMj_^ymj#0nKMgnrSh8<-+ARKQ2}1!g6Do5g27wI zw;}?ixxkp&y(yf>m-02&VB>v0DMwIAgun6v)N*CHUJEeTHL2WJP-H}CUl z%7pLVqKFK~FbW+DDmxptNc=B$Ujv}d_?`PSJVp}C@@TeX4$pRe@AU3Od47P$pAwHo-jrUwKp zqF`Rki_`1`>v65ZvPe>yC88B?enJ_qXAB3Up8PgC+6{qT8_Gvs9x6jp--osd2<+yO zFr+NikOcr$J94@B$KrB)F+-TyRhu3MoEMHDUtLQOY1g^AH4Sm3oDhA9i_X{*R}s6_ zvF&A3lMyi0=Iv|g1U;@iXfCyTo_Ksf5U8JcJS+fu~{#G+9_&tVNPNBu1NtY)$A(G3+CcSQq|!n`a|c%g*xd zKYTdU4+`XABM5VFB?vM>alZwhOqAtTX=ws;5Tk;Ul9C!ckaGqr_3SL*5db)RhYTD& zYvGz`=-uDiB)abGaR5#NGo^&AqmA18!|)VFudJiASayS8%SL<6k~)LQjQRz~@foS;?}Q6PJtFxwRWVZnw44q-#O~Pz`=rB`9C8shZ@gThm5VR zrKRO?U3ofyogGmkG9E(#kQjh!N=r)v0sV@#gkKJG`?L0m@oO=kiKcZNz!!m3ov#! z6bwldqHP&j?ETYXO5`T=!}nfJvAXEzcReR32f)4p*V*id`V1vyy!zvcZL(AO%qyFa;3C{eCvUzq6LBLAs6Sm|G2@W4zib& z0|8-KS;KvPylNnN6DB~S!t@}iy^>Bq>X(B-Lv)i7cHaJRdInH*JlQu8)-uMaEJVS1 z|03lj#y(>a1?U6&QMc&kxFmE{B_%mDKS$wlfE94-RxH(FP|`)N@8rC_8s45&p6}y> zl2Xnn2URs9c9HTtI?~+s``i6F&aU2LXNwimZ5wJoO$Jd7lgOs;vh)gjjkBjTkm^y1 z8`Pl=b{gnVb31W@BxHX9=w!kWDNtvEiW z9iw7aaW3X6UivXA#5={HSld4|lplgz$(sdqqVMgo94>ILvxMyfI;vt`9y~qf_=0>5 zHwEb>1s`r?r}ae!8J33bI9;CQU@~XvCI!UPYXW?-o;r?gZ{9&;0nw-@dWHg|Z}_j= z5zse6L1=fh1{qa?xVDxa0X^E_Gck5+qnYx_d7Q$|SNdiQzaSI!;6d0Oz;7mj$yQ=P z0J%4P&eCd^&z#Jd!%P;qi`p?3&&m=vz+wN7U7VnEsvOVT88;S{(ksjKw(dS`NC#2+ zEymnGH2g>K<{-sYP}s*6Fa(*4FP`D3C+`z!Az;a1;gxyx4yME%%nI`$z*MXB7a_x92k7TCN?tPQT4KhqEB}QAYliB^&P-tjFnX)S@5sirha{ z4sdrNZf@D!6|(VUKdQ^HIS7vw%J+B#3M)Ky5rgbBoBAPk-5{(Vq^oM6<{um=Q(1?4 z9R>oIFCfx8|5Psnq3mvR?lK4U15n3KkmWG`bDwMY1H$cbs5nxHH44f`1Xh$eeA_gj zpVcOtoLi%OCmPzA6&^M#0+Fr*1jfOlTbl=qrTa)#0Pj%-7H1GzN?j4;_ZZXH*GbzW zj+UE;CyM-I5E|5VbgJ1>96&brv4($kEj_pIUmck0fE)vG6!27)SEbv3*a>W5g)0R^ zdtG48FTcverlt%34* zssLtef<%Y9DwKdnaCdSeu-=n$-RM+ZUDqB;OntXcTysuOR-lmxNT23v95brfl)O2)QXN1kb#$A7^uz9*uju zXW_I9D%-lcV!g*kETDC4bNKl5fb1%Iu6}Pc{3-Cj6ehw1hJS}`mj>q|LjFt3^e&Pp zQPf|Ed~0PEJo&0IYcn-C7f33@%4SJ5gT^;i{*x8~hh!13>|S z>RJ+Opz7olaPT|mg595Vi|=d=hpJya#=G@+#4motFZ-_k)!Cv69GE7&fP}(K3j>nV zUExhZ*d)yvay$3(<+j;xt!B-eSA`dHFI;Hiy10k7GwcZhOgP$Dwf zR8$OW7U;yiuZmy0WsBCkckiChqqtkZM|-g|Dr6Ck!E>*l>K~?lXy!of6Z@wL1D?b7x41Us)-B7~DVUSA#Y@FxN#7 z-NU*P5)(T>xf4DW?q7b<*&n$$5r72FOWdw=J+&w$L_tJW#2nm*%G0v!{$)tU5v0Ip ztcNE`14zF?=75UKU;w-+1IXaDwczkJ*)?v5F+u@G56Q6`bpNEp%jUHVe*XLpku?tN zQKnSa!yP zX|cF|Gy>o{X=y#H%6K(|CcBZpmP@h1oE5VAKoto?Af*rcKwq|Are1H2!WvP4?fl}* znKPZZ8#%pFSg{B0Zqr2JDdyKtcdLHS{<~LoGi~n%yLZLx91X$$uR61gev)+!xFkyX zE-OFZCu|I6gmTo!MWURHGWUgsKM}m%xOY287cN|=34iBCCHghrC+oQ*9zS`q8PzEw zMN9W63`&D|Ttv_xyG}1*oHMaZJ&&VV0pfAZnWKs5#m&nr%B;=z$=6CU6vKWv5WM}N z4j0FqzVC>L0`aX_f8@&`vH@qKz$&~h7Pnl+U1-gk=R-eH>Bz{^yW!2B1eLU{Bt!zW zCFlc1R&V}ol zKGpCil>uqY`P2Iy2)-@db``OKAcrlEaWJjD6!I?|DWS4nBk*rW7L4A532_jv)0>AV z$S`>QV)!{`rPtrayAT`+jiz!SrWEhN9RyT#apO;V@rdicg*~5?jK4hvimkvpzyCgy z);TKzWk46m?I^;dxHVmY+J?gaz@YAa8d&6ME^OW0PmgDVB@Pnke8yuGAb?b*2^TN< zAl_(mPJ8atfhlfq2`3QM>@n2y4hu67agMlSB%|#&2;CGIymIm{T!;|i@9+Qd7Dwsk zy&mHcG1OBN{_G-8U3!91z9F`THr{rV$@8BM{`G*kBMjX;1;q}ffMS518NpHwrMZ9w zfaVBNc#-Bzx-|qT$P|g^V4cJxszV@9XywWs5OT^7s>nowSGXb00iL|)WB3OFTcY(J z9?z(vOF`z>gbcz|$3L7~Ue0&q=D^9*ShN!LkC$D_M>_NOK4e*?4dmJA|)Ev^khae*T7_Mgi@Ia$VEo8%=yN*aiNtaIn~Is927 z{4t>XbN={99CWeOKoP_PHZ&N(*hD6CqBeLpb1rY!dgr}ez%X>n0^nZHu&Im1MPv+3 zo4wc^f;$;8EJ3b|KI6Q6HNZ8vc@l@PkE5XgxOjors4}@Cb01C+E7+v&U-+}!=_<{G z!O9#afp0ql1ORuG5M(Q8)BIDs03CDv_HDheG2AsR&~g8O{9NkDuN86=-&Dv^`)#B+ zL@o(CNe8F2@gIwQSOFf5b?9A~Co|wr3LsXf5)1LitSCgzi0o%D-x&SvRwAY)q+3;4 zLfCMJGnzfJbLY-9AU}leLXDHOg5I0&m-Ej=_M;R8-{CP2TMfWOGS~bSEV4g%M2Mqj z1L4>T=!O*mS&rCHIU z#Jx38+o|L2N}1t$N4ncEb;XpImwQs;WY|ltPp{bxI!J&F8Y5_G;o=K|G5y#Q&QFD~ zmSEK{lu@TId$;0C^gmpP`H8;Fa=ekfk5lya?!gb_#y`(H^V@HWYK?#saNqt~Mu;Zq z)-@Z_?V9r5juT{7x-Po@AGdf%7c!(K9^P#BJT9(MJmbGTJw%@TW2%5&EI@_7JvwSm zLCAy0@s+0+oc3?wq>4s}{-*%OE`c8@{_R`UX)|WT0cmE;80dv7myEyQGS;XB>;LVk z;l_r+{svO{h&x=-?2%iNc8L%mX`$Zd`mx$fL=>207fUmJy?6|) z9N^&;ZGJBid=6rdKPH0L*W*+efBNA+CNoQ6mt1@CnX!kL3?#k{hMapVCA4dY2zrcA zSC`0+#-pQ!d&v^FhGC=$PCt}y&V?|3dcaI#zdh#A9Uoq>Yey`~|0>TPUrs(auZ41n zugBF+gHdd)`zo^Ec7qty1OUQy8sx*P3FK2G{-bJqqT%n}e%E!AOOFotiC8%EU`Z2C z%%)=(9J$G*juef)ouV}NK}(eLi?k2WPu{=X->};G^T$2t133L7cLk%kBQg{vCdzfr zi~J6YC?M%5Q71{7MHwguUH#3R?6+$pcf{n$lQ-GFm#3}^$m{u})k8b39kZ`lf#3Gc zxg1lp20k)P0z}j>1L!}K^X9#?u(aUrp@teDo5EM;%|@J0{fuoiaF`S z_kYur^V6^!jp-%E$&0kb=u7*3h)rm8(AfXur*}`Yhd3|e-?gLbvScH%!XNff96__y zlTV|ZpUq#wrB2RIx_)4Ko5n}qZrxTmYG!5yl9)Q5sN_Xrx-Gt#Ue9|n%p-!jSiE4= zytT#V7N|CAU83LyvIwS9R)r-!To#L!<}=}KYGy_aQ@7wphgsDj)sKS~Y=w92C^Swc zhPdlp_&HQfX`nvHF$-jo3+vcgCgG^%?j0*(kAhzAtd4bQtN4LV!9J({Q=%7wqb6|D z#Z=B|WvzZ-MbGKq+u+x6Hd8LY7FcT#Vm&$OsQw|69365LZee^z}!M|7_%PuQy z*D-&^TW4~v2b$j$ZnL!ww9vUObtb0M{8rum@cTNehbju6t5}Bn-M82xwIt@SxUgDu z)^A)QcbZm=OEq`CKR)ZY&hp{9%X)XZz3vVVx`*2}zjDr%U@+tz)IvEVA=w!T%7ZOA znyzc}0?xicbdhwSElAk@D%}>`@TA@X2u+ZS+OYUAi0dp~m<-t=wIlQJ`42W2q zwlTQkNX@58>BW^5nS&7NuPpZ(P}^)>t-8^yxEv9fe90mZ(4f7iN*jS3CG@-AM!f*kfFr@Tyh!6cP!a$bZ%d<)?dMiNXP@(se=`uZ)uwvX@#@9R|qA{iyJ^6ig%| zL+m)16wI-rMYP2y&SyZ%cnPn=ZArW9=-?8FLCmH8KRRWUFG?&d??_6_kVC4flT8Gf zK=hgl3co+Uyex`%kIz;V4pcq4%`J@J;eeRJ4Q~#Nl-L2}#=~7yML~g>bP$D9*aOSl zL;(2?6sBc>yac)l7NM^pz1tBU3*65gFy$wo5$d#*M|7-2*=|E(9_sXq95K())wK$o zANuY5mK8sy$160(wJ3sC1swL~;P7BR2?TLM&OO}G4t%$~GC{Akv-6Nnx?vSgytTPG z99GZ@Bx+Dm{U{qB9C?D~3qdAk~b0}Q5(pLvAJl{o7`)OI|3Wir9P?g4%Bj!f|=Dv1>%<`tnNoK6R=ChCYFL@>Sm zFa|f#4}wEix7K--St^nL{rD62(y}sVnCx>hU^-oDM9A9rV@$!0SgDt9-jtu2&9lj= zL$yxa3)vJPau|zJ;hEGa>oGu^6z|dthvz*}h?jCwA?Y$bM<3Vfd9vdU#5Pd%; z6zu4N+J#1*n7Mr0BJgbM#?%cvyV@3FU0Uoo_i!bcI%es{lIu=cQd-P&^u^Q~Uw1XGDBB2t*h282Ne<*1lGrAOb7iSj$T=xX!5$DCRVxlzu& z%W!N;?|1o}bB)aZkTbB^suE@R`Q=$oBLfyh1F)=>z9$l)2p+kv8|`BtTiWU*>>e$f zHFM^J%XH+>0pYjD7J|niCp@a(Bv`z`6<2>~2B~mHv)h@NU{=e|-u=O*l+3VNsf!9! zW8L6Gr%o0XBZv@sdXRgeGV*!f{Dh!v!;w(%M7h+-ON~RJJk{43tiKNa8I56I-%rUI z%10xmh*91nYA-`yE&&MWA60$%6Sm!{Kv|~8j@61i>qZC#UQdIMR8J4+VX=f^JYoeH zdG=hR`J8(bFRwI;f_V^xB9E3>CYw4+@$V!+^*#{g7a-DvqfT0f;szI*5qSbwH3P*1 z@j>RF-v6=9`+F8~McAWp*a;b5(4mEgKvH&(p!uo3aHu&D}~xW2#V!@ZU4z0jr*9`n7HjgQ=j5<#&9R`IT< zE1N^#m)@7(K^{axq>zwiso~ujg_kC?Yh6))$&B`-{>Cv#n<}0>r2(#(y4)VeQn+R) z{x*4947%v3`GitEZj7c52QkSfYlZT|ueO>52;5LK@cKi9gAU#!t20DUGc=7>Akk7V zu^*|R2*?oJd6MHoF2E0@F0_>^Esew87p@9l5<~4MtbuyR5`^e#!>0X6 zT1D$Pmt$NgL?sV-Do_z#2kJ&SZ#T01;4ey4cslWRuI}=WrMGF>uw732jQC64{LMBLvBxC?oK|-kiVGtF%&`)b-kz=H| z0<8S{@ahAOAD7B7D_JfBF_TQO#Fs;)CeaBT-0=?TP)8lv1=YBsrzVvwwg~j30qt?% zK;H|={l(&;4=(dbYr$R_ikR($zP%h141VwjqX=h|C}>G0v3aD#5qSm76C{9Wrvwf- z8La?`7+avoB3K2Lgy1{`ZHd~898eS=5W6IX_9b}%5PVzh(RATRk~7E*li6ye6~3^O zCZMjWtp&ytM5V#$tZD1*d{g4eMcH}~yBiEZXfGHaF53Zw4=z4`sO>#}L zAo2qQz=>|<1wPBij2L*ANDxJ8O3^zWgoau2 zA3&u6ic1!I{aWv%YXW*_;D4(zoH2d6$mY$Po4i>)BW-H|GN2b^I?K>ST*zOLU$YyM zC(J#lKZO8?Uk@ci&k$U@7GP5sZKo;Vj>tESW~sW~F))3lq(d)VYH2ka>@|!8SW0G9 zu3(`un(2^*i3?Ul5Z{F2{oECx%s2=glv5#A!&+vvXYXE1a1|qC!%a4QMo67t_Sn}y zqNv}6he{^f1Thf(7kMPKKq7%w)rzPh(lhjg@Mopb-iPhMo*^=YqXz0#%RR7%$OfBk zQMMPis1h~9W{|0%cnqOVqz%rVH%}Y#GP=Ep@`LJU39@P%++zSSr=@*3N%BJa0=d9#fn*L`V_-AS1zrV!o^!+PI z{vUcb!Vrsb8yH!9R}wlu_AKD_tQ?zd7Ua9?@t<8w|FiE)nEL#Pys;rYnva55c^w## zrVStrh-}?j6V`_!w;W4{SgqSo#2#mK@qP~UI zC>$l!!w)U9o*GIwp%sv_JBrP$)7pe@L$lU#?+}sGiqp&=8@i2qyqu|Rb`u3MHP4>Q zJ96X*)#tg&`}dPk4rb$yw-S9)blE9!8lvpiDzG4faNO+Hf0X%;x;xAc22gNY0eA0; zL4c*v0gWDgfzON`I@Xfg?Dd=%$|@?Z)de3>(q?*zF?>&XSRddq3wj@VG1$Rn7Q?qr zL*#ThdJG290UHfd9^|zUflhd>SpqoVxp6AMWg**SOf*I}U4C<0QU%`EY1z~1Eh&@#f?g%CG+PhmFX#ka`s;WO*r!nQxBSG&fY6OKb&^lF>7=|>9YoMbW`tu$Q z8+tT;pE`A_5I=utK^5l)pAgx6`@m6C(-<5>QCXQ2<5B$NG}EvFR1+?JjqFd2WB^8g z?$9;>3@DEe>OQNmNYs?(WP%WB0OdBj516CuwgjZIER8rnGTdj^ZIo`Qd3njky+m4}d1tU-$~<#m zIAU60v-@WHh;HE$ZGaJMW&tK7&4)B$;as!Z4h&MTtW7b!NOENENLL(ngCqHqSb5H1 zq|vRN52k6050B3f6GuxN$B#o*={kOjbJ|vss}zW4zCH^3vyPGXx7Gur0C>u%BVo!B zmgkVyis9;?|ICPj|M!d-Mn6zpEb8g`tw@?Xd1c5DEjI>3`Hz2B5HzlmI(ll8Tq7((nDR|%st{*B-0shtsJ;tD#N5wPjaj}G4df2V1Yz8<=+ zPcyEG7ANwrm}u{r>}!1HpWP)<40>Yu3Cd%eZ9S!N=!qmv8<0Pl{DY+U>3`(3)YgQ^p6lcIt-s_)zvZKSi=+4%Iq{^XFoSN;>e0K;t-Occ3NkY@YpTSoj4B7HeN;@PfyS=$Gb(}20}j~ReaUbq@F<^*EL1h) zQC($SsfCSj__)HQR3-W3XI@_GvXazNaL5ZkpOMO$`U|laC8(Ckqlr&z%^afjwmKvedevQ#nZ&w5KY_1NXE%7pHo6xCmTCU(-Y2fz=hY!a(*&3rQ-;v) z`YQQOT=P(U@qUfvTW)9hfrCZ4)eT7D!p51T@zJAD-7-HSF&xqv#%7@!xoB+z%|-Xo ziDa;)WT5=(f$7w`@?oY2d2ROZzXUu$_Bb(vYz-K>-(kee`<{M!a#B}cCV8eXt_WAF;iBVPy;7r2*V8kx^ zkOe&vO778QopOwPS*tNKRLEe|?$k5mt9bMnx3d{@4aP9ZC&Qca|CQx3E$!`VF)|L* zrp)1w{TA#9ctz839_8k0#;6OD%K)a(-D(C4RNETPkGezXqHZnN5H&CiD;Ja8w!y@P zA72pmUMW#10E9|Ibmq^W@506?zIUi4(NyHJ?}Z(pCu|Tx+S*mp+vfV}Z3KcM9Teb% zYz%N)CBds2LhP**`g<`nVJG$&xgE$&3r#f+N)Zxh5^Sedb#CyaBtnIGT~$S67SP#S zPF6j*lPu9-R{uqQzDWG2JBBO}a;CvQ;HSIc-y!XpgaYUyrRTM0nypp{+fRl}h^hmC zY%!kSCK{##*SCrR1O27nY4<&{vA@2$>opb5+ndaT?|kV<$VZI`U5;O z0@Thp7-9KSM+U)vjzO`QkC!(d-AJ6lkQf+pVf>@2=!3L*rX;6?1NU2iip|I%7ZGkJ zq4ix8=wl%5o{9`S_TJoKLu!PBDKn$nEM1qnk77_w(9~3fL6{O+?DM_{UnMw$&M30v zB3Q%#xVU593ONOGhWCO+Mq?mNqr^;0ERI%;=b#CjYu85k8ZBY4241edqQD{pIII_4 zkRMXLQUyr^b?-@jF+lCmFyUql{foH$kP{D@x7qbofaz}mZU%tj>+Y`WBg2*c?uu}! zRd`C&`cJ+}Oy+x82~)r}Ol_S1*fZA$ z#tcbE>0xIOkpf!>#!Tb_4v4aM-K>BV21j29U_MFcs4PgP0Zp0XjptHE17I;ng zrucw~(Ll#N`c@Lq4M$x!U8Rv131(vNGfSzz1_N_%S8jCf(#Y|N#`dN;J(d8rBj4IX z*xrxtg4}}QYXPN&6#=&_Zs@TQ3{pUi^kA}=fp1C&IL&RCrvOJRQ7=}I0O_=_p9Zmt z%^@EX+2mYh;9ApoE%V3$&|MHNeSavA-%%G7zI>_b;xfeNGksx!CYEx0kq(&#sWCI5 z37eHY5@v0@m&;VEBv^))Rf0i|7IlxJNAh-GN*;?^`I*cCBWMCAH zurS>nORsLQLNGkC(N-t|P;(P|9fqt;lK0ALXXb1GFZeMWVF=dAH*{xcgAP>K*6 z;5{~|jqYD2w05VCYw&3zHDhhId-%yszg2e?n+5nuc=hUC1jXR?p_mOxocwl@l2wWM zaK)c|G4+!i>g#W^bpd8if1gjyKQ#6Z)+J(@S9pFp84;J`%AC+E4a$7lr)yzh0R#Mk z*CW2GG=3RQ7eG4VG?u`fPG^G{s)AUd1>9#K7a%iLIKYCYz`+c850l~cPJDeK*Uv_H z5Y477T^8HsTKK#8Zqr7~iS{Qusk` z;}0s6CU7#f^J^gfW{Sm-pE@BegxAKPj+C)DAvS4yWB+0hiaTXEkLGrm9ciT>JB`=tAGZBH<9oUbicfP zBzwpUGzCK1mh9z>Jth!0MtmmvCM*hkCt*H9db-?T`9bQ?0Q5X&HHyqg4ap#Y%mTRH zFE;4Q8xKbwz}8Vl6rNkMF5LPE_MRcsbK4V-;P;@od4K=SdvM}MBjiyN1A33C)CLa_ z2gH=o)Vj&EaFH{+Rui4MT4>S59**)rVmfQG$^5N6b%Vpg5qRsC2viL(4CLJYW$L0S z_)Sf0@WE=6D>1NR!0uOy!NX+OILEhjEe7^spHn~sDAPK2>^^B8kRGDZ2Ckevnc;Kv zxDjB9E?@y%-4&>Y)6sN70tqr)dYu}NDP}xo!EGwaNzr6gK!_s^9Rp!v;DN3_Fp-Z8 z2d^Q`JBD>_4eeU&6hx5Mb4rZh%%p*7OE+5v;ObXli^+U`x^U@wg z&1lt=KTnGyegy9iCXEf$94Ze9S9LEgC;0r%4IfYzZ9qc3lv6VCE4HM902+e z77|jBc=YKM47<&5?QkCnB*NECagF>5sO2)6(KJMsBDAN*BP0|8A5mU`hggDR{K5FX z`MA?D@MThi>62dwAkvT%lpO@HVBxy8&6y-48!x1jRTrP#qd_Gwf8+t9h~d_f0t3+w z{qz+;P-dD5E1yn+|KNugKv$EDLKupK=io3S?>NKv_6OAE@-Z~h7(F6nCLv@41(W(r zydd(7z*3}%4Ztjg`Hcz$JUi;Wr0J7@L{a2*#+MmiT3cFZwmQ+pG`9h>W{9+=iUylt z04a*o|3%vx?Scf=0lruQgr#h1)6@VAC=8PigcMoKe}F*=X~|UE13Q8oM!>ToNEWC# z4pOK9b~J~jh^%nr?S$>#9HzGs_&N-ojC2^j0wA?Q7oiUMjRF?Vl$t(7w0pZIUjoK9 zM>3%yY@q6r;7BB>OC%+XaMZ;leXhbX|MMPLjL7kgxe-it>fc0n5e;|(lYSLhSdfW3 zeiw-@6nOx&ACnb9LN72LjZDIYtmVK=RHYc8?vx8g7b7=s*~BBoVRFw)6d|P*KT9H5 zQYRG+r2r9bQ?&#z6#L|LQ4xEtr{E+;)f3EjmXAq=2ZqLmnmoS*UmBCrE4&~Bz*q__ zjFqS9P9Phfi|=N#o_)-^_yJ>oZ9=rsyn;SzaMD2-v}|FOWR#zW%i|10phmg@Ol*1u z;~l(4XU1wYhLs19QH8|A z8S9F$OC%Gfnpo%WQeIfyHCi)dIds`I^+jJ{1ABtO73^|^#8(vBooGd|<#OBV` zf{C(Wt{20X^@nlmEKP>c!v2iryrG*b3zmG%nHjq2{Xe7f{#icx&wd%YNDp-_bR)3+ zUN2q%6KUZ=^nI0LsPbZLH2xH*x|zX=D75?s0UK&(EDe&0!LXh6HxrK#H=qG}BJP3a z$J*V8h7`I#Fuj_l>k@rLt+bHp)i4BxB^r;ABgZSWy znyL)diU6`@0}3mu=wJos9Z5Kpx)(bSAcowAXlL^pljCIwozbbC!4T3!z=}iw@4D;S z_>RZOGc2>wT?tJmbQPRXN;cUmSIAyVN<}ov6q@r*JvX2!Ea3Jd28TWag{*m0_<+^x zd0tJEFh_efQtK8g0u%D1(dI-0Vaam=Pi8kJ84#O7S4(ZjDY>plFSNj6(k0b|1^^-6 zmtptPmwF+n!C+Szr&~k@ci0!{^HiWmV&?Q!0U|x&B`HIoB`-Y;G4i;5WNi`G&*CRN zG7H-z0KS<(>$NY!Q2;^i^WDUI8 zSRw=@K%N`&DG;+o4H1~Nfd+;n()4hs@fRkZI7m$yLr^l!@g^CEKeV?;t=x6C0}}() z;q4%+Ba=RYfbthjo5MR1A&DW+-4AuL?qywW|S{h-eZmu}jpm zBp{5qL`omch9gh$2)ci2dOcZA=yM^AoAEQ+qv9~%Uj;!f5VmcYLdcIo^8>KhQ*x0u zylF~4g%mbwwW{3V#yA}Q6;c=rZT!m!a^N?zTIJR;gkJqvZ*T{hkJjU}Vc;MQ0v4eW z32an?8hA(aqpxJDgP4F+8cl>3It(+e>|5@(n?=|=XFZwGb8PaSJb5C>B!{EeZiBjD zNoAx}B8y-{uSRmp@LkOWn#m{up-h%385m1g5fk6r)ZW#a zZbLVN+*KH-0QvS0Y8p2Sc6;7y^&XxqSYzv|_~AoBTcH}t9czfkSd&q}VOwDojO0du z@l}t+E^=;KMs(=&fMw_J? z?8PA7s7VqPA8e0R9r~HNdW~*DVtV(HkfaCJ83o%dVZ7J<9ygOA^o4Q*Apm!b*WQ(VjVJu9xVyqj;ki~IwgQrP$Lweyv46wHEP?IV?uA@gbKa z@TgJH8@g%c0siKDzTxh)ZZ!A+`z(SV0lq6~-jX^I8Q8|C=@?#eo10-ygG=I2SHCmg zkJZQlDsD8K3}Sx_C@eI1A3%f;#8RO4nFNdgheB$aw`|)D>WCpu86cZRQ6e>KL!&2m zH$Jk2hP+VLK=`Fmz6Ka?L&zP^hQQakFVCJmv%uP>{&AbSA?P&78Rnqm<}TB%{~c`& zbYPm@jg-b=mB9LpVLmoZ3L~z72Je(sup99?FZa+mO9Tt3vA0AqqPl>%y!dPPRE8jx zF8s`qH5sR&<=monVH|Jdwb3|dz&skducKXr*h+BM_}GN3x6i2`3PfZCo*I1;0g4Fv z;0^4$qur^flIeq{sCq!XSDR^G&qDe491ri8MfyO=>GD`1A26Pp8_)mOH!LZ1Wm zI@8BXp|0Z_X&t^qk^?<=jHoTdVW^QGqcnP7rccJX;-fx%)dgXC0S$Q&3IQG+QY9@S zqPs9(A{HMULhWAkfjHFeNZ5f{nv_2t3m0}5wta>-0y|oHUmSSx9Xm1>&?W|FxKzeH zjfh+@wpLWWHslQ7%{$8QT}bFFvM^+PU~SILP)u*2*)P=gPlF{m!xk{I6mD6&+0;E) z0YhdQY>dzO=v76(OutIQV z@1k#vNLm0SMH*ZJ`VRgXK_&%vn&VrGW?vc&M??QQZy>91T9(jgws?HnB*q2uGlXiy zXvSSh)@3jflW-@=n1ss3Nk^pvm(GHwMma^AlTpb+v@%O1gHn%-f>fDcxxzb zSGIrROkhuvq*Y_}NB19W+&dqQ4w~oY^7XUP;@U9zN)1Ne64D3kshAjRn&?DF*gH@H z1J9S)32oVuQP4hvu}F>+w`md!hbO@Z47VqFNSJh8yGBuK+ggYM%@M0i<`tBa1Jqnb zQ*NOvV$Bp?#8IrEm@@)&VIU=mkTXmsGIA@g94cJ)7i`sTfTVZe6ngw#ym-;%bO(>3 zCf5bj2>RU7v<6P5b+Y~>n3+JQ<9g^bC8#k9ix$KjKS5J+H^J*mABWLR{n`QqgQ>#; zDsD6aN?nFrl3Ms#5U``m{N7>sE}=KnNu>k`KnK!zI)bA>E{XTD_A}(?)4*}XbRn+IqHB?@P(4q8(=xCEn2xX=p>)L<+1oDdJnVk8I= ze1-*FsWix!##l6gd1FYOe}>Pox@$0%@xEBvslz{FLPm0`)a?I}kp!Iz)E^);_@mM= z_AtOF-GR6MzdAejxR~=bj!$g4Y;+OQ)@lcdI;nKuw!~qibyOwD&$hCb!_8I|Nf#KWBx(0t)BD#eMnH+-hXD))a$O(i|sjf{#@dr zld2vEmGeQh_^m9|dOid^GSV)0+L=H@n6TewLi!9R7a%8<)#%z!~(I6E7% zE3XHRN84Oh(^O?Ew3)lK=!CYRVM1EkP!8uJT475cUN~QQ0$i3*xkPMY5`Xyc*yc`1 zWFlDlb{e^7CxNXXft%|~(MjAB^xfDBZ{EJW{P1BG>*MR|dpjV3lCMpKYhEUUSiHBb zziqjD!Qc;TXYznE46uRA+)IE~KTZ*PBOFL>|C z<;M4aIbAHSmFf*1tTS}zM($|Dh~9a>RWTIsIp=D7H$Y~m{g0nRxl1Gx8FMJ2-#7Mw zKk|@DrP)#N2ooYBor%_5(V@%g>OPA_I+O!AwG9lah-Kb%bUdRf3r5c>xOX$@rNah2|{!^no_IOKJvW`e(fg8lmF>0a*x|Kr+g%m z-RkMp?$OaMj0dv#w53IaV`zIt^$b3{OCes|~p@bE%r zRn-)MAW73!KkFRb-L5T&f%KcbMkgD2dE+CVibz;0cp|I7>-uSb6t&Okz$d+Gci~rW z-rS!6Q8Z!Nw1MI0GTCW-vMmyoB86-lOx~L_%@$&DkB{3FZg2x7lHd6HPLj!F>!UEp z%TK3EWBT^%_Y2)Vz0&OzY(R|{%U7(3z!tZ#7VO|LXBxuOuc@$}y=C-=w{v_z zjOpR&`3uWQv+n@wUGlA4HB`lU>N4C>p3|)JX63W+C8GDhTQ$=$W6Bz3!r7`^5)whS zZ7AGhp261=iE7`WC^q$D#tQvJPcdprC%U<*Q6eBVGuuMkmI&A+SHQ;yB9H+0Qp$0d*t}R#Z{)=!;joyGm#p&*G z(aPiUikMyNc5t9R?eICZLb~Pwy1nXj3=hWB+j|rzkzo;9v{m*FhaVYN@ zIwRb-rKeBDZH1O9r5cZ@iugob-PUQ^x8Lfx_Iw-`8g3pE=t<@gf8^+GeiPfOwrz*yLXG<=9P}05o7q zAJ;xuVy(le5)ySnTxNHkpGOd#i+7o0o~qGpH1O;7D~!cr@7qRWbacMAGan+XDg#iVVpqj1HtGzOvQcj+mQZ%GLBI)VB-}4(I`*^&P3H?FjXu1R!Vf6-p_^M~+@uE}6Bp(>X}XFuzc{O9*%p zjJ`}|GZu$==_I9k3Ah>bW8WYf;vb{}TY+*=tXoOvo* zDypjRJfcUhUXc@H$Q-cWZH;^-`wv(<}%3)WP)&Uy-q!*$uaKb}Omf~Rl$5C5wA z3K(uR9RZ6-ld{s+r2XZ0mim78$#LG6ocy@+#MG%%`Lp@F!si+@|9r)vStc7Gkv#1> zbwR1lm)7^iu`WW9SRE0Ol#(JS){e4A7ppC%VvWWK_o%k_`purewAh8pWB2Ock(7^` zFs=j~w*SBZwb!J+;5Z_(s|N9Vg}Y4(Y{UzQ-gk95ouPH-?z=}j@}}Y4orP6Nn!jMm zl>X>Yq6B@bUhB&yczIo)c}xr1(h!U;CZJ!vW{s|%o+L2PB>dd@Tb;4)J_h_;BObLAh;Ki;2 z6w64W_xfwC~LpeG2FP%f5Z< z;M>0O^ScfrWnJ5jd3tylo0}()4?_lE8G)oQ8A5q}tv$O*^l#uthV3SKg<>gQbZfeQ zoLv6#56B<22>}5C*?<1nVLP442?JoLw8=DdMx(#D(B}v(!~Ilw7RJW0C{V&sP?6wT zQ3M=UKG&x9^+q^qN)SY3hI522bOQPI0-^vxk*)Tqxw+3piUWo@$c)KJYl2klwKpe6 zgQu9*MQ4SZWP+9RT6+x530olloz4 z_wE(N(!6I~Nr^LLk~^nf&D6pIE#BpKshw(S>C2v%1*YN%$}DhmRh8hT(92eX+O5;&vyxk&-=A zO1ek(DaxRCCMWB z?LOs;aC#+1p!FLx45!Gn(`x(n?H_ zgXVr`w^3yS>|yY$>@@1=7kpr0_F__kznML|Ib0zYheQdMZ2#o(>D4o4&a}dN&T zH^_Qd#)>ofLf?=)r*W8(#Jd8%+>)A+aUD49LleyPQE&p~%TEu|`lUcK>-wp>Bv ze)5k*g0GI%&{}o2y(Pvjy?W{H;S+d!QCt4Gj8!`UOB`bPu9AxVYljHfmYG=%nQUdCh>MG3r!6C7 zn))ZCi(oq7LWO67*)X4fI)MjuTN#KDRf3{Y0=ouSf$*)Uu6~w(?q$d@`>(#rT(_=k5_Ehs(HsxDAewH`T2)qHUGCKttg$Mr8y+2 zX#(DK{{~BaO|mh%b0I= zJhEjC#U0-HUXrKx1*cwUJaFTBVh7;=MZzeXY9Ey?cojx8z{Y?8HM3WVyuSTk(7^V; i_WD4B{}n*o?ord-xLHfj;8Ki0H+oK Date: Fri, 21 Jun 2019 16:33:18 -0700 Subject: [PATCH 034/211] Fix tables for v4 docs --- README.rst | 83 +++++++++++++++++++++++++++--------------------------- 1 file changed, 41 insertions(+), 42 deletions(-) diff --git a/README.rst b/README.rst index f1cac23..b58e208 100644 --- a/README.rst +++ b/README.rst @@ -67,7 +67,6 @@ Does your company or website use `DiskCache`_? Send us a `message Features -------- -- TODO: update with Comparison below - Pure-Python - Fully Documented - Benchmark comparisons (alternatives, Django cache backends) @@ -161,56 +160,56 @@ other projects are shown in the tables below. **Features** -================ ================ ======= ======= ============ ============ -Feature diskcache dbm shelve sqlitedict pickleDB -================ ================ ======= ======= ============ ============ -Atomic? Always Maybe Maybe Maybe No -Persistent? Yes Yes Yes Yes Yes -Thread-safe? Yes No No Yes No -Process-safe? Yes No No Maybe No -Backend? SQLite DBM DBM SQLite File -Serialization? Customizable None Pickle Customizable JSON -Data Types? Mapping/Deque Mapping Mapping Mapping Mapping -Ordering? Insertion/Sorted None None None None -Eviction? None/LRS/LRU/LFU None None None None -Vacuum? Automatic Maybe Maybe Manual Automatic -Transactions? Yes No No Maybe No -Multiprocessing? Yes No No No No -Forkable? Yes No No No No -Metadata? Yes No No No No -================ ================ ======= ======= ============ ============ +================ ============= ========= ========= ============ ============ +Feature diskcache dbm shelve sqlitedict pickleDB +================ ============= ========= ========= ============ ============ +Atomic? Always Maybe Maybe Maybe No +Persistent? Yes Yes Yes Yes Yes +Thread-safe? Yes No No Yes No +Process-safe? Yes No No Maybe No +Backend? SQLite DBM DBM SQLite File +Serialization? Customizable None Pickle Customizable JSON +Data Types? Mapping/Deque Mapping Mapping Mapping Mapping +Ordering? Insert/Sorted None None None None +Eviction? LRU/LFU/more None None None None +Vacuum? Automatic Maybe Maybe Manual Automatic +Transactions? Yes No No Maybe No +Multiprocessing? Yes No No No No +Forkable? Yes No No No No +Metadata? Yes No No No No +================ ============= ========= ========= ============ ============ **Quality** -================ ================ ======= ======= ============ ============ -Project diskcache dbm shelve sqlitedict pickleDB -================ ================ ======= ======= ============ ============ -Tests? Yes Yes Yes Yes Yes -Coverage? Yes Yes Yes Yes No -Stress? Yes No No No No -CI Tests? Travis/AppVeyor Yes Yes Travis No -Python? 2/3/PyPy All All 2/3 2/3 -License? Apache2 Python Python Apache2 3-Clause BSD -Docs? Extensive Summary Summary Readme Summary -Benchmarks? Yes No No No No -Sources? GitHub GitHub GitHub GitHub GitHub -Pure-Python? Yes Yes Yes Yes Yes -Server? No No No No No -Integrations? Django None None None None -================ ================ ======= ======= ============ ============ +================ ============= ========= ========= ============ ============ +Project diskcache dbm shelve sqlitedict pickleDB +================ ============= ========= ========= ============ ============ +Tests? Yes Yes Yes Yes Yes +Coverage? Yes Yes Yes Yes No +Stress? Yes No No No No +CI Tests? Linux/Windows Yes Yes Linux No +Python? 2/3/PyPy All All 2/3 2/3 +License? Apache2 Python Python Apache2 3-Clause BSD +Docs? Extensive Summary Summary Readme Summary +Benchmarks? Yes No No No No +Sources? GitHub GitHub GitHub GitHub GitHub +Pure-Python? Yes Yes Yes Yes Yes +Server? No No No No No +Integrations? Django None None None None +================ ============= ========= ========= ============ ============ **Timings** These are very rough measurements. See `DiskCache Cache Benchmarks`_ for more rigorous data. -================ ================ ======= ======= ============ ============ -Project diskcache dbm shelve sqlitedict pickleDB -================ ================ ======= ======= ============ ============ -get 25 µs 36 µs 41 µs 513 µs 92 µs -set 198 µs 900 µs 928 µs 697 µs 1,020 µs -delete 248 µs 740 µs 702 µs 1,717 µs 1,020 µs -================ ================ ======= ======= ============ ============ +================ ============= ========= ========= ============ ============ +Project diskcache dbm shelve sqlitedict pickleDB +================ ============= ========= ========= ============ ============ +get 25 µs 36 µs 41 µs 513 µs 92 µs +set 198 µs 900 µs 928 µs 697 µs 1,020 µs +delete 248 µs 740 µs 702 µs 1,717 µs 1,020 µs +================ ============= ========= ========= ============ ============ Caching Libraries ................. From 4be3e397af2bdc9f7e11e359d583afb833f70ca7 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 21 Jun 2019 17:36:23 -0700 Subject: [PATCH 035/211] Update quickstart with module, caching, persistence, and recipes sections --- README.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/README.rst b/README.rst index b58e208..6299ce6 100644 --- a/README.rst +++ b/README.rst @@ -99,14 +99,31 @@ Installing DiskCache is simple with You can access documentation in the interpreter with Python's built-in help function:: + >>> import diskcache + >>> help(diskcache) + +// caching + >>> from diskcache import Cache, FanoutCache, DjangoCache >>> help(Cache) >>> help(FanoutCache) >>> help(DjangoCache) + +// persistence + >>> from diskcache import Deque, Index >>> help(Deque) >>> help(Index) +// recipes + + >>> from diskcache import memoize_stampede, Lock, throttle + >>> help(memoize_stampede) + >>> help(Lock) + >>> help(throttle) + +// tutorial and api are required reading + User Guide ---------- From 99454781f23af5a4cabf03a18ae00b29451b8b89 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 21 Jun 2019 17:36:55 -0700 Subject: [PATCH 036/211] Reformat api page and improve docstrings --- diskcache/__init__.py | 8 ++++++- docs/api.rst | 51 ++++++++++++++++++++----------------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index dba3cde..96a5732 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -1,4 +1,10 @@ -"DiskCache: disk and file backed cache." +""" +DiskCache API Reference +======================= + +The :doc:`tutorial` provides a helpful walkthrough of most methods. + +""" from .core import Cache, Disk, EmptyDirWarning, UnknownFileWarning, Timeout from .core import DEFAULT_SETTINGS, ENOVAL, EVICTION_POLICY, UNKNOWN diff --git a/docs/api.rst b/docs/api.rst index 660d094..7a6d91e 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,19 +1,17 @@ -DiskCache API Reference -======================= - -The :doc:`tutorial` provides a helpful walkthrough of most methods. +.. automodule:: diskcache .. contents:: :local: -DjangoCache ------------ +Cache +----- -Read the :ref:`DjangoCache tutorial ` for example usage. +Read the :ref:`Cache tutorial ` for example usage. -.. autoclass:: diskcache.DjangoCache +.. autoclass:: diskcache.Cache :members: :special-members: + :exclude-members: __weakref__ FanoutCache ----------- @@ -25,12 +23,27 @@ Read the :ref:`FanoutCache tutorial ` for example usage. :special-members: :exclude-members: __weakref__ -Cache +DjangoCache +----------- + +Read the :ref:`DjangoCache tutorial ` for example usage. + +.. autoclass:: diskcache.DjangoCache + :members: + :special-members: + +Deque ----- -Read the :ref:`Cache tutorial ` for example usage. +.. autoclass:: diskcache.Deque + :members: + :special-members: + :exclude-members: __weakref__ -.. autoclass:: diskcache.Cache +Index +----- + +.. autoclass:: diskcache.Index :members: :special-members: :exclude-members: __weakref__ @@ -84,19 +97,3 @@ Timeout ------- .. autoexception:: diskcache.Timeout - -Deque ------ - -.. autoclass:: diskcache.Deque - :members: - :special-members: - :exclude-members: __weakref__ - -Index ------ - -.. autoclass:: diskcache.Index - :members: - :special-members: - :exclude-members: __weakref__ From 379ef3cfb1e152ec2bc061cc812ccb6802ca5ed0 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 24 Jun 2019 11:13:00 -0700 Subject: [PATCH 037/211] Update Quickstart with caching, persistence, and recipes sections --- README.rst | 27 +++++++++++++++++++++------ docs/tutorial.rst | 13 +++++++++++-- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 6299ce6..7435706 100644 --- a/README.rst +++ b/README.rst @@ -91,8 +91,7 @@ Features Quickstart ---------- -Installing DiskCache is simple with -`pip `_:: +Installing `DiskCache`_ is simple with `pip `_:: $ pip install diskcache @@ -102,27 +101,43 @@ function:: >>> import diskcache >>> help(diskcache) -// caching +The core of `DiskCache`_ is three data types intended for caching. `Cache`_ +objects manage a SQLite database and filesystem directory to store key and +value pairs. `FanoutCache`_ provides a sharding layer to utilize multiple +caches and `DjangoCache`_ integrates that with `Django`_:: >>> from diskcache import Cache, FanoutCache, DjangoCache >>> help(Cache) >>> help(FanoutCache) >>> help(DjangoCache) -// persistence +Built atop the caching data types, are `Deque`_ and `Index`_ which work as a +cross-process, persistent replacement for Python's ``collections.deque`` and +``dict``. These implement the sequence and mapping container base classes:: >>> from diskcache import Deque, Index >>> help(Deque) >>> help(Index) -// recipes +Finally, a number of `recipes`_ for cross-process synchronization are provided +using an underlying cache. Features like memoization with cache stampede +prevention, cross-process locking, and cross-process throttling are available:: >>> from diskcache import memoize_stampede, Lock, throttle >>> help(memoize_stampede) >>> help(Lock) >>> help(throttle) -// tutorial and api are required reading +Python's docstrings are a quick way to get started but not intended as a +replacement for the `DiskCache Tutorial`_ and `DiskCache API Reference`_. + +.. _`Cache`: http://www.grantjenks.com/docs/diskcache/tutorial.html#cache +.. _`FanoutCache`: http://www.grantjenks.com/docs/diskcache/tutorial.html#fanoutcache +.. _`DjangoCache`: http://www.grantjenks.com/docs/diskcache/tutorial.html#djangocache +.. _`Django`: https://www.djangoproject.com/ +.. _`Deque`: http://www.grantjenks.com/docs/diskcache/tutorial.html#deque +.. _`Index`: http://www.grantjenks.com/docs/diskcache/tutorial.html#index +.. _`recipes`: http://www.grantjenks.com/docs/diskcache/tutorial.html#recipes User Guide ---------- diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 1d70e55..eef8e31 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -527,6 +527,15 @@ cross-thread and cross-process communication. :class:`Index ` objects are also useful in scenarios where contents should remain persistent or limitations prohibit holding all items in memory at the same time. +.. _tutorial-recipes: + +Recipes +------- + +.. todo:: + + Synchronization recipes. + .. _tutorial-settings: Settings @@ -723,8 +732,8 @@ cache statistics are being recorded). .. _`Python Anywhere`: https://www.pythonanywhere.com/ .. _`Parallels`: https://www.parallels.com/ -Implementation Notes --------------------- +Implementation +-------------- :doc:`DiskCache ` is mostly built on SQLite and the filesystem. Some techniques used to improve performance: From d4a0afd23e26bded1ce557e89069b67a572557c1 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 09:59:09 -0700 Subject: [PATCH 038/211] Fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 7435706..7c953f6 100644 --- a/README.rst +++ b/README.rst @@ -112,7 +112,7 @@ caches and `DjangoCache`_ integrates that with `Django`_:: >>> help(DjangoCache) Built atop the caching data types, are `Deque`_ and `Index`_ which work as a -cross-process, persistent replacement for Python's ``collections.deque`` and +cross-process, persistent replacements for Python's ``collections.deque`` and ``dict``. These implement the sequence and mapping container base classes:: >>> from diskcache import Deque, Index From 1266d7ebe957d531e77b509b46400e9b5c762931 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 09:59:43 -0700 Subject: [PATCH 039/211] Add note about versioning --- docs/tutorial.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index eef8e31..ce33cd9 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -18,14 +18,14 @@ Pip & PyPI Installing :doc:`DiskCache ` is simple with `pip `_:: - $ pip install diskcache - -or, with `easy_install `_:: - - $ easy_install diskcache - -But `prefer pip `_ if at all -possible. + $ pip install --upgrade diskcache + +The versioning scheme uses `major.minor.micro` with `micro` intended for bug +fixes, `minor` intended for small features or improvements, and `major` +intended for significant new features and breaking changes. While it is +intended that only `major` version changes are backwards incompatible, it is +not always guaranteed. When running in production, it is recommended to pin at +least the `major` version. Get the Code ............ From 28409c65d71f03015e2bd92777860a5e887f1755 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 09:59:59 -0700 Subject: [PATCH 040/211] Refactor link to issues --- docs/tutorial.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index ce33cd9..5475246 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -51,12 +51,12 @@ or install it into your site-packages easily:: $ python setup.py install :doc:`DiskCache ` is looking for a Debian package maintainer. If you can -help, please open an issue in the `DiskCache Issue Tracker -`_. +help, please open an issue in the `DiskCache Issue Tracker`_. :doc:`DiskCache ` is looking for a CentOS/RPM package maintainer. If -you can help, please open an issue in the `DiskCache Issue Tracker -`_. +you can help, please open an issue in the `DiskCache Issue Tracker`_. + +.. _`DiskCache Issue Tracker`: https://github.com/grantjenks/python-diskcache/issues/ .. _tutorial-cache: From e572d198ee3ca72093224a3d4c894ecbca7a24dd Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:00:34 -0700 Subject: [PATCH 041/211] Improve Cache tutorial --- docs/tutorial.rst | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 5475246..f18e888 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -65,22 +65,22 @@ Cache The core of :doc:`DiskCache ` is :class:`diskcache.Cache` which represents a disk and file backed cache. As a Cache, it supports a familiar -Python Mapping interface with additional cache and performance parameters. +Python mapping interface with additional cache and performance parameters. >>> from diskcache import Cache >>> cache = Cache() -Initialization requires a directory path reference. If the directory path does -not exist, it will be created. Additional keyword parameters are discussed -below. Cache objects are thread-safe and may be shared between threads. Two -Cache objects may also reference the same directory from separate threads or -processes. In this way, they are also process-safe and support cross-process -communication. +Initialization expects a directory path reference. If the directory path does +not exist, it will be created. When not specified, a temporary directory is +automatically created. Additional keyword parameters are discussed below. Cache +objects are thread-safe and may be shared between threads. Two Cache objects +may also reference the same directory from separate threads or processes. In +this way, they are also process-safe and support cross-process communication. Cache objects open and maintain one or more file handles. But unlike files, all Cache operations are atomic and Cache objects support process-forking and may be serialized using Pickle. Each thread that accesses a cache should also call -:meth:`close ` on the cache. Cache objects can be used +:meth:`close <.Cache.close>` on the cache. Cache objects can be used in a `with` statement to safeguard calling :meth:`close `. @@ -89,8 +89,8 @@ in a `with` statement to safeguard calling :meth:`close ... pass Closed Cache objects will automatically re-open when accessed. But opening -Cache objects is relatively slow, and since all operations are atomic, you can -safely leave Cache objects open. +Cache objects is relatively slow, and since all operations are atomic, may be +safely left open. >>> cache.set('key', 'value') True @@ -167,8 +167,7 @@ decrementing a missing key will raise a :exc:`KeyError`. KeyError: 'carol' Increment and decrement operations are atomic and assume the value may be -stored in a SQLite column. Most builds that target machines with 64-bit pointer -widths will support 64-bit signed integers. +stored in a SQLite integer column. SQLite supports 64-bit signed integers. Like :meth:`delete ` and :meth:`get `, the method :meth:`pop ` can be @@ -194,7 +193,7 @@ The :meth:`pop ` operation is atomic and using :meth:`incr statistics in long-running systems. Unlike :meth:`get ` the `read` argument is not supported. -Another four methods remove items from the cache. +Another four methods remove items from the cache:: >>> cache.clear() 3 @@ -318,6 +317,11 @@ consistency. It can also fix inconsistencies and reclaim unused space. The return value is a list of warnings. >>> warnings = cache.check() + +Caches do not automatically remove the underlying directory where keys and +values are stored. The cache is intended to be persistent and so must be +deleted manually. + >>> cache.close() >>> import shutil >>> try: @@ -325,6 +329,8 @@ return value is a list of warnings. ... except OSError: # Windows wonkiness ... pass +To permanently delete the cache, recursively remove the cache's directory. + .. _tutorial-fanoutcache: FanoutCache From 47fc277de355024bafd54511f21c6a19e331dee3 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:00:46 -0700 Subject: [PATCH 042/211] Improve Timeout and retry tutorial --- docs/tutorial.rst | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index f18e888..a9744fb 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -348,17 +348,19 @@ transactions. Transactions are used for every operation that writes to the database. When the timeout expires, a :exc:`diskcache.Timeout` error is raised internally. This `timeout` parameter is also present on :class:`diskcache.Cache`. When a :exc:`Timeout ` error -occurs in :class:`Cache ` methods, the exception is raised to -the caller. In contrast, :class:`FanoutCache ` catches -timeout errors and aborts the operation. As a result, :meth:`set +occurs in :class:`Cache ` methods, the exception may be raised +to the caller. In contrast, :class:`FanoutCache ` +catches all timeout errors and aborts the operation. As a result, :meth:`set ` and :meth:`delete ` -methods may silently fail. Most methods that handle :exc:`Timeout -` exceptions also include a `retry` keyword parameter -(default ``False``) to automatically repeat attempts that timeout. The Mapping -interface operators: :meth:`cache[key] `, -:meth:`cache[key] = value `, and :meth:`del -cache[key] ` automatically retry operations -when :exc:`Timeout ` errors occur. :class:`FanoutCache +methods may silently fail. + +Most methods that handle :exc:`Timeout ` exceptions also +include a `retry` keyword parameter (default ``False``) to automatically repeat +attempts that timeout. The mapping interface operators: :meth:`cache[key] +`, :meth:`cache[key] = value +`, and :meth:`del cache[key] +` automatically retry operations when +:exc:`Timeout ` errors occur. :class:`FanoutCache ` will never raise a :exc:`Timeout ` exception. The default `timeout` is 0.010 (10 milliseconds). From ef087b93e98f5c9692df04db3a9f44e443ae24b6 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:01:09 -0700 Subject: [PATCH 043/211] Improve memoize docs --- docs/tutorial.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index a9744fb..c51430e 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -372,9 +372,9 @@ a one second timeout. Operations will attempt to abort if they take longer than one second. The remaining API of :class:`FanoutCache ` matches :class:`Cache ` as described above. -:class:`FanoutCache ` adds an additional feature: -:meth:`memoizing ` cache decorator. The -decorator wraps a callable and caches arguments and return values. +Caches have an additional feature: :meth:`memoizing +` decorator. The decorator wraps a callable and +caches arguments and return values. >>> from diskcache import FanoutCache >>> cache = FanoutCache() @@ -386,14 +386,14 @@ decorator wraps a callable and caches arguments and return values. ... return 1 ... else: ... return fibonacci(number - 1) + fibonacci(number - 2) - >>> print(sum(fibonacci(number=value) for value in range(100))) + >>> print(sum(fibonacci(value) for value in range(100))) 573147844013817084100 The arguments to memoize are like those for `functools.lru_cache `_ and -:meth:`FanoutCache.set `. Remember to call -:meth:`memoize ` when decorating a callable. If -you forget, then a TypeError will occur. +:meth:`Cache.set <.Cache.set>`. Remember to call :meth:`memoize +<.FanoutCache.memoize>` when decorating a callable. If you forget, then a +TypeError will occur:: >>> @cache.memoize ... def test(): @@ -403,7 +403,7 @@ you forget, then a TypeError will occur. TypeError: name cannot be callable Observe the lack of parenthenses after :meth:`memoize -` above. +` above. .. _`Sharding`: https://en.wikipedia.org/wiki/Shard_(database_architecture) From 40612613ff92beec9613fcd4e85dfdcf8f8b3f2f Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:01:30 -0700 Subject: [PATCH 044/211] Improve Deque and Index tutorial --- docs/tutorial.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index c51430e..63c3cb3 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -479,7 +479,7 @@ Deque `_-compatible double-ended queue. Deques are a generalization of stacks and queues with fast access and editing at both front and back sides. :class:`Deque -` objects inherit the benefits of the :class:`Cache +` objects inherit the benefits of :class:`Cache ` objects but never evict items. >>> from diskcache import Deque @@ -512,7 +512,7 @@ Index `_ and `ordered dictionary `_ -interface. :class:`Index ` objects inherit the benefits of +interface. :class:`Index ` objects inherit all the benefits of :class:`Cache ` objects but never evict items. >>> from diskcache import Index @@ -533,7 +533,9 @@ interface. :class:`Index ` objects inherit the benefits of :class:`Index ` objects provide an efficient and safe means of cross-thread and cross-process communication. :class:`Index ` objects are also useful in scenarios where contents should remain persistent or -limitations prohibit holding all items in memory at the same time. +limitations prohibit holding all items in memory at the same time. The index +uses a fixed amout of memory regardless of the size or number of items stored +inside it. .. _tutorial-recipes: From 8e44b090639563fe05638bc8963953c8febc332d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:01:39 -0700 Subject: [PATCH 045/211] Add transactions section to tutorial --- docs/tutorial.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 63c3cb3..3cac7b3 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -546,6 +546,23 @@ Recipes Synchronization recipes. + Update docs for recipes. + +.. _tutorial-transactions: + +Transactions +------------ + +.. todo:: + + Demonstrate use of transactions on Cache, Index, and Deque objects. + + Example, consistency: Averager + + Example, performance: get_many, set_many, delete_many + + Update docs for Cache.transact, Deque.transact, Index.transact + .. _tutorial-settings: Settings From cdc59c1b06ee0b34fd69b0b57e642bbe42b26643 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:03:59 -0700 Subject: [PATCH 046/211] Add note about FanoutCache and DjangoCache missing transactions --- docs/tutorial.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 3cac7b3..0640f07 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -557,6 +557,9 @@ Transactions Demonstrate use of transactions on Cache, Index, and Deque objects. + Missing from FanoutCache and DjangoCache due to sharding. Request Cache, + Index, or Deque object. + Example, consistency: Averager Example, performance: get_many, set_many, delete_many From 71d1b47c00602e50c3be46b5534257debdbb6c80 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:17:49 -0700 Subject: [PATCH 047/211] Document the "none" eviction policy in tutorial --- docs/api.rst | 2 ++ docs/tutorial.rst | 31 +++++++++++++++++++------------ 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index 7a6d91e..1c2ed63 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -48,6 +48,8 @@ Index :special-members: :exclude-members: __weakref__ +.. _constants: + Constants --------- diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 0640f07..509d63c 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -193,6 +193,8 @@ The :meth:`pop ` operation is atomic and using :meth:`incr statistics in long-running systems. Unlike :meth:`get ` the `read` argument is not supported. +.. _tutorial-culling: + Another four methods remove items from the cache:: >>> cache.clear() @@ -645,22 +647,27 @@ accessible at :data:`diskcache.DEFAULT_SETTINGS`. Eviction Policies ----------------- -:doc:`DiskCache ` supports three eviction policies each with different +:doc:`DiskCache ` supports four eviction policies each with different tradeoffs for accessing and storing items. -* `Least Recently Stored` is the default. Every cache item records the time it - was stored in the cache. This policy adds an index to that field. On access, - no update is required. Keys are evicted starting with the oldest stored - keys. As :doc:`DiskCache ` was intended for large caches (gigabytes) - this policy usually works well enough in practice. -* `Least Recently Used` is the most commonly used policy. An index is added to - the access time field stored in the cache database. On every access, the +* ``"least-recently-stored"`` is the default. Every cache item records the time + it was stored in the cache. This policy adds an index to that field. On + access, no update is required. Keys are evicted starting with the oldest + stored keys. As :doc:`DiskCache ` was intended for large caches + (gigabytes) this policy usually works well enough in practice. +* ``"least-recently-used"`` is the most commonly used policy. An index is added + to the access time field stored in the cache database. On every access, the field is updated. This makes every access into a read and write which slows accesses. -* `Least Frequently Used` works well in some cases. An index is added to the - access count field stored in the cache database. On every access, the field - is incremented. Every access therefore requires writing the database which - slows accesses. +* ``"least-frequently-used"`` works well in some cases. An index is added to + the access count field stored in the cache database. On every access, the + field is incremented. Every access therefore requires writing the database + which slows accesses. +* ``"none"`` disables cache evictions. Caches will grow in size without + bound. Cache items will still be lazily removed if they expire. The + persistent data types, :class:`.Deque` and :class:`.Index`, use the + ``"none"`` eviction policy. For :ref:`lazy culling ` use + the :ref:`cull_limit ` setting instead. All clients accessing the cache are expected to use the same eviction policy. The policy can be set during initialization using a keyword argument. From 07f6e5ff25f22a4485c5390d6e66f8a00bb1edb0 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:25:56 -0700 Subject: [PATCH 048/211] Add note about fanoutcache and shard size limit --- docs/tutorial.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 509d63c..1e43a20 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -374,6 +374,13 @@ a one second timeout. Operations will attempt to abort if they take longer than one second. The remaining API of :class:`FanoutCache ` matches :class:`Cache ` as described above. +The :class:`.FanoutCache` :ref:`size_limit ` is used as the total +size of the cache. The size limit of individual cache shards is the total size +divided by the number of shards. In the example above, the default total size +is one gigabyte and there are four shards so each cache shard has a size limit +of 256 megabytes. Items that are larger than the size limit are immediately +culled. + Caches have an additional feature: :meth:`memoizing ` decorator. The decorator wraps a callable and caches arguments and return values. From df66e3eae295133c0c785194a2045ecf22d9ec79 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:34:22 -0700 Subject: [PATCH 049/211] Remove delay fuzzer --- docs/case-study-delay-fuzzer.rst | 138 ------------------------------- 1 file changed, 138 deletions(-) delete mode 100644 docs/case-study-delay-fuzzer.rst diff --git a/docs/case-study-delay-fuzzer.rst b/docs/case-study-delay-fuzzer.rst deleted file mode 100644 index e71bdae..0000000 --- a/docs/case-study-delay-fuzzer.rst +++ /dev/null @@ -1,138 +0,0 @@ -Case Study: Delay Fuzzer -======================== - -Raymond keynote: -https://dl.dropboxusercontent.com/u/3967849/pybay2017_keynote/_build/html/index.html - -Fuzzing technique: -https://dl.dropboxusercontent.com/u/3967849/pybay2017_keynote/_build/html/threading.html#fuzzing - -Code below is simple on purpose. Not something to use in production. Ok for -testing. - -// discuss sys.settrace - - >>> def delayfuzzer(function): - ... """Insert random delays into function. - ... - ... WARNING: Not to be used in production scenarios. - ... The use of `sys.settrace` may affect other Python - ... tools like `pdb` and `coverage`. - ... - ... Decorator to insert random delays into a function to - ... encourage race conditions in multi-threaded code. - ... - ... """ - ... from functools import wraps - ... from sys import settrace - ... - ... try: - ... code = function.__code__ - ... except AttributeError: # Python 2 compatibility. - ... code = function.co_code - ... - ... def tracer(frame, event, arg): - ... "Activate sleeper in calls to function." - ... if event == 'call' and frame.f_code is code: - ... return sleeper - ... - ... @wraps(function) - ... def wrapper(*args, **kwargs): - ... """Set tracer before calling function. - ... - ... Tracing is thread-local so set the tracer before - ... every function call. - ... - ... """ - ... settrace(tracer) - ... return function(*args, **kwargs) - ... - ... return wrapper - -Sleeper function that prints location: - - >>> from time import sleep - >>> from random import expovariate - >>> def sleeper(frame, event, arg): - ... "Sleep for random period." - ... lineno = frame.f_lineno - ... print('Tracing line %s in diskcache/core.py' % lineno) - ... sleep(expovariate(100)) - -Check that it's working: - - >>> import diskcache - >>> diskcache.Cache.incr = delayfuzzer(diskcache.Cache.incr) - >>> cache = diskcache.FanoutCache('tmp') - >>> cache.incr(0) - Tracing line 797 in diskcache/core.py - Tracing line 798 in diskcache/core.py - Tracing line 800 in diskcache/core.py - Tracing line 804 in diskcache/core.py - Tracing line 805 in diskcache/core.py - Tracing line 807 in diskcache/core.py - Tracing line 808 in diskcache/core.py - Tracing line 811 in diskcache/core.py - Tracing line 812 in diskcache/core.py - Tracing line 813 in diskcache/core.py - Tracing line 814 in diskcache/core.py - Tracing line 815 in diskcache/core.py - Tracing line 815 in diskcache/core.py - 1 - >>> cache.clear() - 1 - -Simple sleeper function: - - >>> def sleeper(frame, event, arg): - ... "Sleep for random period." - ... sleep(expovariate(100)) - -Increment all numbers in a range: - - >>> def task(cache): - ... for num in range(100): - ... cache.incr(num, retry=True) - -Process worker to start many tasks in separate threads. - - >>> import threading - >>> def worker(): - ... cache = diskcache.FanoutCache('tmp') - ... threads = [] - ... - ... for num in range(8): - ... thread = threading.Thread(target=task, args=(cache,)) - ... threads.append(thread) - ... - ... for thread in threads: - ... thread.start() - ... - ... for thread in threads: - ... thread.join() - -Start many worker processes: - - >>> import multiprocessing - >>> def main(): - ... processes = [] - ... - ... for _ in range(8): - ... process = multiprocessing.Process(target=worker) - ... processes.append(process) - ... - ... for process in processes: - ... process.start() - ... - ... for process in processes: - ... process.join() - -Ok, here goes: - - >>> main() - >>> sorted(cache) == list(range(100)) - True - >>> all(cache[key] == 64 for key in cache) - True - -Yaay! It worked. From 935cc3933bed14c57ec8785ee4f87aa877439491 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Jun 2019 10:34:36 -0700 Subject: [PATCH 050/211] Add link to landing page caching --- README.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.rst b/README.rst index 7c953f6..d0b74d0 100644 --- a/README.rst +++ b/README.rst @@ -149,6 +149,7 @@ tutorial, benchmarks, API, and development. * `DiskCache Cache Benchmarks`_ * `DiskCache DjangoCache Benchmarks`_ * `Case Study: Web Crawler`_ +* `Case Study: Landing Page Caching`_ * `Talk: All Things Cached - SF Python 2017 Meetup`_ * `DiskCache API Reference`_ * `DiskCache Development`_ @@ -158,6 +159,7 @@ tutorial, benchmarks, API, and development. .. _`DiskCache DjangoCache Benchmarks`: http://www.grantjenks.com/docs/diskcache/djangocache-benchmarks.html .. _`Talk: All Things Cached - SF Python 2017 Meetup`: http://www.grantjenks.com/docs/diskcache/sf-python-2017-meetup-talk.html .. _`Case Study: Web Crawler`: http://www.grantjenks.com/docs/diskcache/case-study-web-crawler.html +.. _`Case Study: Landing Page Caching`: http://www.grantjenks.com/docs/diskcache/case-study-landing-page-caching.html .. _`DiskCache API Reference`: http://www.grantjenks.com/docs/diskcache/api.html .. _`DiskCache Development`: http://www.grantjenks.com/docs/diskcache/development.html From 8d950e5e4762c576ca381ed554a378f24670a287 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 09:42:47 -0700 Subject: [PATCH 051/211] Use reference in with-statement --- docs/tutorial.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 1e43a20..f726642 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -86,14 +86,13 @@ in a `with` statement to safeguard calling :meth:`close >>> cache.close() >>> with Cache(cache.directory) as reference: - ... pass + ... reference.set('key', 'value') + True Closed Cache objects will automatically re-open when accessed. But opening Cache objects is relatively slow, and since all operations are atomic, may be safely left open. - >>> cache.set('key', 'value') - True >>> cache.close() >>> cache.get('key') # Automatically opens, but slower. 'value' From 6ed398b4d615c46f47b63c1a0a889eb5ff14c66e Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 09:43:09 -0700 Subject: [PATCH 052/211] Add quick note on touch() api --- docs/tutorial.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index f726642..1bc6b6e 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -131,6 +131,14 @@ The return value is a tuple containing the value, expire time (seconds from epoch), and tag. Because we passed ``read=True`` the value is returned as a file-like object. +Use :meth:`touch <.Cache.touch>` to update the expiration time of an item in +the cache. + + >>> cache.touch('key', expire=None) + True + >>> cache.touch('does-not-exist', expire=1) + False + Like :meth:`set `, the method :meth:`add ` can be used to insert an item in the cache. The item is inserted only if the key is not already present. From 1506b722dd6ed6107f2b0640eeb24df22d853bae Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 09:43:24 -0700 Subject: [PATCH 053/211] Improve wording --- docs/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 1bc6b6e..8cc3035 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -280,8 +280,8 @@ remove items until the cache volume is less than the size limit. True Some users may defer all culling to a cron-like process by setting the -:ref:`cull_limit ` to zero and calling :meth:`cull -` to manually remove items. Like :meth:`evict +:ref:`cull_limit ` to zero and manually calling :meth:`cull +` to remove items. Like :meth:`evict ` and :meth:`expire `, calls to :meth:`cull ` will work regardless of the :ref:`cull_limit `. From 2e344cdd1c754ae0ac3c7210fc89a364dcef88ca Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 09:44:12 -0700 Subject: [PATCH 054/211] Add section on insertion order and sort order --- docs/tutorial.rst | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 8cc3035..c79401b 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -294,6 +294,26 @@ Some users may defer all culling to a cron-like process by setting the Each of these methods is designed to work concurrent to others. None of them block readers or writers in other threads or processes. +Caches may be iterated by either insertion order or sorted order. The default +ordering uses insertion order. To iterate by sorted order, use :meth:`iterkeys +<.Cache.iterkeys>`. The sort order is determined by the database which makes it +valid only for `str`, `bytes`, `int`, and `float` data types. Other types of +keys will be serialized which is likely to have a meaningless sorted order. + + >>> for key in 'cab': + ... cache[key] = None + >>> list(cache) + ['c', 'a', 'b'] + >>> list(cache.iterkeys()) + ['a', 'b', 'c'] + >>> cache.peekitem() + ('b', None) + >>> cache.peekitem(last=False) + ('c', None) + +If only the first or last item in insertion order is desired then +:meth:`peekitem <.Cache.peekitem>` is more efficient than using iteration. + Lastly, three methods support metadata about the cache. The first is :meth:`volume ` which returns the estimated total size in bytes of the cache directory on disk. From 55ce695ce0b80363ddce5e09bc5ce1f9c6c4658a Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 09:44:31 -0700 Subject: [PATCH 055/211] Add section on push, pull, and peek --- docs/tutorial.rst | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index c79401b..8fe4c46 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -314,6 +314,36 @@ keys will be serialized which is likely to have a meaningless sorted order. If only the first or last item in insertion order is desired then :meth:`peekitem <.Cache.peekitem>` is more efficient than using iteration. +Three additional methods use the sorted ordering of keys to maintain a +queue-like data structure within the cache. The :meth:`push <.Cache.push>`, +:meth:`pull <.Cache.pull>`, and :meth:`peek <.Cache.peek>` methods +automatically assign the key within the cache. + + >>> key = cache.push('first') + >>> print(key) + 500000000000000 + >>> cache[key] + 'first' + >>> _ = cache.push('second') + >>> _ = cache.push('zeroth', side='front') + >>> _, value = cache.peek() + >>> value + 'zeroth' + >>> key, value = cache.pull() + >>> print(key) + 499999999999999 + >>> value + 'zeroth' + +The `side` parameter supports access to either the ``'front'`` or ``'back'`` of +the cache. In addition, the `prefix` parameter can be used to maintain multiple +queue-like data structures within a single cache. When prefix is ``None``, +integer keys are used. Otherwise, string keys are used in the format +“prefix-integer”. Integer starts at 500 trillion. Like :meth:`set <.Cache.set>` +and :meth:`get <.Cache.get>`, methods :meth:`push <.Cache.push>`, :meth:`pull +<.Cache.pull>`, and :meth:`peek <.Cache.peek>` support cache metadata like the +expiration time and tag. + Lastly, three methods support metadata about the cache. The first is :meth:`volume ` which returns the estimated total size in bytes of the cache directory on disk. From 616f848dcb61f6820714606a377fdffccec5cdc5 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 09:48:27 -0700 Subject: [PATCH 056/211] Add reference to push, pull, and peek in deque --- docs/tutorial.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 8fe4c46..ce9155f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -545,8 +545,9 @@ Deque `_-compatible double-ended queue. Deques are a generalization of stacks and queues with fast access and editing at both front and back sides. :class:`Deque -` objects inherit the benefits of :class:`Cache -` objects but never evict items. +` objects use the :meth:`push <.Cache.push>`, :meth:`pull +<.Cache.pull>`, and :meth:`peek <.Cache.peek>` methods of :class:`Cache +<.Cache>` objects but never evict or expire items. >>> from diskcache import Deque >>> deque = Deque(range(5, 10)) @@ -579,7 +580,7 @@ Index and `ordered dictionary `_ interface. :class:`Index ` objects inherit all the benefits of -:class:`Cache ` objects but never evict items. +:class:`Cache ` objects but never evict or expire items. >>> from diskcache import Index >>> index = Index([('a', 1), ('b', 2), ('c', 3)]) From 4b7e395a0cdf99033c4cbd5dd36b30c9a66a8166 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 11:06:46 -0700 Subject: [PATCH 057/211] Add recipes in tutorial and api docs --- docs/api.rst | 21 +++++++++++++++++++++ docs/tutorial.rst | 22 +++++++++++++++++++++- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index 1c2ed63..99686bb 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -48,6 +48,27 @@ Index :special-members: :exclude-members: __weakref__ +Recipes +------- + +.. autoclass:: diskcache.Averager + :members: + +.. autoclass:: diskcache.Lock + :members: + +.. autoclass:: diskcache.RLock + :members: + +.. autoclass:: diskcache.BoundedSemaphore + :members: + +.. autodecorator:: diskcache.throttle + +.. autodecorator:: diskcache.barrier + +.. autodecorator:: diskcache.memoize_stampede + .. _constants: Constants diff --git a/docs/tutorial.rst b/docs/tutorial.rst index ce9155f..c17b6a6 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -630,8 +630,28 @@ Transactions Example, consistency: Averager Example, performance: get_many, set_many, delete_many +.. _tutorial-recipes: + +Recipes +------- - Update docs for Cache.transact, Deque.transact, Index.transact +:doc:`DiskCache ` includes a few synchronization recipes for +cross-thread and cross-process communication: + +* :class:`.Averager` -- maintains a running average like that shown above. +* :class:`.Lock`, :class:`.RLock`, and :class:`.BoundedSemaphore` -- recipes + for synchronization around critical sections like those found in Python's + `threading`_ and `multiprocessing`_ modules. +* :func:`throttle <.throttle>` -- function decorator to rate-limit calls to a + function. +* :func:`barrier <.barrier>` -- function decorator to synchronize calls to a + function. +* :func:`memoize_stampede <.memoize_stampede>` -- memoizing function decorator + with cache stampede protection. Read :doc:`case-study-landing-page-caching` + for a comparison of memoization strategies. + +.. _threading: https://docs.python.org/3/library/threading.html +.. _multiprocessing: https://docs.python.org/3/library/multiprocessing.html .. _tutorial-settings: From e4cd4dc0daf7ff9e56fb3d564fcc060147dc48ad Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 11:07:12 -0700 Subject: [PATCH 058/211] Add note about memory usage and deques --- docs/tutorial.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index c17b6a6..017a8df 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -569,7 +569,9 @@ access and editing at both front and back sides. :class:`Deque :class:`Deque ` objects provide an efficient and safe means of cross-thread and cross-process communication. :class:`Deque ` objects are also useful in scenarios where contents should remain persistent or -limitations prohibit holding all items in memory at the same time. +limitations prohibit holding all items in memory at the same time. The deque +uses a fixed amout of memory regardless of the size or number of items stored +inside it. Index ----- From 126b033c5715fd058a258fc3c104b7c8d0858fbb Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 11:07:44 -0700 Subject: [PATCH 059/211] Add tutorial section on Transactions --- docs/tutorial.rst | 58 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 017a8df..e028e54 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -606,32 +606,60 @@ limitations prohibit holding all items in memory at the same time. The index uses a fixed amout of memory regardless of the size or number of items stored inside it. -.. _tutorial-recipes: +.. _tutorial-transactions: -Recipes -------- +Transactions +------------ -.. todo:: +Transactions are implemented by the :class:`.Cache`, :class:`.Deque`, and +:class:`.Index` data types and support consistency and improved +performance. Use transactions to guarantee a group of operations occur +atomically. For example, to calculate a running average, the total and count +could be incremented together:: + + >>> with cache.transact(): + ... total = cache.incr('total', 123.45) + ... count = cache.incr('count') + >>> total + 123.45 + >>> count + 1 - Synchronization recipes. +And to calculate the average, the values could be retrieved together: - Update docs for recipes. + >>> with cache.transact(): + ... total = cache.get('total') + ... count = cache.get('count') + >>> average = None if count == 0 else total / count + >>> average + 123.45 -.. _tutorial-transactions: +Keep transactions as short as possible because within a transaction, no other +writes may occur to the cache. Every write operation uses a transaction and +transactions may be nested to improve performance. For example, a possible +implementation to set many items within the cache:: -Transactions ------------- + >>> def set_many(cache, mapping): + ... with cache.transact(): + ... for key, value in mapping.items(): + ... cache[key] = value -.. todo:: +By grouping all operations in a single transaction, performance may improve two +to five times. But be careful, a large mapping will block other concurrent +writers. - Demonstrate use of transactions on Cache, Index, and Deque objects. +Transactions are not implemented by :class:`.FanoutCache` and +:class:`.DjangoCache` due to key sharding. Instead, a cache shard with +transaction support may be requested. - Missing from FanoutCache and DjangoCache due to sharding. Request Cache, - Index, or Deque object. + >>> fanout_cache = FanoutCache() + >>> tutorial_cache = fanout_cache.cache('tutorial') + >>> username_queue = fanout_cache.deque('usernames') + >>> url_to_response = fanout_cache.index('responses') - Example, consistency: Averager +The cache shard exists in a subdirectory of the fanout-cache with the given +name. - Example, performance: get_many, set_many, delete_many .. _tutorial-recipes: Recipes From 5eff80e019de2158d126997655481030223a744b Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 11:08:05 -0700 Subject: [PATCH 060/211] Improve formatting for autodoc --- diskcache/recipes.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 48b8241..08b873b 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -33,15 +33,17 @@ class Averager(object): total and count. The average can then be calculated at any time. >>> import diskcache - >>> cache = diskcache.Cache() + >>> cache = diskcache.FanoutCache() >>> ave = Averager(cache, 'latency') >>> ave.add(0.080) >>> ave.add(0.120) >>> ave.get() 0.1 >>> ave.add(0.160) - >>> ave.get() + >>> ave.pop() 0.12 + >>> print(ave.get()) + None """ def __init__(self, cache, key, expire=None, tag=None): @@ -61,14 +63,14 @@ def add(self, value): ) def get(self): - "Get current average." + "Get current average or return `None` if count equals zero." total, count = self._cache.get(self._key, default=(0.0, 0), retry=True) - return 0.0 if count == 0 else total / count + return None if count == 0 else total / count def pop(self): - "Return current average and reset average to 0.0." + "Return current average and delete key." total, count = self._cache.pop(self._key, default=(0.0, 0), retry=True) - return 0.0 if count == 0 else total / count + return None if count == 0 else total / count class Lock(object): @@ -178,7 +180,7 @@ class BoundedSemaphore(object): >>> import diskcache >>> cache = diskcache.Cache() - >>> semaphore = BoundedSemaphore(cache, 'max-connections', value=2) + >>> semaphore = BoundedSemaphore(cache, 'max-cons', value=2) >>> semaphore.acquire() >>> semaphore.acquire() >>> semaphore.release() @@ -242,7 +244,7 @@ def throttle(cache, count, seconds, name=None, expire=None, tag=None, >>> start = time.time() >>> while (time.time() - start) <= 2: ... increment() - >>> count in (6, 7) # 6 or 7 calls depending on processor load + >>> count in (6, 7) # 6 or 7 calls depending on CPU load True """ @@ -329,7 +331,7 @@ def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1): probabilistically before expiration in a background thread of execution. Early probabilistic recomputation is based on research by Vattani, A.; Chierichetti, F.; Lowenstein, K. (2015), Optimal Probabilistic - Cache Stampede Prevention, VLDB, pp. 886?897, ISSN 2150-8097 + Cache Stampede Prevention, VLDB, pp. 886-897, ISSN 2150-8097 If name is set to None (default), the callable name will be determined automatically. @@ -338,27 +340,27 @@ def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1): cached separately. For example, f(3) and f(3.0) will be treated as distinct calls with distinct results. - The original underlying function is accessible through the __wrapped__ + The original underlying function is accessible through the `__wrapped__` attribute. This is useful for introspection, for bypassing the cache, or for rewrapping the function with a different cache. >>> from diskcache import Cache >>> cache = Cache() >>> @memoize_stampede(cache, expire=1) - ... def fibonacci(number): + ... def fib(number): ... if number == 0: ... return 0 ... elif number == 1: ... return 1 ... else: - ... return fibonacci(number - 1) + fibonacci(number - 2) - >>> print(fibonacci(100)) + ... return fib(number - 1) + fib(number - 2) + >>> print(fib(100)) 354224848179261915075 An additional `__cache_key__` attribute can be used to generate the cache key used for the given arguments. - >>> key = fibonacci.__cache_key__(100) + >>> key = fib.__cache_key__(100) >>> del cache[key] Remember to call memoize when decorating a callable. If you forget, then a From dfad0aa27362354901d90457e465b8b246570c3e Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 11:23:41 -0700 Subject: [PATCH 061/211] Update caveats regarding asyncio support --- docs/tutorial.rst | 37 +++++++++++++++++++++++++------------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index e028e54..8b20df9 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -865,24 +865,37 @@ floats, equality matches Python's definition. But large integers and all other types will be converted to bytes using pickling and the bytes representation will define equality. -:doc:`DiskCache ` uses SQLite to synchronize database access between -threads and processes and as such inherits all SQLite caveats. Most notably -SQLite is `not recommended`_ for use with Network File System (NFS) mounts. For -this reason, :doc:`DiskCache ` currently `performs poorly`_ on `Python -Anywhere`_. Users have also reported issues running inside of `Parallels`_ -shared folders. - -:doc:`DiskCache ` uses transactions when writing data to disk using -SQLite. When the disk or database is full, a :exc:`sqlite3.OperationalError` -will be raised from any method that attempts to write data. Read operations -will still succeed so long as they do not cause any write (as might occur if -cache statistics are being recorded). +SQLite is used to synchronize database access between threads and processes and +as such inherits all SQLite caveats. Most notably SQLite is `not recommended`_ +for use with Network File System (NFS) mounts. For this reason, :doc:`DiskCache +` currently `performs poorly`_ on `Python Anywhere`_. Users have also +reported issues running inside of `Parallels`_ shared folders. + +When the disk or database is full, a :exc:`sqlite3.OperationalError` will be +raised from any method that attempts to write data. Read operations will still +succeed so long as they do not cause any write (as might occur if cache +statistics are being recorded). + +Asynchronous support using Python's ``async`` and ``await`` keywords and +`asyncio`_ module is blocked by a lack of support in the underlying SQLite +module. But it is possible to run :doc:`DiskCache ` methods in a +thread-pool executor asynchronously. For example:: + + >>> import asyncio + >>> async def set_async(key, val): + ... loop = asyncio.get_running_loop() + ... future = loop.run_in_executor(None, cache.set, key, val) + ... result = await future + ... return result + >>> asyncio.run(set_async('test-key', 'test-value')) + True .. _`hash protocol`: https://docs.python.org/library/functions.html#hash .. _`not recommended`: https://www.sqlite.org/faq.html#q5 .. _`performs poorly`: https://www.pythonanywhere.com/forums/topic/1847/ .. _`Python Anywhere`: https://www.pythonanywhere.com/ .. _`Parallels`: https://www.parallels.com/ +.. _`asyncio`: https://docs.python.org/3/library/asyncio.html Implementation -------------- From 5682d1dbf3fa1a1c91aa95b917c541c386ebc754 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 12:05:32 -0700 Subject: [PATCH 062/211] Half-draft of landing page caching case study --- docs/case-study-landing-page-caching.rst | 42 ++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/docs/case-study-landing-page-caching.rst b/docs/case-study-landing-page-caching.rst index eb5dabe..97010aa 100644 --- a/docs/case-study-landing-page-caching.rst +++ b/docs/case-study-landing-page-caching.rst @@ -4,10 +4,52 @@ Case Study: Landing Page Caching :doc:`DiskCache ` version 4 added recipes for cache stampede mitigation. Let's look at how that applies to landing page caching. + >>> import time + >>> def generate_landing_page(): + ... time.sleep(0.2) # Work really hard. + ... # Return HTML response. + +Imagine a website under heavy load with a function used to generate the landing +page. There are two processes each with five threads for a total of ten +concurrent workers. Also assume that generating the landing page takes about +two hundred milliseconds. + .. image:: _static/no-caching.png +When we look at the number of concurrent workers and the latency with no +caching at all, the graph looks as above. Notice each worker constantly +regenerates the page with a consistently slow latency. + + >>> import diskcache as dc + >>> cache = dc.Cache() + >>> @cache.memoize(expire=1) + ... def generate_landing_page(): + ... time.sleep(0.2) + +With traditional caching, the result of generating the landing page can be +memoized for one second. After each second, the cached HTML expires and all ten +workers rush to regenerate the result. + .. image:: _static/traditional-caching.png +There is a huge improvement in average latency now but some requests experience +worse latency than before due to the added overhead of caching. The cache +stampede is visible now as the spikes in the concurrency graph. If generating +the landing page requires significant resources then the spikes may be +prohibitive. + +To reduce the number of concurrent workers, a barrier can be used to +synchronize generating the landing page:: + + >>> @cache.memoize(expire=0) + ... @dc.barrier(cache, dc.Lock) + ... @cache.memoize(expire=1) + ... def generate_landing_page(): + ... time.sleep(0.2) + +The double-checked locking uses two memoization decorators to optimistically +look up the cache result before locking. + .. image:: _static/synchronized-locking.png .. image:: _static/early-recomputation.png From 4b721b3bad706e049548bb17a435e26e93e28891 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 1 Jul 2019 17:42:39 -0700 Subject: [PATCH 063/211] Show asyncio without running through doctests --- docs/tutorial.rst | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 8b20df9..a4c3317 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -881,14 +881,15 @@ Asynchronous support using Python's ``async`` and ``await`` keywords and module. But it is possible to run :doc:`DiskCache ` methods in a thread-pool executor asynchronously. For example:: - >>> import asyncio - >>> async def set_async(key, val): - ... loop = asyncio.get_running_loop() - ... future = loop.run_in_executor(None, cache.set, key, val) - ... result = await future - ... return result - >>> asyncio.run(set_async('test-key', 'test-value')) - True + import asyncio + + async def set_async(key, val): + loop = asyncio.get_running_loop() + future = loop.run_in_executor(None, cache.set, key, val) + result = await future + return result + + asyncio.run(set_async('test-key', 'test-value')) .. _`hash protocol`: https://docs.python.org/library/functions.html#hash .. _`not recommended`: https://www.sqlite.org/faq.html#q5 From 2d1f43ea2be4c82a430d245de6260c3e18059ba1 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 2 Jul 2019 10:13:55 -0700 Subject: [PATCH 064/211] Final draft of case study for landing page caching --- diskcache/recipes.py | 8 +-- docs/case-study-landing-page-caching.rst | 81 +++++++++++++++++++++--- 2 files changed, 76 insertions(+), 13 deletions(-) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 08b873b..fb64250 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -321,10 +321,10 @@ def wrapper(*args, **kwargs): def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1): """Memoizing cache decorator with cache stampede protection. - Cache stampedes are a type of cascading failure that can occur when - parallel computing systems using memoization come under heavy load. This - behaviour is sometimes also called dog-piling, cache miss storm, cache - choking, or the thundering herd problem. + Cache stampedes are a type of system overload that can occur when parallel + computing systems using memoization come under heavy load. This behaviour + is sometimes also called dog-piling, cache miss storm, cache choking, or + the thundering herd problem. The memoization decorator implements cache stampede protection through early recomputation. Early recomputation of function results will occur diff --git a/docs/case-study-landing-page-caching.rst b/docs/case-study-landing-page-caching.rst index 97010aa..a85d6fc 100644 --- a/docs/case-study-landing-page-caching.rst +++ b/docs/case-study-landing-page-caching.rst @@ -2,7 +2,11 @@ Case Study: Landing Page Caching ================================ :doc:`DiskCache ` version 4 added recipes for cache stampede mitigation. -Let's look at how that applies to landing page caching. +Cache stampedes are a type of system overload that can occur when parallel +computing systems using memoization come under heavy load. This behaviour is +sometimes also called dog-piling, cache miss storm, cache choking, or the +thundering herd problem. Let's look at how that applies to landing page +caching. >>> import time >>> def generate_landing_page(): @@ -10,9 +14,9 @@ Let's look at how that applies to landing page caching. ... # Return HTML response. Imagine a website under heavy load with a function used to generate the landing -page. There are two processes each with five threads for a total of ten -concurrent workers. Also assume that generating the landing page takes about -two hundred milliseconds. +page. There are five processes each with two threads for a total of ten +concurrent workers. The landing page is loaded constantly and takes about two +hundred milliseconds to generate. .. image:: _static/no-caching.png @@ -26,15 +30,15 @@ regenerates the page with a consistently slow latency. ... def generate_landing_page(): ... time.sleep(0.2) -With traditional caching, the result of generating the landing page can be -memoized for one second. After each second, the cached HTML expires and all ten -workers rush to regenerate the result. +Assume the result of generating the landing page can be memoized for one +second. Memoization supports a traditional caching strategy. After each second, +the cached HTML expires and all ten workers rush to regenerate the result. .. image:: _static/traditional-caching.png There is a huge improvement in average latency now but some requests experience worse latency than before due to the added overhead of caching. The cache -stampede is visible now as the spikes in the concurrency graph. If generating +stampede is visible too as the spikes in the concurrency graph. If generating the landing page requires significant resources then the spikes may be prohibitive. @@ -48,12 +52,71 @@ synchronize generating the landing page:: ... time.sleep(0.2) The double-checked locking uses two memoization decorators to optimistically -look up the cache result before locking. +look up the cached result before locking. With `expire` set to zero, the +cache's get-operation is performed but the set-operation is skipped. Only the +inner-nested memoize decorator will update the cache. .. image:: _static/synchronized-locking.png +The number of concurrent workers is now greatly improved. Rather than having +ten workers all attempt to generate the same result, a single worker generates +the result and the other ten benefit. The maximum latency has increased however +as three layers of caching and locking wrap the function. + +Ideally, the system would anticipate the pending expiration of the cached item +and would recompute the result in a separate thread of execution. Coordinating +recomputation would be a function of the number of workers, the expiration +time, and the duration of computation. Fortunately, Vattani, et al. published +the solution in "Optimal Probabilistic Cache Stampede Prevention" in 2015. + + >>> @dc.memoize_stampede(cache, expire=1) + ... def generate_landing_page(): + ... time.sleep(0.2) + +Early probabilistic recomputation uses a random number generator to simulate a +cache miss prior to expiration. The new result is then computed in a separate +thread while the cached result is returned to the caller. When the cache item +is missing, the result is computed and cached synchronously. + .. image:: _static/early-recomputation.png +The latency is now its theoretical best. An initial warmup execution takes two +hundred milliseconds and the remaining calls all return immediately from the +cache. Behind the scenes, separate threads of execution are recomputing the +result of workers and updating the cache. The concurrency graph shows a nearly +constant stream of workers recomputing the function's result. + + >>> @dc.memoize_stampede(cache, expire=1, beta=0.5) + ... def generate_landing_page(): + ... time.sleep(0.2) + +Vattani described an additional parameter, :math:`\beta`, which could be used +to tune the eagerness of recomputation. As the number and frequency of +concurrent worker calls increases, eagerness can be lessened by decreasing the +:math:`\beta` parameter. The default value of :math:`\beta` is one, and above +it is set to half. + .. image:: _static/early-recomputation-05.png +Latency is now still its theoretical best while the worker load has decreased +significantly. The likelihood of simulated cache misses is now half what it was +before. The value was determined through experimentation. + + >>> @dc.memoize_stampede(cache, expire=1, beta=0.3) + ... def generate_landing_page(): + ... time.sleep(0.2) + +Lets see what happens when :math:`\beta` is set too low. + .. image:: _static/early-recomputation-03.png + +When set too low, the cache item expires before a new value is recomputed. The +real cache miss then causes the workers to synchronously recompute the landing +page and cache the result. With no barrier in place, eleven workers cause a +cache stampede. The eleven workers are composed of ten synchronous workers and +one in a background thread. The best way to customize :math:`\beta` is through +experimentation, otherwise the default is very reasonable. + +:doc:`DiskCache ` provides data types and recipes for memoization and +mitigation of cache stampedes. The decorators provided are composable for a +variety of scenarios. The best way to get started is with the :doc:`tutorial`. From 1149e822c4a9b8bb8eaf27874c228100ddc380b1 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 2 Jul 2019 10:39:00 -0700 Subject: [PATCH 065/211] Change doctest-like snippets to code-blocks with emphasis --- docs/case-study-landing-page-caching.rst | 68 +++++++++++++++--------- 1 file changed, 44 insertions(+), 24 deletions(-) diff --git a/docs/case-study-landing-page-caching.rst b/docs/case-study-landing-page-caching.rst index a85d6fc..14b2de0 100644 --- a/docs/case-study-landing-page-caching.rst +++ b/docs/case-study-landing-page-caching.rst @@ -8,10 +8,13 @@ sometimes also called dog-piling, cache miss storm, cache choking, or the thundering herd problem. Let's look at how that applies to landing page caching. - >>> import time - >>> def generate_landing_page(): - ... time.sleep(0.2) # Work really hard. - ... # Return HTML response. +.. code-block:: python + + import time + + def generate_landing_page(): + time.sleep(0.2) # Work really hard. + # Return HTML response. Imagine a website under heavy load with a function used to generate the landing page. There are five processes each with two threads for a total of ten @@ -24,11 +27,16 @@ When we look at the number of concurrent workers and the latency with no caching at all, the graph looks as above. Notice each worker constantly regenerates the page with a consistently slow latency. - >>> import diskcache as dc - >>> cache = dc.Cache() - >>> @cache.memoize(expire=1) - ... def generate_landing_page(): - ... time.sleep(0.2) +.. code-block:: python + :emphasize-lines: 5 + + import diskcache as dc + + cache = dc.Cache() + + @cache.memoize(expire=1) + def generate_landing_page(): + time.sleep(0.2) Assume the result of generating the landing page can be memoized for one second. Memoization supports a traditional caching strategy. After each second, @@ -43,13 +51,16 @@ the landing page requires significant resources then the spikes may be prohibitive. To reduce the number of concurrent workers, a barrier can be used to -synchronize generating the landing page:: +synchronize generating the landing page. - >>> @cache.memoize(expire=0) - ... @dc.barrier(cache, dc.Lock) - ... @cache.memoize(expire=1) - ... def generate_landing_page(): - ... time.sleep(0.2) +.. code-block:: python + :emphasize-lines: 1,2,3 + + @cache.memoize(expire=0) + @dc.barrier(cache, dc.Lock) + @cache.memoize(expire=1) + def generate_landing_page(): + time.sleep(0.2) The double-checked locking uses two memoization decorators to optimistically look up the cached result before locking. With `expire` set to zero, the @@ -69,9 +80,12 @@ recomputation would be a function of the number of workers, the expiration time, and the duration of computation. Fortunately, Vattani, et al. published the solution in "Optimal Probabilistic Cache Stampede Prevention" in 2015. - >>> @dc.memoize_stampede(cache, expire=1) - ... def generate_landing_page(): - ... time.sleep(0.2) +.. code-block:: python + :emphasize-lines: 1 + + @dc.memoize_stampede(cache, expire=1) + def generate_landing_page(): + time.sleep(0.2) Early probabilistic recomputation uses a random number generator to simulate a cache miss prior to expiration. The new result is then computed in a separate @@ -86,9 +100,12 @@ cache. Behind the scenes, separate threads of execution are recomputing the result of workers and updating the cache. The concurrency graph shows a nearly constant stream of workers recomputing the function's result. - >>> @dc.memoize_stampede(cache, expire=1, beta=0.5) - ... def generate_landing_page(): - ... time.sleep(0.2) +.. code-block:: python + :emphasize-lines: 1 + + @dc.memoize_stampede(cache, expire=1, beta=0.5) + def generate_landing_page(): + time.sleep(0.2) Vattani described an additional parameter, :math:`\beta`, which could be used to tune the eagerness of recomputation. As the number and frequency of @@ -102,9 +119,12 @@ Latency is now still its theoretical best while the worker load has decreased significantly. The likelihood of simulated cache misses is now half what it was before. The value was determined through experimentation. - >>> @dc.memoize_stampede(cache, expire=1, beta=0.3) - ... def generate_landing_page(): - ... time.sleep(0.2) +.. code-block:: python + :emphasize-lines: 1 + + @dc.memoize_stampede(cache, expire=1, beta=0.3) + def generate_landing_page(): + time.sleep(0.2) Lets see what happens when :math:`\beta` is set too low. From c7f826a22f27e7e70ac431b37d5689e7da705e60 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 2 Jul 2019 11:22:18 -0700 Subject: [PATCH 066/211] Invoke scripts with "python -m" and use "tox -e py" --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2c15404..c67140c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ sudo: false language: python -install: pip install tox -script: tox +install: python -m pip install tox +script: python -m tox -e py matrix: include: - python: 2.7 From 845ae3ed8e28c51eee3403adc2c927a72d37daa6 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 2 Jul 2019 11:24:35 -0700 Subject: [PATCH 067/211] Pin pytest to 4.6.* for AppVeyor support on Python 2.7 and 3.4 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 0566db9..e17bdf4 100644 --- a/tox.ini +++ b/tox.ini @@ -6,7 +6,7 @@ skip_missing_interpreters=True deps= django==1.11.* mock - pytest + pytest==4.6.* pytest-django pytest-xdist commands=python -m pytest From 2c79bb981ea812f775331f758b07ce30fb0f864c Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 2 Jul 2019 16:55:00 -0700 Subject: [PATCH 068/211] Bump version to 4.0.0 --- diskcache/__init__.py | 4 ++-- tox.ini | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 96a5732..46536bb 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -43,8 +43,8 @@ pass __title__ = 'diskcache' -__version__ = '3.1.1' -__build__ = 0x030101 +__version__ = '4.0.0' +__build__ = 0x040000 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2018 Grant Jenks' diff --git a/tox.ini b/tox.ini index e17bdf4..43839bf 100644 --- a/tox.ini +++ b/tox.ini @@ -33,3 +33,6 @@ deps= django==1.11.* pylint commands=pylint diskcache + +[doc8] +ignore=D000 From ab8713e271ec8e7fdef9513c31a3a38544ecb5dd Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 27 Sep 2019 22:33:28 -0700 Subject: [PATCH 069/211] Add deepsource setup file --- .deepsource.toml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..154e132 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,14 @@ +version = 1 + +test_patterns = [ + '*/tests/*' +] + +exclude_patterns = [ + +] + +[[analyzers]] +name = 'python' +enabled = true +runtime_version = '3.x.x' From ee47430ce388e4373caa2033307c8b2f321c4638 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 2 Jul 2019 21:20:45 -0700 Subject: [PATCH 070/211] Add rstcheck to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 00f826c..659180d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ pytest-cov pytest-django pytest-env pytest-xdist +rstcheck sphinx tox twine From 798c3ea4052a78f02bdaa6c52dc73646cd155746 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 28 Sep 2019 11:39:23 -0700 Subject: [PATCH 071/211] Remove deepsource setup --- .deepsource.toml | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 .deepsource.toml diff --git a/.deepsource.toml b/.deepsource.toml deleted file mode 100644 index 154e132..0000000 --- a/.deepsource.toml +++ /dev/null @@ -1,14 +0,0 @@ -version = 1 - -test_patterns = [ - '*/tests/*' -] - -exclude_patterns = [ - -] - -[[analyzers]] -name = 'python' -enabled = true -runtime_version = '3.x.x' From 8ba1d34972835a143bedda5927b4cdca338c2ebf Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 28 Sep 2019 22:03:14 -0700 Subject: [PATCH 072/211] Proselint fixes --- README.rst | 10 +++++----- docs/cache-benchmarks.rst | 6 +++--- docs/case-study-landing-page-caching.rst | 4 ++-- docs/case-study-web-crawler.rst | 2 +- docs/djangocache-benchmarks.rst | 6 +++--- docs/sf-python-2017-meetup-talk.rst | 4 ++-- docs/tutorial.rst | 14 +++++++------- 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index d0b74d0..57b7a2e 100644 --- a/README.rst +++ b/README.rst @@ -234,7 +234,7 @@ Integrations? Django None None None None **Timings** -These are very rough measurements. See `DiskCache Cache Benchmarks`_ for more +These are rough measurements. See `DiskCache Cache Benchmarks`_ for more rigorous data. ================ ============= ========= ========= ============ ============ @@ -279,8 +279,8 @@ Pure-Python Databases ..................... * `ZODB`_ supports an isomorphic interface for database operations which means - there's very little impact on your code to make objects persistent and - there's no database mapper that partially hides the datbase. + there's little impact on your code to make objects persistent and there's no + database mapper that partially hides the datbase. * `CodernityDB`_ is an open source, pure-Python, multi-platform, schema-less, NoSQL database and includes an HTTP server version, and a Python client library that aims to be 100% compatible with the embedded version. @@ -332,7 +332,7 @@ SQL Databases PostgreSQL adapter for the Python programming language. * `Oracle DB`_ is a relational database management system (RDBMS) from the Oracle Corporation. Originally developed in 1977, Oracle DB is one of the - most trusted and widely-used enterprise relational database engines. + most trusted and widely used enterprise relational database engines. * `Microsoft SQL Server`_ is a relational database management system developed by Microsoft. As a database server, it stores and retrieves data as requested by other software applications. @@ -398,7 +398,7 @@ License at Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the +CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. .. _`DiskCache`: http://www.grantjenks.com/docs/diskcache/ diff --git a/docs/cache-benchmarks.rst b/docs/cache-benchmarks.rst index 3f0343d..5b125e8 100644 --- a/docs/cache-benchmarks.rst +++ b/docs/cache-benchmarks.rst @@ -116,7 +116,7 @@ Timings for pylibmc.Client Total 98999 2.669s ========= ========= ========= ========= ========= ========= ========= ========= -Memcached performance is low latency and very stable. +Memcached performance is low latency and stable. ========= ========= ========= ========= ========= ========= ========= ========= Timings for redis.StrictRedis @@ -144,8 +144,8 @@ Get .. image:: _static/core-p8-get.png -Under heavy load, :doc:`DiskCache ` gets are very low latency. At the -90th percentile, they are less than half the latency of Memcached. +Under heavy load, :doc:`DiskCache ` gets are low latency. At the 90th +percentile, they are less than half the latency of Memcached. Set ... diff --git a/docs/case-study-landing-page-caching.rst b/docs/case-study-landing-page-caching.rst index 14b2de0..677186e 100644 --- a/docs/case-study-landing-page-caching.rst +++ b/docs/case-study-landing-page-caching.rst @@ -75,7 +75,7 @@ the result and the other ten benefit. The maximum latency has increased however as three layers of caching and locking wrap the function. Ideally, the system would anticipate the pending expiration of the cached item -and would recompute the result in a separate thread of execution. Coordinating +and would recompute the result in a separate thread of execution. Coordinating recomputation would be a function of the number of workers, the expiration time, and the duration of computation. Fortunately, Vattani, et al. published the solution in "Optimal Probabilistic Cache Stampede Prevention" in 2015. @@ -135,7 +135,7 @@ real cache miss then causes the workers to synchronously recompute the landing page and cache the result. With no barrier in place, eleven workers cause a cache stampede. The eleven workers are composed of ten synchronous workers and one in a background thread. The best way to customize :math:`\beta` is through -experimentation, otherwise the default is very reasonable. +experimentation, otherwise the default is reasonable. :doc:`DiskCache ` provides data types and recipes for memoization and mitigation of cache stampedes. The decorators provided are composable for a diff --git a/docs/case-study-web-crawler.rst b/docs/case-study-web-crawler.rst index 982a3c8..c37e2a5 100644 --- a/docs/case-study-web-crawler.rst +++ b/docs/case-study-web-crawler.rst @@ -116,7 +116,7 @@ the crawl function and query it. >>> len(results) 99 -As an added benefit, our code also now works in parallel. For free! +As an added benefit, our code also now works in parallel. >>> results.clear() >>> from multiprocessing import Process diff --git a/docs/djangocache-benchmarks.rst b/docs/djangocache-benchmarks.rst index 4f791ff..88d1fec 100644 --- a/docs/djangocache-benchmarks.rst +++ b/docs/djangocache-benchmarks.rst @@ -102,8 +102,8 @@ Get .. image:: _static/djangocache-get.png -Under heavy load, :class:`DjangoCache ` gets are very -low latency. At the 99th percentile they are on par with the Memcached cache +Under heavy load, :class:`DjangoCache ` gets are low +latency. At the 99th percentile they are on par with the Memcached cache backend. Set @@ -157,7 +157,7 @@ Timings for memcached Total 791992 68.825s ========= ========= ========= ========= ========= ========= ========= ========= -Memcached performance is low latency and very stable. +Memcached performance is low latency and stable. ========= ========= ========= ========= ========= ========= ========= ========= Timings for redis diff --git a/docs/sf-python-2017-meetup-talk.rst b/docs/sf-python-2017-meetup-talk.rst index 214b66b..5f79000 100644 --- a/docs/sf-python-2017-meetup-talk.rst +++ b/docs/sf-python-2017-meetup-talk.rst @@ -20,7 +20,7 @@ Landscape Backends -------- -* Backends have very different designs and tradeoffs. +* Backends have different designs and tradeoffs. Frameworks @@ -165,7 +165,7 @@ SQLite * Use a context manager for isolation level management. * Pragmas tune the behavior and performance of SQLite. - * Default is very robust and slow. + * Default is robust and slow. * Use write-ahead-log so writers don't block readers. * Memory-map pages for fast lookups. diff --git a/docs/tutorial.rst b/docs/tutorial.rst index a4c3317..f704cb6 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -53,8 +53,8 @@ or install it into your site-packages easily:: :doc:`DiskCache ` is looking for a Debian package maintainer. If you can help, please open an issue in the `DiskCache Issue Tracker`_. -:doc:`DiskCache ` is looking for a CentOS/RPM package maintainer. If -you can help, please open an issue in the `DiskCache Issue Tracker`_. +:doc:`DiskCache ` is looking for a CentOS/RPM package maintainer. If you +can help, please open an issue in the `DiskCache Issue Tracker`_. .. _`DiskCache Issue Tracker`: https://github.com/grantjenks/python-diskcache/issues/ @@ -778,11 +778,11 @@ tradeoffs for accessing and storing items. the access count field stored in the cache database. On every access, the field is incremented. Every access therefore requires writing the database which slows accesses. -* ``"none"`` disables cache evictions. Caches will grow in size without - bound. Cache items will still be lazily removed if they expire. The - persistent data types, :class:`.Deque` and :class:`.Index`, use the - ``"none"`` eviction policy. For :ref:`lazy culling ` use - the :ref:`cull_limit ` setting instead. +* ``"none"`` disables cache evictions. Caches will grow without bound. Cache + items will still be lazily removed if they expire. The persistent data types, + :class:`.Deque` and :class:`.Index`, use the ``"none"`` eviction policy. For + :ref:`lazy culling ` use the :ref:`cull_limit ` + setting instead. All clients accessing the cache are expected to use the same eviction policy. The policy can be set during initialization using a keyword argument. From 912f5b6c310ae62c6bd0bf12b196b2a11fd57e63 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 2 Jul 2019 21:38:37 -0700 Subject: [PATCH 073/211] Move zero-expiration logic into memoize (rather than Cache.set) --- diskcache/core.py | 13 ++++++------- diskcache/djangocache.py | 11 ++++++++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index c74241e..69b1952 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -737,9 +737,6 @@ def set(self, key, value, expire=None, read=False, tag=None, retry=False): When `read` is `True`, `value` should be a file-like object opened for reading in binary mode. - If `expire` is less than or equal to zero then immediately returns - `False`. - Raises :exc:`Timeout` error when database timeout occurs and `retry` is `False` (default). @@ -754,9 +751,6 @@ def set(self, key, value, expire=None, read=False, tag=None, retry=False): :raises Timeout: if database timeout occurs """ - if expire is not None and expire <= 0: - return False - now = time.time() db_key, raw = self._disk.put(key) expire_time = None if expire is None else now + expire @@ -1779,6 +1773,10 @@ def memoize(self, name=None, typed=False, expire=None, tag=None): If name is set to None (default), the callable name will be determined automatically. + When expire is set to zero, function results will not be set in the + cache. Cache lookups still occur, however. Read + :doc:`case-study-landing-page-caching` for example usage. + If typed is set to True, function arguments of different types will be cached separately. For example, f(3) and f(3.0) will be treated as distinct calls with distinct results. @@ -1843,7 +1841,8 @@ def wrapper(*args, **kwargs): if result is ENOVAL: result = func(*args, **kwargs) - self.set(key, result, expire=expire, tag=tag, retry=True) + if expire is None or expire > 0: + self.set(key, result, expire, tag=tag, retry=True) return result diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index a2d863c..00a96bf 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -368,6 +368,10 @@ def memoize(self, name=None, timeout=DEFAULT_TIMEOUT, version=None, If name is set to None (default), the callable name will be determined automatically. + When timeout is set to zero, function results will not be set in the + cache. Cache lookups still occur, however. Read + :doc:`case-study-landing-page-caching` for example usage. + If typed is set to True, function arguments of different types will be cached separately. For example, f(3) and f(3.0) will be treated as distinct calls with distinct results. @@ -407,9 +411,10 @@ def wrapper(*args, **kwargs): if result is ENOVAL: result = func(*args, **kwargs) - self.set( - key, result, timeout, version, tag=tag, retry=True, - ) + if timeout is None or timeout > 0: + self.set( + key, result, timeout, version, tag=tag, retry=True, + ) return result From 9cfe5df3fa0a3d98b522bb3426aa9934bb481b2c Mon Sep 17 00:00:00 2001 From: Zoran Simic Date: Sun, 13 Oct 2019 15:42:48 -0700 Subject: [PATCH 074/211] Provide JSONDisk with diskcache (#124) --- .gitignore | 1 + diskcache/__init__.py | 3 ++- diskcache/core.py | 29 +++++++++++++++++++++++++++++ docs/tutorial.rst | 3 ++- tests/test_core.py | 31 +------------------------------ 5 files changed, 35 insertions(+), 32 deletions(-) diff --git a/.gitignore b/.gitignore index 0ef1c27..c496721 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ # virutalenv directories /env*/ +/.venv*/ # test files/directories /.cache/ diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 46536bb..b392164 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -6,7 +6,7 @@ """ -from .core import Cache, Disk, EmptyDirWarning, UnknownFileWarning, Timeout +from .core import Cache, Disk, EmptyDirWarning, JSONDisk, UnknownFileWarning, Timeout from .core import DEFAULT_SETTINGS, ENOVAL, EVICTION_POLICY, UNKNOWN from .fanout import FanoutCache from .persistent import Deque, Index @@ -25,6 +25,7 @@ 'EmptyDirWarning', 'FanoutCache', 'Index', + "JSONDisk", 'Lock', 'RLock', 'Timeout', diff --git a/diskcache/core.py b/diskcache/core.py index 69b1952..5454924 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -7,6 +7,7 @@ import errno import functools as ft import io +import json import os import os.path as op import pickletools @@ -361,6 +362,34 @@ def remove(self, filename): raise +class JSONDisk(Disk): + """Cache key and value (de)serialized as JSON.""" + def __init__(self, directory, compress_level=1, **kwargs): + self.compress_level = compress_level + super(JSONDisk, self).__init__(directory, **kwargs) + + def put(self, key): + json_bytes = json.dumps(key).encode('utf-8') + data = zlib.compress(json_bytes, self.compress_level) + return super(JSONDisk, self).put(data) + + def get(self, key, raw): + data = super(JSONDisk, self).get(key, raw) + return json.loads(zlib.decompress(data).decode('utf-8')) + + def store(self, value, read, key=UNKNOWN): + if not read: + json_bytes = json.dumps(value).encode('utf-8') + value = zlib.compress(json_bytes, self.compress_level) + return super(JSONDisk, self).store(value, read, key=key) + + def fetch(self, mode, filename, value, read): + data = super(JSONDisk, self).fetch(mode, filename, value, read) + if not read: + data = json.loads(zlib.decompress(data).decode('utf-8')) + return data + + class Timeout(Exception): "Database timeout expired." diff --git a/docs/tutorial.rst b/docs/tutorial.rst index f704cb6..7b65f99 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -813,7 +813,8 @@ database while values are sometimes stored separately in files. To customize serialization, you may pass in a :class:`Disk ` subclass to initialize the cache. All clients accessing the cache are expected to use the same serialization. The default implementation uses Pickle and the -example below uses compressed JSON. +example below uses compressed JSON, +available for convenience as :class:`Disk `. .. code-block:: python diff --git a/tests/test_core.py b/tests/test_core.py index acf6b3f..7e79995 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -7,7 +7,6 @@ import functools as ft import hashlib import io -import json import mock import os import os.path as op @@ -22,7 +21,6 @@ import time import unittest import warnings -import zlib try: import cPickle as pickle @@ -94,35 +92,8 @@ def test_disk_valueerror(): pass -class JSONDisk(diskcache.Disk): - def __init__(self, directory, compress_level=1, **kwargs): - self.compress_level = compress_level - super(JSONDisk, self).__init__(directory, **kwargs) - - def put(self, key): - json_bytes = json.dumps(key).encode('utf-8') - data = zlib.compress(json_bytes, self.compress_level) - return super(JSONDisk, self).put(data) - - def get(self, key, raw): - data = super(JSONDisk, self).get(key, raw) - return json.loads(zlib.decompress(data).decode('utf-8')) - - def store(self, value, read, key=dc.UNKNOWN): - if not read: - json_bytes = json.dumps(value).encode('utf-8') - value = zlib.compress(json_bytes, self.compress_level) - return super(JSONDisk, self).store(value, read, key=key) - - def fetch(self, mode, filename, value, read): - data = super(JSONDisk, self).fetch(mode, filename, value, read) - if not read: - data = json.loads(zlib.decompress(data).decode('utf-8')) - return data - - def test_custom_disk(): - with dc.Cache(disk=JSONDisk, disk_compress_level=6) as cache: + with dc.Cache(disk=dc.JSONDisk, disk_compress_level=6) as cache: values = [None, True, 0, 1.23, {}, [None] * 10000] for value in values: From 19f908bd5d6d90c4f69710b27a1cbad22eb93e31 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 13 Oct 2019 16:07:48 -0700 Subject: [PATCH 075/211] Add docs for JSONDisk. --- diskcache/__init__.py | 2 +- diskcache/core.py | 18 +++++++++++++++++- docs/api.rst | 10 ++++++++++ docs/tutorial.rst | 4 ++-- 4 files changed, 30 insertions(+), 4 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index b392164..2f11ea4 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -25,7 +25,7 @@ 'EmptyDirWarning', 'FanoutCache', 'Index', - "JSONDisk", + 'JSONDisk', 'Lock', 'RLock', 'Timeout', diff --git a/diskcache/core.py b/diskcache/core.py index 5454924..0c8fd2c 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -363,26 +363,42 @@ def remove(self, filename): class JSONDisk(Disk): - """Cache key and value (de)serialized as JSON.""" + "Cache key and value using JSON serialization with zlib compression." def __init__(self, directory, compress_level=1, **kwargs): + """Initialize JSON disk instance. + + Keys and values are compressed using the zlib library. The + `compress_level` is an integer from 0 to 9 controlling the level of + compression; 1 is fastest and produces the least compression, 9 is + slowest and produces the most compression, and 0 is no compression. + + :param str directory: directory path + :param int compress_level: zlib compression level (default 1) + :param kwargs: super class arguments + + """ self.compress_level = compress_level super(JSONDisk, self).__init__(directory, **kwargs) + def put(self, key): json_bytes = json.dumps(key).encode('utf-8') data = zlib.compress(json_bytes, self.compress_level) return super(JSONDisk, self).put(data) + def get(self, key, raw): data = super(JSONDisk, self).get(key, raw) return json.loads(zlib.decompress(data).decode('utf-8')) + def store(self, value, read, key=UNKNOWN): if not read: json_bytes = json.dumps(value).encode('utf-8') value = zlib.compress(json_bytes, self.compress_level) return super(JSONDisk, self).store(value, read, key=key) + def fetch(self, mode, filename, value, read): data = super(JSONDisk, self).fetch(mode, filename, value, read) if not read: diff --git a/docs/api.rst b/docs/api.rst index 99686bb..e38f254 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -116,6 +116,16 @@ Read the :ref:`Disk tutorial ` for details. :special-members: :exclude-members: __weakref__ +JSONDisk +-------- + +Read the :ref:`Disk tutorial ` for details. + +.. autoclass:: diskcache.JSONDisk + :members: + :special-members: + :exclude-members: __weakref__ + Timeout ------- diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 7b65f99..b29322c 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -813,8 +813,8 @@ database while values are sometimes stored separately in files. To customize serialization, you may pass in a :class:`Disk ` subclass to initialize the cache. All clients accessing the cache are expected to use the same serialization. The default implementation uses Pickle and the -example below uses compressed JSON, -available for convenience as :class:`Disk `. +example below uses compressed JSON, available for convenience as +:class:`JSONDisk `. .. code-block:: python From 9bee7b9ff43f2caf6f84fc6bdf1b6d7a695c0fe8 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 13 Oct 2019 16:09:20 -0700 Subject: [PATCH 076/211] Add check for DEFAULT_TIMEOUT in DjangoCache.memoize --- diskcache/djangocache.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index 00a96bf..997b852 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -411,7 +411,12 @@ def wrapper(*args, **kwargs): if result is ENOVAL: result = func(*args, **kwargs) - if timeout is None or timeout > 0: + valid_timeout = ( + timeout is None + or timeout == DEFAULT_TIMEOUT + or timeout > 0 + ) + if valid_timeout: self.set( key, result, timeout, version, tag=tag, retry=True, ) From 0e273fb54f9bfec6a33e6be7cdf9a68f0ea2adaa Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 13 Oct 2019 16:59:15 -0700 Subject: [PATCH 077/211] Bump version to 4.1.0 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 2f11ea4..192524e 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -44,8 +44,8 @@ pass __title__ = 'diskcache' -__version__ = '4.0.0' -__build__ = 0x040000 +__version__ = '4.1.0' +__build__ = 0x040100 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2018 Grant Jenks' From b0451e084ea403c29980f683b8f0d8c9ac2a2dea Mon Sep 17 00:00:00 2001 From: Mengyang Li Date: Wed, 20 Nov 2019 14:11:10 -0800 Subject: [PATCH 078/211] peekleft: fix docstr typo (#132) --- diskcache/persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 961f773..9de5835 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -442,7 +442,7 @@ def peek(self): def peekleft(self): - """Peek at value at back of deque. + """Peek at value at front of deque. Faster than indexing deque at 0. From 727037065e64a698147171f9553a6ee08be02950 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 27 Jun 2020 11:19:13 -0700 Subject: [PATCH 079/211] Bump Django to 2.2.* --- requirements.txt | 2 +- tox.ini | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 659180d..ae05547 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ coverage -django==1.11.* +django==2.2.* django_redis doc8 gj diff --git a/tox.ini b/tox.ini index 43839bf..5813859 100644 --- a/tox.ini +++ b/tox.ini @@ -4,9 +4,8 @@ skip_missing_interpreters=True [testenv] deps= - django==1.11.* - mock - pytest==4.6.* + django==2.2.* + pytest pytest-django pytest-xdist commands=python -m pytest @@ -28,9 +27,9 @@ env = DJANGO_SETTINGS_MODULE=tests.settings PYTHONPATH={PWD}:{PWD}/tests -[testenv:lint] +[testenv:pylint] deps= - django==1.11.* + django==2.2.* pylint commands=pylint diskcache From eb8033c61b59f23772801c8bfa22e269e70cb115 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 27 Jun 2020 11:21:18 -0700 Subject: [PATCH 080/211] Drop Python 2.7 and 3.4 testing and add 3.8 --- .travis.yml | 7 +++---- appveyor.yml | 6 ++---- tox.ini | 2 +- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index c67140c..6c1bd8e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,10 +4,6 @@ install: python -m pip install tox script: python -m tox -e py matrix: include: - - python: 2.7 - env: TOXENV=py27 - - python: 3.4 - env: TOXENV=py34 - python: 3.5 env: TOXENV=py35 - python: 3.6 @@ -15,6 +11,9 @@ matrix: - python: 3.7 dist: xenial env: TOXENV=py37 + - python: 3.8 + dist: xenial + env: TOXENV=py38 - python: pypy env: TOXENV=pypy - python: 3.7 diff --git a/appveyor.yml b/appveyor.yml index dc376f1..1f52a08 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -2,16 +2,14 @@ environment: matrix: - - PYTHON: "C:\\Python27" - - PYTHON: "C:\\Python34" - PYTHON: "C:\\Python35" - PYTHON: "C:\\Python36" - PYTHON: "C:\\Python37" - - PYTHON: "C:\\Python27-x64" - - PYTHON: "C:\\Python34-x64" + - PYTHON: "C:\\Python38" - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python36-x64" - PYTHON: "C:\\Python37-x64" + - PYTHON: "C:\\Python38-x64" install: diff --git a/tox.ini b/tox.ini index 5813859..ea3996d 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py27,py34,py35,py36,py37,pypy,lint +envlist=py35,py36,py37,py38,pypy,pylint skip_missing_interpreters=True [testenv] From 8130ebca2c72a90b9d25feeecfeed8efa547025f Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 27 Jun 2020 11:31:15 -0700 Subject: [PATCH 081/211] Update DjangoCache tests for Django 2.2 --- tests/test_djangocache.py | 222 +++++++++++++++++--------------------- 1 file changed, 98 insertions(+), 124 deletions(-) diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index f70b87c..100f998 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -1,15 +1,12 @@ -# -*- coding: utf-8 -*- - # Most of this file was copied from: -# https://raw.githubusercontent.com/django/django/1.11.12/tests/cache/tests.py +# https://raw.githubusercontent.com/django/django/stable/2.2.x/tests/cache/tests.py # Unit tests for cache framework # Uses whatever cache backend is set in the test settings file. -from __future__ import unicode_literals - import copy import io import os +import pickle import re import shutil import tempfile @@ -17,11 +14,12 @@ import time import unittest import warnings +from unittest import mock from django.conf import settings from django.core import management, signals from django.core.cache import ( - DEFAULT_CACHE_ALIAS, CacheKeyWarning, cache, caches, + DEFAULT_CACHE_ALIAS, CacheKeyWarning, InvalidCacheKey, cache, caches, ) from django.core.cache.utils import make_template_fragment_key from django.db import close_old_connections, connection, connections @@ -37,15 +35,13 @@ from django.template.response import TemplateResponse from django.test import ( RequestFactory, SimpleTestCase, TestCase, TransactionTestCase, - ignore_warnings, mock, override_settings, + override_settings, ) from django.test.signals import setting_changed -from django.utils import six, timezone, translation +from django.utils import timezone, translation from django.utils.cache import ( - get_cache_key, learn_cache_key, patch_cache_control, - patch_response_headers, patch_vary_headers, + get_cache_key, learn_cache_key, patch_cache_control, patch_vary_headers, ) -from django.utils.deprecation import RemovedInDjango21Warning from django.utils.encoding import force_text from django.views.decorators.cache import cache_page @@ -55,27 +51,12 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') -############################################################################ -# GrantJ 2017-03-27 Ignore deprecation warnings. Django's metaclass magic does -# not always play well with Python 3.6. Read -# http://stackoverflow.com/questions/41343263/ for details -############################################################################ - -import warnings -warnings.filterwarnings('ignore', category=DeprecationWarning) - import django django.setup() from .models import Poll, expensive_calculation -try: # Use the same idiom as in cache backends - from django.utils.six.moves import cPickle as pickle -except ImportError: - import pickle - - # functions/classes for complex data type tests def f(): return 42 @@ -86,11 +67,17 @@ def m(n): return 24 -class Unpicklable(object): +class Unpicklable: def __getstate__(self): raise pickle.PickleError() +KEY_ERRORS_WITH_MEMCACHED_MSG = ( + 'Cache key contains characters that will cause errors if used with ' + 'memcached: %r' +) + + class UnpicklableType(object): # Unpicklable using the default pickling protocol on Python 2. __slots__ = 'a', @@ -101,17 +88,12 @@ def custom_key_func(key, key_prefix, version): return 'CUSTOM-' + '-'.join([key_prefix, str(version), key]) -def custom_key_func2(key, key_prefix, version): - "Another customized cache key function" - return '-'.join(['CUSTOM', key_prefix, str(version), key]) - - _caches_setting_base = { 'default': {}, 'prefix': {'KEY_PREFIX': 'cacheprefix{}'.format(os.getpid())}, 'v2': {'VERSION': 2}, 'custom_key': {'KEY_FUNCTION': custom_key_func}, - 'custom_key2': {'KEY_FUNCTION': custom_key_func2}, + 'custom_key2': {'KEY_FUNCTION': 'tests.test_djangocache.custom_key_func'}, 'cull': {'OPTIONS': {'MAX_ENTRIES': 30}}, 'zero_cull': {'OPTIONS': {'CULL_FREQUENCY': 0, 'MAX_ENTRIES': 30}}, } @@ -127,18 +109,16 @@ def caches_setting_for_tests(base=None, exclude=None, **params): # params -> _caches_setting_base -> base base = base or {} exclude = exclude or set() - setting = {k: base.copy() for k in _caches_setting_base.keys() if k not in exclude} + setting = {k: base.copy() for k in _caches_setting_base if k not in exclude} for key, cache_params in setting.items(): cache_params.update(_caches_setting_base[key]) cache_params.update(params) return setting -class BaseCacheTests(object): +class BaseCacheTests: # A common set of tests to apply to all cache backends - - def setUp(self): - self.factory = RequestFactory() + factory = RequestFactory() def tearDown(self): cache.clear() @@ -168,24 +148,20 @@ def test_prefix(self): self.assertEqual(caches['prefix'].get('somekey'), 'value2') def test_non_existent(self): - # Non-existent cache keys return as None/default - # get with non-existent keys + """Nonexistent cache keys return as None/default.""" self.assertIsNone(cache.get("does_not_exist")) self.assertEqual(cache.get("does_not_exist", "bang!"), "bang!") def test_get_many(self): # Multiple cache keys can be returned using get_many - cache.set('a', 'a') - cache.set('b', 'b') - cache.set('c', 'c') - cache.set('d', 'd') - self.assertDictEqual(cache.get_many(['a', 'c', 'd']), {'a': 'a', 'c': 'c', 'd': 'd'}) - self.assertDictEqual(cache.get_many(['a', 'b', 'e']), {'a': 'a', 'b': 'b'}) + cache.set_many({'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'}) + self.assertEqual(cache.get_many(['a', 'c', 'd']), {'a': 'a', 'c': 'c', 'd': 'd'}) + self.assertEqual(cache.get_many(['a', 'b', 'e']), {'a': 'a', 'b': 'b'}) + self.assertEqual(cache.get_many(iter(['a', 'b', 'e'])), {'a': 'a', 'b': 'b'}) def test_delete(self): # Cache keys can be deleted - cache.set("key1", "spam") - cache.set("key2", "eggs") + cache.set_many({'key1': 'spam', 'key2': 'eggs'}) self.assertEqual(cache.get("key1"), "spam") cache.delete("key1") self.assertIsNone(cache.get("key1")) @@ -286,23 +262,6 @@ def test_cache_read_for_model_instance_with_deferred(self): # We only want the default expensive calculation run on creation and set self.assertEqual(expensive_calculation.num_runs, runs_before_cache_read) - def test_touch(self): - # cache.touch() updates the timeout. - cache.set('expire1', 'very quickly', timeout=1) - self.assertTrue(cache.touch('expire1', timeout=2)) - time.sleep(1) - self.assertTrue(cache.has_key('expire1')) - time.sleep(2) - self.assertFalse(cache.has_key('expire1')) - - # cache.touch() works without the timeout argument. - cache.set('expire1', 'very quickly', timeout=1) - self.assertTrue(cache.touch('expire1')) - time.sleep(2) - self.assertTrue(cache.has_key('expire1')) - - self.assertFalse(cache.touch('nonexistent')) - def test_expiration(self): # Cache values can be set to expire cache.set('expire1', 'very quickly', 1) @@ -316,6 +275,23 @@ def test_expiration(self): self.assertEqual(cache.get("expire2"), "newvalue") self.assertFalse(cache.has_key("expire3")) + def test_touch(self): + # cache.touch() updates the timeout. + cache.set('expire1', 'very quickly', timeout=1) + self.assertIs(cache.touch('expire1', timeout=4), True) + time.sleep(2) + self.assertTrue(cache.has_key('expire1')) + time.sleep(3) + self.assertFalse(cache.has_key('expire1')) + + # cache.touch() works without the timeout argument. + cache.set('expire1', 'very quickly', timeout=1) + self.assertIs(cache.touch('expire1'), True) + time.sleep(2) + self.assertTrue(cache.has_key('expire1')) + + self.assertIs(cache.touch('nonexistent'), False) + def test_unicode(self): # Unicode values can be cached stuff = { @@ -326,21 +302,24 @@ def test_unicode(self): } # Test `set` for (key, value) in stuff.items(): - cache.set(key, value) - self.assertEqual(cache.get(key), value) + with self.subTest(key=key): + cache.set(key, value) + self.assertEqual(cache.get(key), value) # Test `add` for (key, value) in stuff.items(): - cache.delete(key) - cache.add(key, value) - self.assertEqual(cache.get(key), value) + with self.subTest(key=key): + cache.delete(key) + cache.add(key, value) + self.assertEqual(cache.get(key), value) # Test `set_many` for (key, value) in stuff.items(): cache.delete(key) cache.set_many(stuff) for (key, value) in stuff.items(): - self.assertEqual(cache.get(key), value) + with self.subTest(key=key): + self.assertEqual(cache.get(key), value) def test_binary_string(self): # Binary strings should be cacheable @@ -372,6 +351,11 @@ def test_set_many(self): self.assertEqual(cache.get("key1"), "spam") self.assertEqual(cache.get("key2"), "eggs") + def test_set_many_returns_empty_list_on_success(self): + """set_many() returns an empty list when all keys are inserted.""" + failing_keys = cache.set_many({'key1': 'spam', 'key2': 'eggs'}) + self.assertEqual(failing_keys, []) + def test_set_many_expiration(self): # set_many takes a second ``timeout`` parameter cache.set_many({"key1": "spam", "key2": "eggs"}, 1) @@ -381,9 +365,7 @@ def test_set_many_expiration(self): def test_delete_many(self): # Multiple keys can be deleted using delete_many - cache.set("key1", "spam") - cache.set("key2", "eggs") - cache.set("key3", "ham") + cache.set_many({'key1': 'spam', 'key2': 'eggs', 'key3': 'ham'}) cache.delete_many(["key1", "key2"]) self.assertIsNone(cache.get("key1")) self.assertIsNone(cache.get("key2")) @@ -391,8 +373,7 @@ def test_delete_many(self): def test_clear(self): # The cache can be emptied using clear - cache.set("key1", "spam") - cache.set("key2", "eggs") + cache.set_many({'key1': 'spam', 'key2': 'eggs'}) cache.clear() self.assertIsNone(cache.get("key1")) self.assertIsNone(cache.get("key2")) @@ -478,10 +459,10 @@ def test_zero_cull(self): def _perform_invalid_key_test(self, key, expected_warning): """ - All the builtin backends (except memcached, see below) should warn on - keys that would be refused by memcached. This encourages portable - caching code without making it too difficult to use production backends - with more liberal key rules. Refs #6447. + All the builtin backends should warn (except memcached that should + error) on keys that would be refused by memcached. This encourages + portable caching code without making it too difficult to use production + backends with more liberal key rules. Refs #6447. """ # mimic custom ``make_key`` method being defined since the default will # never show the below warnings @@ -492,23 +473,16 @@ def func(key, *args): cache.key_func = func try: - with warnings.catch_warnings(record=True) as w: - warnings.simplefilter("always") + with self.assertWarns(CacheKeyWarning) as cm: cache.set(key, 'value') - self.assertEqual(len(w), 1) - self.assertIsInstance(w[0].message, CacheKeyWarning) - self.assertEqual(str(w[0].message.args[0]), expected_warning) + self.assertEqual(str(cm.warning), expected_warning) finally: cache.key_func = old_func def test_invalid_key_characters(self): # memcached doesn't allow whitespace or control characters in keys. key = 'key with spaces and 清' - expected_warning = ( - "Cache key contains characters that will cause errors if used " - "with memcached: %r" % key - ) - self._perform_invalid_key_test(key, expected_warning) + self._perform_invalid_key_test(key, KEY_ERRORS_WITH_MEMCACHED_MSG % key) def test_invalid_key_length(self): # memcached limits key length to 250. @@ -678,43 +652,43 @@ def test_cache_versioning_incr_decr(self): def test_cache_versioning_get_set_many(self): # set, using default version = 1 cache.set_many({'ford1': 37, 'arthur1': 42}) - self.assertDictEqual(cache.get_many(['ford1', 'arthur1']), {'ford1': 37, 'arthur1': 42}) - self.assertDictEqual(cache.get_many(['ford1', 'arthur1'], version=1), {'ford1': 37, 'arthur1': 42}) - self.assertDictEqual(cache.get_many(['ford1', 'arthur1'], version=2), {}) + self.assertEqual(cache.get_many(['ford1', 'arthur1']), {'ford1': 37, 'arthur1': 42}) + self.assertEqual(cache.get_many(['ford1', 'arthur1'], version=1), {'ford1': 37, 'arthur1': 42}) + self.assertEqual(cache.get_many(['ford1', 'arthur1'], version=2), {}) - self.assertDictEqual(caches['v2'].get_many(['ford1', 'arthur1']), {}) - self.assertDictEqual(caches['v2'].get_many(['ford1', 'arthur1'], version=1), {'ford1': 37, 'arthur1': 42}) - self.assertDictEqual(caches['v2'].get_many(['ford1', 'arthur1'], version=2), {}) + self.assertEqual(caches['v2'].get_many(['ford1', 'arthur1']), {}) + self.assertEqual(caches['v2'].get_many(['ford1', 'arthur1'], version=1), {'ford1': 37, 'arthur1': 42}) + self.assertEqual(caches['v2'].get_many(['ford1', 'arthur1'], version=2), {}) # set, default version = 1, but manually override version = 2 cache.set_many({'ford2': 37, 'arthur2': 42}, version=2) - self.assertDictEqual(cache.get_many(['ford2', 'arthur2']), {}) - self.assertDictEqual(cache.get_many(['ford2', 'arthur2'], version=1), {}) - self.assertDictEqual(cache.get_many(['ford2', 'arthur2'], version=2), {'ford2': 37, 'arthur2': 42}) + self.assertEqual(cache.get_many(['ford2', 'arthur2']), {}) + self.assertEqual(cache.get_many(['ford2', 'arthur2'], version=1), {}) + self.assertEqual(cache.get_many(['ford2', 'arthur2'], version=2), {'ford2': 37, 'arthur2': 42}) - self.assertDictEqual(caches['v2'].get_many(['ford2', 'arthur2']), {'ford2': 37, 'arthur2': 42}) - self.assertDictEqual(caches['v2'].get_many(['ford2', 'arthur2'], version=1), {}) - self.assertDictEqual(caches['v2'].get_many(['ford2', 'arthur2'], version=2), {'ford2': 37, 'arthur2': 42}) + self.assertEqual(caches['v2'].get_many(['ford2', 'arthur2']), {'ford2': 37, 'arthur2': 42}) + self.assertEqual(caches['v2'].get_many(['ford2', 'arthur2'], version=1), {}) + self.assertEqual(caches['v2'].get_many(['ford2', 'arthur2'], version=2), {'ford2': 37, 'arthur2': 42}) # v2 set, using default version = 2 caches['v2'].set_many({'ford3': 37, 'arthur3': 42}) - self.assertDictEqual(cache.get_many(['ford3', 'arthur3']), {}) - self.assertDictEqual(cache.get_many(['ford3', 'arthur3'], version=1), {}) - self.assertDictEqual(cache.get_many(['ford3', 'arthur3'], version=2), {'ford3': 37, 'arthur3': 42}) + self.assertEqual(cache.get_many(['ford3', 'arthur3']), {}) + self.assertEqual(cache.get_many(['ford3', 'arthur3'], version=1), {}) + self.assertEqual(cache.get_many(['ford3', 'arthur3'], version=2), {'ford3': 37, 'arthur3': 42}) - self.assertDictEqual(caches['v2'].get_many(['ford3', 'arthur3']), {'ford3': 37, 'arthur3': 42}) - self.assertDictEqual(caches['v2'].get_many(['ford3', 'arthur3'], version=1), {}) - self.assertDictEqual(caches['v2'].get_many(['ford3', 'arthur3'], version=2), {'ford3': 37, 'arthur3': 42}) + self.assertEqual(caches['v2'].get_many(['ford3', 'arthur3']), {'ford3': 37, 'arthur3': 42}) + self.assertEqual(caches['v2'].get_many(['ford3', 'arthur3'], version=1), {}) + self.assertEqual(caches['v2'].get_many(['ford3', 'arthur3'], version=2), {'ford3': 37, 'arthur3': 42}) # v2 set, default version = 2, but manually override version = 1 caches['v2'].set_many({'ford4': 37, 'arthur4': 42}, version=1) - self.assertDictEqual(cache.get_many(['ford4', 'arthur4']), {'ford4': 37, 'arthur4': 42}) - self.assertDictEqual(cache.get_many(['ford4', 'arthur4'], version=1), {'ford4': 37, 'arthur4': 42}) - self.assertDictEqual(cache.get_many(['ford4', 'arthur4'], version=2), {}) + self.assertEqual(cache.get_many(['ford4', 'arthur4']), {'ford4': 37, 'arthur4': 42}) + self.assertEqual(cache.get_many(['ford4', 'arthur4'], version=1), {'ford4': 37, 'arthur4': 42}) + self.assertEqual(cache.get_many(['ford4', 'arthur4'], version=2), {}) - self.assertDictEqual(caches['v2'].get_many(['ford4', 'arthur4']), {}) - self.assertDictEqual(caches['v2'].get_many(['ford4', 'arthur4'], version=1), {'ford4': 37, 'arthur4': 42}) - self.assertDictEqual(caches['v2'].get_many(['ford4', 'arthur4'], version=2), {}) + self.assertEqual(caches['v2'].get_many(['ford4', 'arthur4']), {}) + self.assertEqual(caches['v2'].get_many(['ford4', 'arthur4'], version=1), {'ford4': 37, 'arthur4': 42}) + self.assertEqual(caches['v2'].get_many(['ford4', 'arthur4'], version=2), {}) def test_incr_version(self): cache.set('answer', 42, version=2) @@ -801,13 +775,13 @@ def test_cache_write_unpicklable_object(self): get_cache_data = fetch_middleware.process_request(request) self.assertIsNotNone(get_cache_data) - self.assertEqual(get_cache_data.content, content.encode('utf-8')) + self.assertEqual(get_cache_data.content, content.encode()) self.assertEqual(get_cache_data.cookies, response.cookies) update_middleware.process_response(request, get_cache_data) get_cache_data = fetch_middleware.process_request(request) self.assertIsNotNone(get_cache_data) - self.assertEqual(get_cache_data.content, content.encode('utf-8')) + self.assertEqual(get_cache_data.content, content.encode()) self.assertEqual(get_cache_data.cookies, response.cookies) def test_add_fail_on_pickleerror(self): @@ -838,10 +812,11 @@ def test_get_or_set_callable_returning_none(self): self.assertEqual(cache.get('mykey', 'default'), 'default') def test_get_or_set_version(self): + msg = "get_or_set() missing 1 required positional argument: 'default'" cache.get_or_set('brian', 1979, version=2) - with self.assertRaises(TypeError): + with self.assertRaisesMessage(TypeError, msg): cache.get_or_set('brian') - with self.assertRaises(TypeError): + with self.assertRaisesMessage(TypeError, msg): cache.get_or_set('brian', version=1) self.assertIsNone(cache.get('brian', version=1)) self.assertEqual(cache.get_or_set('brian', 42, version=1), 42) @@ -856,15 +831,14 @@ def test_get_or_set_racing(self): self.assertEqual(cache.get_or_set('key', 'default'), 'default') -class PicklingSideEffect(object): +class PicklingSideEffect: def __init__(self, cache): self.cache = cache self.locked = False def __getstate__(self): - if self.cache._lock.active_writers: - self.locked = True + self.locked = self.cache._lock.locked() return {} @@ -874,9 +848,9 @@ def __getstate__(self): class DiskCacheTests(BaseCacheTests, TestCase): "Specific test cases for diskcache.DjangoCache." def setUp(self): - super(DiskCacheTests, self).setUp() + super().setUp() self.dirname = tempfile.mkdtemp() - # Cache location cannot be modified through override_settings / modify_settings, + # Caches location cannot be modified through override_settings / modify_settings, # hence settings are manipulated directly here and the setting_changed signal # is triggered manually. for cache_params in settings.CACHES.values(): @@ -884,7 +858,7 @@ def setUp(self): setting_changed.send(self.__class__, setting='CACHES', enter=False) def tearDown(self): - super(DiskCacheTests, self).tearDown() + super().tearDown() cache.close() shutil.rmtree(self.dirname, ignore_errors=True) From e8f965945e5e82353080bb3454de68084c7e34db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20G=C3=B3rny?= Date: Sat, 27 Jun 2020 20:35:13 +0200 Subject: [PATCH 082/211] tests: Use unittest.mock over external mock (#144) Mocking support is built-in since Python 3.3. Use it over external mock when available. This helps distributions that are phasing Python 2 out and would like to remove mock package as part of that. Co-authored-by: Grant Jenks --- requirements.txt | 2 +- tests/test_core.py | 6 +++++- tests/test_deque.py | 6 +++++- tests/test_fanout.py | 6 +++++- tests/test_index.py | 6 +++++- tests/test_recipes.py | 6 +++++- 6 files changed, 26 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index ae05547..ef73654 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ django==2.2.* django_redis doc8 gj -mock +mock;python_version<"3.3" nose pylibmc pylint diff --git a/tests/test_core.py b/tests/test_core.py index 7e79995..1bc4e15 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -7,7 +7,6 @@ import functools as ft import hashlib import io -import mock import os import os.path as op import pytest @@ -27,6 +26,11 @@ except: import pickle +try: + from unittest import mock +except: + import mock + import diskcache import diskcache as dc diff --git a/tests/test_deque.py b/tests/test_deque.py index 640cee2..a00b7fc 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -1,11 +1,15 @@ "Test diskcache.persistent.Deque." import functools as ft -import mock import pickle import pytest import shutil +try: + from unittest import mock +except: + import mock + import diskcache as dc from diskcache.core import ENOVAL diff --git a/tests/test_fanout.py b/tests/test_fanout.py index a62f8a2..10919aa 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -7,7 +7,6 @@ import functools as ft import hashlib import io -import mock import os import os.path as op import pytest @@ -21,6 +20,11 @@ import time import warnings +try: + from unittest import mock +except: + import mock + try: import cPickle as pickle except: diff --git a/tests/test_index.py b/tests/test_index.py index f4232c8..308f894 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,12 +1,16 @@ "Test diskcache.persistent.Index." import functools as ft -import mock import pickle import pytest import shutil import sys +try: + from unittest import mock +except: + import mock + import diskcache as dc diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 35de332..4d908c7 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -1,12 +1,16 @@ "Test diskcache.recipes." import diskcache as dc -import mock import pytest import shutil import threading import time +try: + from unittest import mock +except: + import mock + @pytest.fixture def cache(): From 1794c74ffcb9a5d4a174c8beac46f687884d0956 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 27 Jun 2020 11:40:21 -0700 Subject: [PATCH 083/211] Use unittest.mock in tests --- requirements.txt | 1 - tests/test_core.py | 13 ++----------- tests/test_deque.py | 5 +---- tests/test_djangocache.py | 1 + tests/test_fanout.py | 11 ++--------- tests/test_index.py | 5 +---- tests/test_recipes.py | 5 +---- 7 files changed, 8 insertions(+), 33 deletions(-) diff --git a/requirements.txt b/requirements.txt index ef73654..6c1a277 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ django==2.2.* django_redis doc8 gj -mock;python_version<"3.3" nose pylibmc pylint diff --git a/tests/test_core.py b/tests/test_core.py index 1bc4e15..7c38874 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,7 +1,5 @@ "Test diskcache.core.Cache." -from __future__ import print_function - import collections as co import errno import functools as ft @@ -9,6 +7,7 @@ import io import os import os.path as op +import pickle import pytest import random import shutil @@ -21,15 +20,7 @@ import unittest import warnings -try: - import cPickle as pickle -except: - import pickle - -try: - from unittest import mock -except: - import mock +from unittest import mock import diskcache import diskcache as dc diff --git a/tests/test_deque.py b/tests/test_deque.py index a00b7fc..ddf2338 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -5,10 +5,7 @@ import pytest import shutil -try: - from unittest import mock -except: - import mock +from unittest import mock import diskcache as dc from diskcache.core import ENOVAL diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index 100f998..84d2f95 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -14,6 +14,7 @@ import time import unittest import warnings + from unittest import mock from django.conf import settings diff --git a/tests/test_fanout.py b/tests/test_fanout.py index 10919aa..f5d9b70 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -9,6 +9,7 @@ import io import os import os.path as op +import pickle import pytest import random import shutil @@ -20,15 +21,7 @@ import time import warnings -try: - from unittest import mock -except: - import mock - -try: - import cPickle as pickle -except: - import pickle +from unittest import mock import diskcache as dc diff --git a/tests/test_index.py b/tests/test_index.py index 308f894..ef2a0d0 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -6,10 +6,7 @@ import shutil import sys -try: - from unittest import mock -except: - import mock +from unittest import mock import diskcache as dc diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 4d908c7..b26a239 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -6,10 +6,7 @@ import threading import time -try: - from unittest import mock -except: - import mock +from unittest import mock @pytest.fixture From 7e917912f3d1eb1b5263110687b25ce8d231505a Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 5 Jul 2020 14:45:46 -0700 Subject: [PATCH 084/211] Run pylint in 3.8 venv --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6c1bd8e..a2ebaf2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,6 @@ matrix: env: TOXENV=py38 - python: pypy env: TOXENV=pypy - - python: 3.7 + - python: 3.8 dist: xenial - env: TOXENV=lint + env: TOXENV=pylint From de987e377616806303a166f27715c468a4421b86 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 5 Jul 2020 14:46:01 -0700 Subject: [PATCH 085/211] Drop pypy for Django 2 support --- .travis.yml | 2 -- tox.ini | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index a2ebaf2..5e5c760 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,6 @@ matrix: - python: 3.8 dist: xenial env: TOXENV=py38 - - python: pypy - env: TOXENV=pypy - python: 3.8 dist: xenial env: TOXENV=pylint diff --git a/tox.ini b/tox.ini index ea3996d..b2b6d45 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=py35,py36,py37,py38,pypy,pylint +envlist=py35,py36,py37,py38,pylint skip_missing_interpreters=True [testenv] From a6212df9d45b7df157044b1b1720cd997ef0f69f Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 22 Aug 2020 21:59:29 -0700 Subject: [PATCH 086/211] Remove unused "env" setting from pytest section --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index b2b6d45..8e9c2e0 100644 --- a/tox.ini +++ b/tox.ini @@ -23,9 +23,6 @@ addopts= --ignore tests/plot.py norecursedirs=site-packages testpaths=docs diskcache tests -env = - DJANGO_SETTINGS_MODULE=tests.settings - PYTHONPATH={PWD}:{PWD}/tests [testenv:pylint] deps= From 82b45e8e342ee597dba56f0a42c0d6088b0c2037 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 22 Aug 2020 21:59:59 -0700 Subject: [PATCH 087/211] Remove nose references --- requirements.txt | 1 - tests/test_core.py | 5 ----- tests/test_fanout.py | 5 ----- 3 files changed, 11 deletions(-) diff --git a/requirements.txt b/requirements.txt index 6c1a277..835f48a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,6 @@ django==2.2.* django_redis doc8 gj -nose pylibmc pylint pytest diff --git a/tests/test_core.py b/tests/test_core.py index 7c38874..36311e5 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1464,8 +1464,3 @@ def fibrec(num): assert hits2 == (hits1 + count) assert misses2 == misses1 - - -if __name__ == '__main__': - import nose - nose.runmodule() diff --git a/tests/test_fanout.py b/tests/test_fanout.py index f5d9b70..390961b 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -685,8 +685,3 @@ def test_custom_filename_disk(): assert content == str(count) * int(1e5) shutil.rmtree(cache.directory, ignore_errors=True) - - -if __name__ == '__main__': - import nose - nose.runmodule() From 83fe89c80371278d45917b6adde58c179fc5f5e2 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 22 Aug 2020 22:01:36 -0700 Subject: [PATCH 088/211] Fixes for pylint in Python 3.8 --- diskcache/core.py | 32 ++++++++++++++------------------ diskcache/djangocache.py | 4 ++-- diskcache/persistent.py | 2 +- 3 files changed, 17 insertions(+), 21 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 0c8fd2c..0cf95a1 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -180,7 +180,7 @@ def put(self, key): :return: (database key, raw boolean) pair """ - # pylint: disable=bad-continuation,unidiomatic-typecheck + # pylint: disable=unidiomatic-typecheck type_key = type(key) if type_key is BytesType: @@ -378,17 +378,17 @@ def __init__(self, directory, compress_level=1, **kwargs): """ self.compress_level = compress_level - super(JSONDisk, self).__init__(directory, **kwargs) + super().__init__(directory, **kwargs) def put(self, key): json_bytes = json.dumps(key).encode('utf-8') data = zlib.compress(json_bytes, self.compress_level) - return super(JSONDisk, self).put(data) + return super().put(data) def get(self, key, raw): - data = super(JSONDisk, self).get(key, raw) + data = super().get(key, raw) return json.loads(zlib.decompress(data).decode('utf-8')) @@ -396,11 +396,11 @@ def store(self, value, read, key=UNKNOWN): if not read: json_bytes = json.dumps(value).encode('utf-8') value = zlib.compress(json_bytes, self.compress_level) - return super(JSONDisk, self).store(value, read, key=key) + return super().store(value, read, key=key) def fetch(self, mode, filename, value, read): - data = super(JSONDisk, self).fetch(mode, filename, value, read) + data = super().fetch(mode, filename, value, read) if not read: data = json.loads(zlib.decompress(data).decode('utf-8')) return data @@ -448,7 +448,6 @@ def args_to_key(base, args, kwargs, typed): class Cache(object): "Disk and file backed cache." - # pylint: disable=bad-continuation def __init__(self, directory=None, timeout=60, disk=Disk, **settings): """Initialize cache instance. @@ -461,7 +460,7 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): try: assert issubclass(disk, Disk) except (TypeError, AssertionError): - raise ValueError('disk must subclass diskcache.Disk') + raise ValueError('disk must subclass diskcache.Disk') from None if directory is None: directory = tempfile.mkdtemp(prefix='diskcache-') @@ -482,7 +481,7 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): error.errno, 'Cache directory "%s" does not exist' ' and could not be created' % self._directory - ) + ) from None sql = self._sql_retry @@ -756,7 +755,7 @@ def _transact(self, retry=False, filename=None): continue if filename is not None: _disk_remove(filename) - raise Timeout + raise Timeout from None try: yield sql, filenames.append @@ -1609,8 +1608,7 @@ def pull(self, prefix=None, default=(None, None), side='front', if error.errno == errno.ENOENT: # Key was deleted before we could retrieve result. continue - else: - raise + raise finally: if name is not None: self._disk.remove(name) @@ -1719,8 +1717,7 @@ def peek(self, prefix=None, default=(None, None), side='front', if error.errno == errno.ENOENT: # Key was deleted before we could retrieve result. continue - else: - raise + raise finally: if name is not None: self._disk.remove(name) @@ -1794,8 +1791,7 @@ def peekitem(self, last=True, expire_time=False, tag=False, retry=False): if error.errno == errno.ENOENT: # Key was deleted before we could retrieve result. continue - else: - raise + raise break if expire_time and tag: @@ -2162,7 +2158,7 @@ def cull(self, retry=False): for filename, in rows: cleanup(filename) except Timeout: - raise Timeout(count) + raise Timeout(count) from None return count @@ -2215,7 +2211,7 @@ def _select_delete(self, select, args, row_index=0, arg_index=0, cleanup(row[-1]) except Timeout: - raise Timeout(count) + raise Timeout(count) from None return count diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index 997b852..329b966 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -22,7 +22,7 @@ def __init__(self, directory, params): :param dict params: cache parameters """ - super(DjangoCache, self).__init__(params) + super().__init__(params) shards = params.get('SHARDS', 8) timeout = params.get('DATABASE_TIMEOUT', 0.010) options = params.get('OPTIONS', {}) @@ -228,7 +228,7 @@ def incr(self, key, delta=1, version=None, default=None, retry=True): try: return self._cache.incr(key, delta, default, retry) except KeyError: - raise ValueError("Key '%s' not found" % key) + raise ValueError("Key '%s' not found" % key) from None def decr(self, key, delta=1, version=None, default=None, retry=True): diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 9de5835..76eb9a2 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -915,7 +915,7 @@ def popitem(self, last=True): :raises KeyError: if index is empty """ - # pylint: disable=arguments-differ + # pylint: disable=arguments-differ,unbalanced-tuple-unpacking _cache = self._cache with _cache.transact(retry=True): From e197a93e60a10fb8ed6030fc6b556b122a664573 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 22 Aug 2020 22:25:17 -0700 Subject: [PATCH 089/211] Update 2019 date references to 2020 --- LICENSE | 2 +- README.rst | 4 ++-- docs/conf.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 3259b98..d31985f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2016-2019 Grant Jenks +Copyright 2016-2020 Grant Jenks Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the diff --git a/README.rst b/README.rst index 57b7a2e..ba53988 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ DiskCache: Disk Backed Cache `DiskCache`_ is an Apache2 licensed disk and file backed cache library, written in pure-Python, and compatible with Django. -The cloud-based computing of 2019 puts a premium on memory. Gigabytes of empty +The cloud-based computing of 2020 puts a premium on memory. Gigabytes of empty space is left on disks as processes vie for memory. Among these processes is Memcached (and sometimes Redis) which is used as a cache. Wouldn't it be nice to leverage empty disk space for caching? @@ -388,7 +388,7 @@ Reference License ------- -Copyright 2016-2019 Grant Jenks +Copyright 2016-2020 Grant Jenks Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the diff --git a/docs/conf.py b/docs/conf.py index 683dd3e..2556188 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -51,7 +51,7 @@ # General information about the project. project = u'DiskCache' -copyright = u'2019, Grant Jenks' +copyright = u'2020, Grant Jenks' author = u'Grant Jenks' # The version info for the project you're documenting, acts as replacement for From 2ba5b7985d17141afa7fb5fb3bf5913b0e496fe6 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 22 Aug 2020 22:44:32 -0700 Subject: [PATCH 090/211] Update references to Python 2 support --- README.rst | 4 ++-- docs/development.rst | 4 +--- setup.py | 5 +---- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index ba53988..847b2cf 100644 --- a/README.rst +++ b/README.rst @@ -77,8 +77,8 @@ Features - Thread-safe and process-safe - Supports multiple eviction policies (LRU and LFU included) - Keys support "tag" metadata and eviction -- Developed on Python 3.7 -- Tested on CPython 2.7, 3.4, 3.5, 3.6, 3.7 and PyPy +- Developed on Python 3.8 +- Tested on CPython 3.5, 3.6, 3.7, 3.8 - Tested on Linux, Mac OS X, and Windows - Tested using Travis CI and AppVeyor CI diff --git a/docs/development.rst b/docs/development.rst index 6d16d44..828c3be 100644 --- a/docs/development.rst +++ b/docs/development.rst @@ -67,12 +67,10 @@ Testing :doc:`DiskCache ` currently tests against five versions of Python: -* CPython 2.7 -* CPython 3.4 * CPython 3.5 * CPython 3.6 * CPython 3.7 -* PyPy2 +* CPython 3.8 Testing uses `tox `_. If you don't want to install all the development requirements, then, after downloading, you can diff --git a/setup.py b/setup.py index 90dc280..d9be963 100644 --- a/setup.py +++ b/setup.py @@ -38,14 +38,11 @@ def run_tests(self): 'License :: OSI Approved :: Apache Software License', 'Natural Language :: English', 'Programming Language :: Python', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: Implementation :: CPython', - 'Programming Language :: Python :: Implementation :: PyPy', ), ) From 4497cfc85197d57298120dbd238d263bfb9e9557 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 22 Aug 2020 22:58:07 -0700 Subject: [PATCH 091/211] Remove Python 2 shims --- diskcache/core.py | 68 +++++++++++------------------------------ diskcache/fanout.py | 29 ++---------------- diskcache/persistent.py | 25 +++------------ diskcache/recipes.py | 17 ++--------- 4 files changed, 27 insertions(+), 112 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 0cf95a1..1ec6acc 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -10,6 +10,7 @@ import json import os import os.path as op +import pickle import pickletools import sqlite3 import struct @@ -20,42 +21,9 @@ import warnings import zlib -############################################################################ -# BEGIN Python 2/3 Shims -############################################################################ - -if sys.hexversion < 0x03000000: - import cPickle as pickle # pylint: disable=import-error - # ISSUE #25 Fix for http://bugs.python.org/issue10211 - from cStringIO import StringIO as BytesIO # pylint: disable=import-error - from thread import get_ident # pylint: disable=import-error,no-name-in-module - TextType = unicode # pylint: disable=invalid-name,undefined-variable - BytesType = str - INT_TYPES = int, long # pylint: disable=undefined-variable - range = xrange # pylint: disable=redefined-builtin,invalid-name,undefined-variable - io_open = io.open # pylint: disable=invalid-name -else: - import pickle - from io import BytesIO # pylint: disable=ungrouped-imports - from threading import get_ident - TextType = str - BytesType = bytes - INT_TYPES = (int,) - io_open = open # pylint: disable=invalid-name - def full_name(func): "Return full name of `func` by adding the module and function name." - try: - # The __qualname__ attribute is only available in Python 3.3 and later. - # GrantJ 2019-03-29 Remove after support for Python 2 is dropped. - name = func.__qualname__ - except AttributeError: - name = func.__name__ - return func.__module__ + '.' + name - -############################################################################ -# END Python 2/3 Shims -############################################################################ + return func.__module__ + '.' + func.__qualname__ try: WindowsError @@ -164,9 +132,9 @@ def hash(self, key): if type_disk_key is sqlite3.Binary: return zlib.adler32(disk_key) & mask - elif type_disk_key is TextType: + elif type_disk_key is str: return zlib.adler32(disk_key.encode('utf-8')) & mask # pylint: disable=no-member - elif type_disk_key in INT_TYPES: + elif type_disk_key is int: return disk_key % mask else: assert type_disk_key is float @@ -183,10 +151,10 @@ def put(self, key): # pylint: disable=unidiomatic-typecheck type_key = type(key) - if type_key is BytesType: + if type_key is bytes: return sqlite3.Binary(key), True - elif ((type_key is TextType) - or (type_key in INT_TYPES + elif ((type_key is str) + or (type_key is int and -9223372036854775808 <= key <= 9223372036854775807) or (type_key is float)): return key, True @@ -206,9 +174,9 @@ def get(self, key, raw): """ # pylint: disable=no-self-use,unidiomatic-typecheck if raw: - return BytesType(key) if type(key) is sqlite3.Binary else key + return bytes(key) if type(key) is sqlite3.Binary else key else: - return pickle.load(BytesIO(key)) + return pickle.load(io.BytesIO(key)) def store(self, value, read, key=UNKNOWN): @@ -225,12 +193,12 @@ def store(self, value, read, key=UNKNOWN): type_value = type(value) min_file_size = self.min_file_size - if ((type_value is TextType and len(value) < min_file_size) - or (type_value in INT_TYPES + if ((type_value is str and len(value) < min_file_size) + or (type_value is int and -9223372036854775808 <= value <= 9223372036854775807) or (type_value is float)): return 0, MODE_RAW, None, value - elif type_value is BytesType: + elif type_value is bytes: if len(value) < min_file_size: return 0, MODE_RAW, None, sqlite3.Binary(value) else: @@ -240,10 +208,10 @@ def store(self, value, read, key=UNKNOWN): writer.write(value) return len(value), MODE_BINARY, filename, None - elif type_value is TextType: + elif type_value is str: filename, full_path = self.filename(key, value) - with io_open(full_path, 'w', encoding='UTF-8') as writer: + with open(full_path, 'w', encoding='UTF-8') as writer: writer.write(value) size = op.getsize(full_path) @@ -286,7 +254,7 @@ def fetch(self, mode, filename, value, read): """ # pylint: disable=no-self-use,unidiomatic-typecheck if mode == MODE_RAW: - return BytesType(value) if type(value) is sqlite3.Binary else value + return bytes(value) if type(value) is sqlite3.Binary else value elif mode == MODE_BINARY: if read: return open(op.join(self._directory, filename), 'rb') @@ -295,14 +263,14 @@ def fetch(self, mode, filename, value, read): return reader.read() elif mode == MODE_TEXT: full_path = op.join(self._directory, filename) - with io_open(full_path, 'r', encoding='UTF-8') as reader: + with open(full_path, 'r', encoding='UTF-8') as reader: return reader.read() elif mode == MODE_PICKLE: if value is None: with open(op.join(self._directory, filename), 'rb') as reader: return pickle.load(reader) else: - return pickle.load(BytesIO(value)) + return pickle.load(io.BytesIO(value)) def filename(self, key=UNKNOWN, value=UNKNOWN): @@ -738,7 +706,7 @@ def _transact(self, retry=False, filename=None): sql = self._sql filenames = [] _disk_remove = self._disk.remove - tid = get_ident() + tid = threading.get_ident() txn_id = self._txn_id if tid == txn_id: diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 8a0a722..1a59aa2 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -1,5 +1,6 @@ "Fanout cache automatically shards keys and values." +import functools import itertools as it import operator import os.path as op @@ -11,17 +12,6 @@ from .core import ENOVAL, DEFAULT_SETTINGS, Cache, Disk, Timeout from .persistent import Deque, Index -############################################################################ -# BEGIN Python 2/3 Shims -############################################################################ - -if sys.hexversion >= 0x03000000: - from functools import reduce - -############################################################################ -# END Python 2/3 Shims -############################################################################ - class FanoutCache(object): "Cache that shards keys and values." @@ -383,7 +373,7 @@ def check(self, fix=False, retry=False): """ warnings = (shard.check(fix, retry) for shard in self._shards) - return reduce(operator.iadd, warnings, []) + return functools.reduce(operator.iadd, warnings, []) def expire(self, retry=False): @@ -661,17 +651,4 @@ def index(self, name): return temp -############################################################################ -# BEGIN Python 2/3 Shims -############################################################################ - -if sys.hexversion < 0x03000000: - import types - memoize_func = Cache.__dict__['memoize'] # pylint: disable=invalid-name - FanoutCache.memoize = types.MethodType(memoize_func, None, FanoutCache) -else: - FanoutCache.memoize = Cache.memoize - -############################################################################ -# END Python 2/3 Shims -############################################################################ +FanoutCache.memoize = Cache.memoize diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 76eb9a2..5047df5 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -6,29 +6,12 @@ import sys from collections import OrderedDict +from collections.abc import MutableMapping, Sequence +from collections.abc import KeysView, ValuesView, ItemsView from contextlib import contextmanager from shutil import rmtree -from .core import BytesType, Cache, ENOVAL, TextType - -############################################################################ -# BEGIN Python 2/3 Shims -############################################################################ - -try: - from collections.abc import MutableMapping, Sequence - from collections.abc import KeysView, ValuesView, ItemsView -except ImportError: - from collections import MutableMapping, Sequence - from collections import KeysView, ValuesView, ItemsView - -if sys.hexversion < 0x03000000: - from itertools import izip as zip # pylint: disable=redefined-builtin,no-name-in-module,ungrouped-imports - range = xrange # pylint: disable=redefined-builtin,invalid-name,undefined-variable - -############################################################################ -# END Python 2/3 Shims -############################################################################ +from .core import Cache, ENOVAL def _make_compare(seq_op, doc): @@ -699,7 +682,7 @@ def __init__(self, *args, **kwargs): 4 """ - if args and isinstance(args[0], (BytesType, TextType)): + if args and isinstance(args[0], (bytes, str)): directory = args[0] args = args[1:] else: diff --git a/diskcache/recipes.py b/diskcache/recipes.py index fb64250..010f1f2 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -12,19 +12,6 @@ from .core import ENOVAL, args_to_key, full_name -############################################################################ -# BEGIN Python 2/3 Shims -############################################################################ - -if sys.hexversion < 0x03000000: - from thread import get_ident # pylint: disable=import-error -else: - from threading import get_ident - -############################################################################ -# END Python 2/3 Shims -############################################################################ - class Averager(object): """Recipe for calculating a running average. @@ -139,7 +126,7 @@ def __init__(self, cache, key, expire=None, tag=None): def acquire(self): "Acquire lock by incrementing count using spin-lock algorithm." pid = os.getpid() - tid = get_ident() + tid = threading.get_ident() pid_tid = '{}-{}'.format(pid, tid) while True: @@ -156,7 +143,7 @@ def acquire(self): def release(self): "Release lock by decrementing count." pid = os.getpid() - tid = get_ident() + tid = threading.get_ident() pid_tid = '{}-{}'.format(pid, tid) with self._cache.transact(retry=True): From 8e0f545a986c8dbb514aaa3c526472e1961934b6 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 22 Aug 2020 23:13:45 -0700 Subject: [PATCH 092/211] Add flake8 to linters and fix issues --- diskcache/__init__.py | 6 ++++-- diskcache/core.py | 22 +++++++++++++++------- diskcache/fanout.py | 3 +-- diskcache/persistent.py | 5 +++-- diskcache/recipes.py | 7 +++++-- tox.ini | 13 +++++++++++-- 6 files changed, 39 insertions(+), 17 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 192524e..a301f26 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -6,7 +6,9 @@ """ -from .core import Cache, Disk, EmptyDirWarning, JSONDisk, UnknownFileWarning, Timeout +from .core import ( + Cache, Disk, EmptyDirWarning, JSONDisk, UnknownFileWarning, Timeout +) from .core import DEFAULT_SETTINGS, ENOVAL, EVICTION_POLICY, UNKNOWN from .fanout import FanoutCache from .persistent import Deque, Index @@ -37,7 +39,7 @@ ] try: - from .djangocache import DjangoCache # pylint: disable=wrong-import-position + from .djangocache import DjangoCache # noqa __all__.append('DjangoCache') except Exception: # pylint: disable=broad-except # Django not installed or not setup so ignore. diff --git a/diskcache/core.py b/diskcache/core.py index 1ec6acc..4317160 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -14,23 +14,25 @@ import pickletools import sqlite3 import struct -import sys import tempfile import threading import time import warnings import zlib + def full_name(func): "Return full name of `func` by adding the module and function name." return func.__module__ + '.' + func.__qualname__ + try: WindowsError except NameError: class WindowsError(Exception): "Windows error place-holder on platforms without support." + class Constant(tuple): "Pretty display of immutable constant." def __new__(cls, name): @@ -39,6 +41,7 @@ def __new__(cls, name): def __repr__(self): return '%s' % self[0] + DBNAME = 'cache.db' ENOVAL = Constant('ENOVAL') UNKNOWN = Constant('UNKNOWN') @@ -133,7 +136,7 @@ def hash(self, key): if type_disk_key is sqlite3.Binary: return zlib.adler32(disk_key) & mask elif type_disk_key is str: - return zlib.adler32(disk_key.encode('utf-8')) & mask # pylint: disable=no-member + return zlib.adler32(disk_key.encode('utf-8')) & mask # noqa elif type_disk_key is int: return disk_key % mask else: @@ -1056,7 +1059,9 @@ def incr(self, key, delta=1, default=0, retry=False): raise KeyError(key) value = default + delta - columns = (None, None) + self._disk.store(value, False, key=key) + columns = ( + (None, None) + self._disk.store(value, False, key=key) + ) self._row_insert(db_key, raw, now, columns) self._cull(now, sql, cleanup) return value @@ -1068,7 +1073,9 @@ def incr(self, key, delta=1, default=0, retry=False): raise KeyError(key) value = default + delta - columns = (None, None) + self._disk.store(value, False, key=key) + columns = ( + (None, None) + self._disk.store(value, False, key=key) + ) self._row_update(rowid, now, columns) self._cull(now, sql, cleanup) cleanup(filename) @@ -1184,7 +1191,7 @@ def get(self, key, default=None, read=False, expire_time=False, tag=False, return default (rowid, db_expire_time, db_tag, - mode, filename, db_value), = rows + mode, filename, db_value), = rows # noqa: E127 try: value = self._disk.fetch(mode, filename, db_value, read) @@ -1269,7 +1276,7 @@ def __contains__(self, key): return bool(rows) - def pop(self, key, default=None, expire_time=False, tag=False, retry=False): + def pop(self, key, default=None, expire_time=False, tag=False, retry=False): # noqa: E501 """Remove corresponding item for `key` from cache and return value. If `key` is missing, return `default`. @@ -2341,7 +2348,8 @@ def close(self): def __enter__(self): # Create connection in thread. - connection = self._con # pylint: disable=unused-variable + # pylint: disable=unused-variable + connection = self._con # noqa return self diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 1a59aa2..a579a17 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -5,7 +5,6 @@ import operator import os.path as op import sqlite3 -import sys import tempfile import time @@ -290,7 +289,7 @@ def __contains__(self, key): return key in shard - def pop(self, key, default=None, expire_time=False, tag=False, retry=False): + def pop(self, key, default=None, expire_time=False, tag=False, retry=False): # noqa: E501 """Remove corresponding item for `key` from cache and return value. If `key` is missing, return `default`. diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 5047df5..2e40074 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -197,8 +197,9 @@ def __setitem__(self, index, value): :raises IndexError: if index out of range """ - set_value = lambda key: self._cache.__setitem__(key, value) - self._index(index, set_value) + def _set_value(key): + return self._cache.__setitem__(key, value) + self._index(index, _set_value) def __delitem__(self, index): diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 010f1f2..1a5890d 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -6,7 +6,6 @@ import math import os import random -import sys import threading import time @@ -82,7 +81,11 @@ def acquire(self): "Acquire lock using spin-lock algorithm." while True: added = self._cache.add( - self._key, None, expire=self._expire, tag=self._tag, retry=True, + self._key, + None, + expire=self._expire, + tag=self._tag, + retry=True, ) if added: break diff --git a/tox.ini b/tox.ini index 8e9c2e0..b54e574 100644 --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,7 @@ deps= pytest-django pytest-xdist commands=python -m pytest -setenv = +setenv= DJANGO_SETTINGS_MODULE=tests.settings PYTHONPATH={toxinidir} @@ -27,8 +27,17 @@ testpaths=docs diskcache tests [testenv:pylint] deps= django==2.2.* + flake8 pylint -commands=pylint diskcache +commands= + flake8 diskcache + pylint diskcache [doc8] ignore=D000 + +[flake8] +ignore= + E124 + E303 + W503 From 4f2ff540155c0182ff07a5a3408aceb8c10cff1e Mon Sep 17 00:00:00 2001 From: ume Date: Sat, 1 Aug 2020 10:26:23 +0900 Subject: [PATCH 093/211] Add locked method to Lock --- diskcache/recipes.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 1a5890d..85509c7 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -95,6 +95,10 @@ def release(self): "Release lock by deleting key." self._cache.delete(self._key, retry=True) + def locked(self): + "Return true if the lock is acquired." + return self._key in self._cache + def __enter__(self): self.acquire() From a8a014cb96ed0d96c12d07b6b7d4729b54c0c011 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 23 Aug 2020 15:47:49 -0700 Subject: [PATCH 094/211] Add paragraph to caveats about cache volume for Issue #140 --- docs/tutorial.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index b29322c..8f8dc0f 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -892,6 +892,12 @@ thread-pool executor asynchronously. For example:: asyncio.run(set_async('test-key', 'test-value')) +The cache :meth:`volume ` is based on the size of the +database that stores metadata and the size of the values stored in files. It +does not account the size of directories themselves or other filesystem +metadata. If directory count or size is a concern then consider implementing an +alternative :class:`Disk `. + .. _`hash protocol`: https://docs.python.org/library/functions.html#hash .. _`not recommended`: https://www.sqlite.org/faq.html#q5 .. _`performs poorly`: https://www.pythonanywhere.com/forums/topic/1847/ From b1ab12c7a85bf5473fdc491d2d03c9adc6de5bba Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 23 Aug 2020 15:56:19 -0700 Subject: [PATCH 095/211] Bump version to 5.0.0 --- diskcache/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index a301f26..62766c5 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -46,8 +46,8 @@ pass __title__ = 'diskcache' -__version__ = '4.1.0' -__build__ = 0x040100 +__version__ = '5.0.0' +__build__ = 0x050000 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' -__copyright__ = 'Copyright 2016-2018 Grant Jenks' +__copyright__ = 'Copyright 2016-2020 Grant Jenks' From 38bd9019e1b8d5adec52f7bc893a6f662c49cd20 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 23 Aug 2020 16:23:02 -0700 Subject: [PATCH 096/211] Remove Python 2 mapping methods keys/values/items/iter*/view* --- diskcache/persistent.py | 207 ++++++---------------------------------- 1 file changed, 30 insertions(+), 177 deletions(-) diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 2e40074..4324660 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -1031,196 +1031,49 @@ def __len__(self): return len(self._cache) - if sys.hexversion < 0x03000000: - def keys(self): - """List of index keys. + def keys(self): + """Set-like object providing a view of index keys. - >>> index = Index() - >>> index.update([('a', 1), ('b', 2), ('c', 3)]) - >>> index.keys() - ['a', 'b', 'c'] - - :return: list of keys - - """ - return list(self._cache) - - - def values(self): - """List of index values. - - >>> index = Index() - >>> index.update([('a', 1), ('b', 2), ('c', 3)]) - >>> index.values() - [1, 2, 3] - - :return: list of values - - """ - return list(self.itervalues()) - - - def items(self): - """List of index items. - - >>> index = Index() - >>> index.update([('a', 1), ('b', 2), ('c', 3)]) - >>> index.items() - [('a', 1), ('b', 2), ('c', 3)] - - :return: list of items - - """ - return list(self.iteritems()) - - - def iterkeys(self): - """Iterator of index keys. - - >>> index = Index() - >>> index.update([('a', 1), ('b', 2), ('c', 3)]) - >>> list(index.iterkeys()) - ['a', 'b', 'c'] - - :return: iterator of keys - - """ - return iter(self._cache) - - - def itervalues(self): - """Iterator of index values. - - >>> index = Index() - >>> index.update([('a', 1), ('b', 2), ('c', 3)]) - >>> list(index.itervalues()) - [1, 2, 3] - - :return: iterator of values - - """ - _cache = self._cache - - for key in _cache: - while True: - try: - yield _cache[key] - except KeyError: - pass - break - - - def iteritems(self): - """Iterator of index items. - - >>> index = Index() - >>> index.update([('a', 1), ('b', 2), ('c', 3)]) - >>> list(index.iteritems()) - [('a', 1), ('b', 2), ('c', 3)] - - :return: iterator of items - - """ - _cache = self._cache - - for key in _cache: - while True: - try: - yield key, _cache[key] - except KeyError: - pass - break - - - def viewkeys(self): - """Set-like object providing a view of index keys. - - >>> index = Index() - >>> index.update({'a': 1, 'b': 2, 'c': 3}) - >>> keys_view = index.viewkeys() - >>> 'b' in keys_view - True - - :return: keys view - - """ - return KeysView(self) - - - def viewvalues(self): - """Set-like object providing a view of index values. - - >>> index = Index() - >>> index.update({'a': 1, 'b': 2, 'c': 3}) - >>> values_view = index.viewvalues() - >>> 2 in values_view - True - - :return: values view - - """ - return ValuesView(self) - - - def viewitems(self): - """Set-like object providing a view of index items. - - >>> index = Index() - >>> index.update({'a': 1, 'b': 2, 'c': 3}) - >>> items_view = index.viewitems() - >>> ('b', 2) in items_view - True - - :return: items view - - """ - return ItemsView(self) - - - else: - def keys(self): - """Set-like object providing a view of index keys. - - >>> index = Index() - >>> index.update({'a': 1, 'b': 2, 'c': 3}) - >>> keys_view = index.keys() - >>> 'b' in keys_view - True + >>> index = Index() + >>> index.update({'a': 1, 'b': 2, 'c': 3}) + >>> keys_view = index.keys() + >>> 'b' in keys_view + True - :return: keys view + :return: keys view - """ - return KeysView(self) + """ + return KeysView(self) - def values(self): - """Set-like object providing a view of index values. + def values(self): + """Set-like object providing a view of index values. - >>> index = Index() - >>> index.update({'a': 1, 'b': 2, 'c': 3}) - >>> values_view = index.values() - >>> 2 in values_view - True + >>> index = Index() + >>> index.update({'a': 1, 'b': 2, 'c': 3}) + >>> values_view = index.values() + >>> 2 in values_view + True - :return: values view + :return: values view - """ - return ValuesView(self) + """ + return ValuesView(self) - def items(self): - """Set-like object providing a view of index items. + def items(self): + """Set-like object providing a view of index items. - >>> index = Index() - >>> index.update({'a': 1, 'b': 2, 'c': 3}) - >>> items_view = index.items() - >>> ('b', 2) in items_view - True + >>> index = Index() + >>> index.update({'a': 1, 'b': 2, 'c': 3}) + >>> items_view = index.items() + >>> ('b', 2) in items_view + True - :return: items view + :return: items view - """ - return ItemsView(self) + """ + return ItemsView(self) __hash__ = None From a1bfadd2c72304cea6aa324a0b40b07b35568ed8 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 23 Aug 2020 16:23:36 -0700 Subject: [PATCH 097/211] Update tests for Python 3 --- tests/benchmark_core.py | 9 +-------- tests/benchmark_djangocache.py | 9 +-------- tests/benchmark_glob.py | 2 -- tests/benchmark_incr.py | 2 -- tests/stress_test_core.py | 17 +++-------------- tests/stress_test_deque.py | 2 -- tests/stress_test_deque_mp.py | 2 -- tests/stress_test_fanout.py | 17 +++-------------- tests/stress_test_index.py | 2 -- tests/stress_test_index_mp.py | 2 -- tests/test_core.py | 6 +----- tests/test_fanout.py | 5 ----- tests/utils.py | 2 -- 13 files changed, 9 insertions(+), 68 deletions(-) diff --git a/tests/benchmark_core.py b/tests/benchmark_core.py index 1811de0..2eeae3b 100644 --- a/tests/benchmark_core.py +++ b/tests/benchmark_core.py @@ -6,23 +6,16 @@ """ -from __future__ import print_function - import collections as co import multiprocessing as mp import os +import pickle import random import shutil import sys import time import warnings -if sys.hexversion < 0x03000000: - range = xrange - import cPickle as pickle -else: - import pickle - from utils import display PROCS = 8 diff --git a/tests/benchmark_djangocache.py b/tests/benchmark_djangocache.py index 898188f..0512752 100644 --- a/tests/benchmark_djangocache.py +++ b/tests/benchmark_djangocache.py @@ -6,23 +6,16 @@ """ -from __future__ import print_function - import collections as co import multiprocessing as mp import os +import pickle import random import shutil import sys import time import warnings -if sys.hexversion < 0x03000000: - range = xrange - import cPickle as pickle -else: - import pickle - from utils import display PROCS = 8 diff --git a/tests/benchmark_glob.py b/tests/benchmark_glob.py index 0402ef8..82237de 100644 --- a/tests/benchmark_glob.py +++ b/tests/benchmark_glob.py @@ -1,7 +1,5 @@ "Benchmark glob.glob1 as used by django.core.cache.backends.filebased." -from __future__ import print_function - import os import os.path as op import shutil diff --git a/tests/benchmark_incr.py b/tests/benchmark_incr.py index 4a01628..9c8e2fa 100644 --- a/tests/benchmark_incr.py +++ b/tests/benchmark_incr.py @@ -2,8 +2,6 @@ """ -from __future__ import print_function - import json import multiprocessing as mp import shutil diff --git a/tests/stress_test_core.py b/tests/stress_test_core.py index bce0d16..68f2ebd 100644 --- a/tests/stress_test_core.py +++ b/tests/stress_test_core.py @@ -1,11 +1,11 @@ "Stress test diskcache.core.Cache." -from __future__ import print_function - import collections as co from diskcache import Cache, UnknownFileWarning, EmptyDirWarning, Timeout import multiprocessing as mp import os +import pickle +import queue import random import shutil import sys @@ -13,17 +13,6 @@ import time import warnings -try: - import Queue -except ImportError: - import queue as Queue - -if sys.hexversion < 0x03000000: - range = xrange - import cPickle as pickle -else: - import pickle - from .utils import display OPERATIONS = int(1e4) @@ -163,7 +152,7 @@ def dispatch(num, eviction_policy, processes, threads): with open('input-%s.pkl' % num, 'rb') as reader: process_queue = pickle.load(reader) - thread_queues = [Queue.Queue() for _ in range(threads)] + thread_queues = [queue.Queue() for _ in range(threads)] subthreads = [ threading.Thread( target=worker, args=(thread_queue, eviction_policy, processes, threads) diff --git a/tests/stress_test_deque.py b/tests/stress_test_deque.py index cf48812..7b3ac2f 100644 --- a/tests/stress_test_deque.py +++ b/tests/stress_test_deque.py @@ -1,7 +1,5 @@ """Stress test diskcache.persistent.Deque.""" -from __future__ import print_function - import collections as co import functools as ft import itertools as it diff --git a/tests/stress_test_deque_mp.py b/tests/stress_test_deque_mp.py index 4624d71..db7b5c4 100644 --- a/tests/stress_test_deque_mp.py +++ b/tests/stress_test_deque_mp.py @@ -1,7 +1,5 @@ """Stress test diskcache.persistent.Deque.""" -from __future__ import print_function - import functools as ft import itertools as it import multiprocessing as mp diff --git a/tests/stress_test_fanout.py b/tests/stress_test_fanout.py index 080b8d8..422e874 100644 --- a/tests/stress_test_fanout.py +++ b/tests/stress_test_fanout.py @@ -1,11 +1,11 @@ "Stress test diskcache.core.Cache." -from __future__ import print_function - import collections as co from diskcache import FanoutCache, UnknownFileWarning, EmptyDirWarning import multiprocessing as mp import os +import pickle +import queue import random import shutil import sys @@ -13,17 +13,6 @@ import time import warnings -try: - import Queue -except ImportError: - import queue as Queue - -if sys.hexversion < 0x03000000: - range = xrange - import cPickle as pickle -else: - import pickle - from .utils import display OPERATIONS = int(1e4) @@ -155,7 +144,7 @@ def dispatch(num, eviction_policy, processes, threads): with open('input-%s.pkl' % num, 'rb') as reader: process_queue = pickle.load(reader) - thread_queues = [Queue.Queue() for _ in range(threads)] + thread_queues = [queue.Queue() for _ in range(threads)] subthreads = [ threading.Thread( target=worker, args=(thread_queue, eviction_policy, processes, threads) diff --git a/tests/stress_test_index.py b/tests/stress_test_index.py index 2846d9c..e7ba3f6 100644 --- a/tests/stress_test_index.py +++ b/tests/stress_test_index.py @@ -1,7 +1,5 @@ """Stress test diskcache.persistent.Index.""" -from __future__ import print_function - import collections as co import itertools as it import random diff --git a/tests/stress_test_index_mp.py b/tests/stress_test_index_mp.py index b3ed813..f8718f0 100644 --- a/tests/stress_test_index_mp.py +++ b/tests/stress_test_index_mp.py @@ -1,7 +1,5 @@ """Stress test diskcache.persistent.Index.""" -from __future__ import print_function - import itertools as it import multiprocessing as mp import os diff --git a/tests/test_core.py b/tests/test_core.py index 36311e5..31b4034 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -22,14 +22,10 @@ from unittest import mock -import diskcache import diskcache as dc pytestmark = pytest.mark.filterwarnings('ignore', category=dc.EmptyDirWarning) -if sys.hexversion < 0x03000000: - range = xrange - @pytest.fixture def cache(): with dc.Cache() as cache: @@ -100,7 +96,7 @@ def test_custom_disk(): shutil.rmtree(cache.directory, ignore_errors=True) -class SHA256FilenameDisk(diskcache.Disk): +class SHA256FilenameDisk(dc.Disk): def filename(self, key=dc.UNKNOWN, value=dc.UNKNOWN): filename = hashlib.sha256(key).hexdigest()[:32] full_path = op.join(self._directory, filename) diff --git a/tests/test_fanout.py b/tests/test_fanout.py index 390961b..aa3c833 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -1,7 +1,5 @@ "Test diskcache.fanout.FanoutCache." -from __future__ import print_function - import collections as co import errno import functools as ft @@ -28,9 +26,6 @@ warnings.simplefilter('error') warnings.simplefilter('ignore', category=dc.EmptyDirWarning) -if sys.hexversion < 0x03000000: - range = xrange - @pytest.fixture def cache(): diff --git a/tests/utils.py b/tests/utils.py index f2370da..47da791 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,5 +1,3 @@ -from __future__ import print_function - import os import subprocess as sp From cdeee61aa15335fd95ef923f6807cc451d05209b Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 23 Aug 2020 16:30:49 -0700 Subject: [PATCH 098/211] Bump version to 5.0.1 --- diskcache/__init__.py | 4 ++-- diskcache/persistent.py | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 62766c5..e1eb93f 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -46,8 +46,8 @@ pass __title__ = 'diskcache' -__version__ = '5.0.0' -__build__ = 0x050000 +__version__ = '5.0.1' +__build__ = 0x050001 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2020 Grant Jenks' diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 4324660..d0a452e 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -3,7 +3,6 @@ """ import operator as op -import sys from collections import OrderedDict from collections.abc import MutableMapping, Sequence From 1f339c12fec3baf6955cd6e1596a16bb6bba447b Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 25 Aug 2020 21:54:32 -0700 Subject: [PATCH 099/211] Bump version to 5.0.2 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index e1eb93f..01efa1b 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -46,8 +46,8 @@ pass __title__ = 'diskcache' -__version__ = '5.0.1' -__build__ = 0x050001 +__version__ = '5.0.2' +__build__ = 0x050002 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2020 Grant Jenks' From d6c3a4950beeca124575a5b6d039af381ec00ba0 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 8 Sep 2020 21:45:45 -0700 Subject: [PATCH 100/211] Add python_requires kwarg to setup --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index d9be963..f8c465e 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ def run_tests(self): packages=['diskcache'], tests_require=['tox'], cmdclass={'test': Tox}, + python_requires='>=3', install_requires=[], classifiers=( 'Development Status :: 5 - Production/Stable', From 9670fbb957af8caac826ddc1144da913682de447 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 8 Sep 2020 21:46:12 -0700 Subject: [PATCH 101/211] Bump version to 5.0.3 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 01efa1b..a98cfee 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -46,8 +46,8 @@ pass __title__ = 'diskcache' -__version__ = '5.0.2' -__build__ = 0x050002 +__version__ = '5.0.3' +__build__ = 0x050003 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2020 Grant Jenks' From 49205f5fc25bb0886e9dcc764ec2dcc3d8afe36d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 8 Nov 2020 19:25:54 -0800 Subject: [PATCH 102/211] Prevent cache shard attribute access when unsafe --- diskcache/fanout.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index a579a17..3b8ae30 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -58,6 +58,9 @@ def directory(self): def __getattr__(self, name): + safe_names = {'timeout', 'disk'} + valid_name = name in DEFAULT_SETTINGS or name in safe_names + assert valid_name, f'cannot access {name} in cache shard' return getattr(self._shards[0], name) From 77ca254ae2fda78550ffd678c7666d62ea0c0af1 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 8 Nov 2020 19:26:11 -0800 Subject: [PATCH 103/211] Support transactions in FanoutCache (probably a bad idea) --- diskcache/fanout.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 3b8ae30..7b85df5 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -1,5 +1,6 @@ "Fanout cache automatically shards keys and values." +import contextlib as cl import functools import itertools as it import operator @@ -64,6 +65,40 @@ def __getattr__(self, name): return getattr(self._shards[0], name) + @cl.contextmanager + def transact(self, retry=True): + """Context manager to perform a transaction by locking the cache. + + While the cache is locked, no other write operation is permitted. + Transactions should therefore be as short as possible. Read and write + operations performed in a transaction are atomic. Read operations may + occur concurrent to a transaction. + + Transactions may be nested and may not be shared between threads. + + Blocks until transactions are held on all cache shards by retrying as + necessary. + + >>> cache = FanoutCache() + >>> with cache.transact(): # Atomically increment two keys. + ... _ = cache.incr('total', 123.4) + ... _ = cache.incr('count', 1) + >>> with cache.transact(): # Atomically calculate average. + ... average = cache['total'] / cache['count'] + >>> average + 123.4 + + :return: context manager for use in `with` statement + + """ + assert retry, 'retry must be True in FanoutCache' + with cl.ExitStack() as stack: + for shard in self._shards: + shard_transaction = shard.transact(retry=True) + stack.enter_context(shard_transaction) + yield + + def set(self, key, value, expire=None, read=False, tag=None, retry=False): """Set `key` and `value` item in cache. From c5259bd6b6046789ae2bcdf1955e1781adb07bd4 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 8 Nov 2020 19:27:27 -0800 Subject: [PATCH 104/211] Bump version to 5.1.0 --- diskcache/__init__.py | 4 ++-- diskcache/fanout.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index a98cfee..e4d747b 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -46,8 +46,8 @@ pass __title__ = 'diskcache' -__version__ = '5.0.3' -__build__ = 0x050003 +__version__ = '5.1.0' +__build__ = 0x050100 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2020 Grant Jenks' diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 7b85df5..7e227c8 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -61,7 +61,7 @@ def directory(self): def __getattr__(self, name): safe_names = {'timeout', 'disk'} valid_name = name in DEFAULT_SETTINGS or name in safe_names - assert valid_name, f'cannot access {name} in cache shard' + assert valid_name, 'cannot access {} in cache shard'.format(name) return getattr(self._shards[0], name) From 40ce0dedb90ccefacaea41ac11c19ddd491d5a6d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 8 Nov 2020 20:24:02 -0800 Subject: [PATCH 105/211] Use no hardcoded /tmp/diskcache/... paths in tests --- tests/test_deque.py | 5 +++-- tests/test_index.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/test_deque.py b/tests/test_deque.py index ddf2338..1ca966d 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -4,6 +4,7 @@ import pickle import pytest import shutil +import tempfile from unittest import mock @@ -26,7 +27,7 @@ def deque(): def test_init(): - directory = '/tmp/diskcache/deque' + directory = tempfile.mkdtemp() sequence = list('abcde') deque = dc.Deque(sequence, None) @@ -156,7 +157,7 @@ def test_indexerror(deque): def test_repr(): - directory = '/tmp/diskcache/deque' + directory = tempfile.mkdtemp() deque = dc.Deque(directory=directory) assert repr(deque) == 'Deque(directory=%r)' % directory diff --git a/tests/test_index.py b/tests/test_index.py index ef2a0d0..575558c 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -5,6 +5,7 @@ import pytest import shutil import sys +import tempfile from unittest import mock @@ -26,7 +27,7 @@ def index(): def test_init(): - directory = '/tmp/diskcache/index' + directory = tempfile.mkdtemp() mapping = {'a': 5, 'b': 4, 'c': 3, 'd': 2, 'e': 1} index = dc.Index(None, mapping) From c4ba1f78bb8494bcf6aba9d7d1c3aa49a1093508 Mon Sep 17 00:00:00 2001 From: Cologler Date: Sat, 12 Dec 2020 00:36:01 +0800 Subject: [PATCH 106/211] replace open mode 'w' to 'x' --- diskcache/core.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 4317160..1fae11b 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -207,14 +207,14 @@ def store(self, value, read, key=UNKNOWN): else: filename, full_path = self.filename(key, value) - with open(full_path, 'wb') as writer: + with open(full_path, 'xb') as writer: writer.write(value) return len(value), MODE_BINARY, filename, None elif type_value is str: filename, full_path = self.filename(key, value) - with open(full_path, 'w', encoding='UTF-8') as writer: + with open(full_path, 'x', encoding='UTF-8') as writer: writer.write(value) size = op.getsize(full_path) @@ -224,7 +224,7 @@ def store(self, value, read, key=UNKNOWN): reader = ft.partial(value.read, 2 ** 22) filename, full_path = self.filename(key, value) - with open(full_path, 'wb') as writer: + with open(full_path, 'xb') as writer: for chunk in iter(reader, b''): size += len(chunk) writer.write(chunk) @@ -238,7 +238,7 @@ def store(self, value, read, key=UNKNOWN): else: filename, full_path = self.filename(key, value) - with open(full_path, 'wb') as writer: + with open(full_path, 'xb') as writer: writer.write(result) return len(result), MODE_PICKLE, filename, None From ce44a42cfe01ac48e167efeff2612a6e768fdf55 Mon Sep 17 00:00:00 2001 From: C2D <50617709+i404788@users.noreply.github.com> Date: Wed, 20 Jan 2021 16:34:02 +0000 Subject: [PATCH 107/211] Use disk provided by the user whenever possible --- diskcache/fanout.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 7e227c8..b39c744 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -36,6 +36,7 @@ def __init__(self, directory=None, shards=8, timeout=0.010, disk=Disk, self._count = shards self._directory = directory + self._disk = disk self._shards = tuple( Cache( directory=op.join(directory, '%03d' % num), @@ -622,7 +623,7 @@ def cache(self, name): except KeyError: parts = name.split('/') directory = op.join(self._directory, 'cache', *parts) - temp = Cache(directory=directory) + temp = Cache(directory=directory, disk=self._disk) _caches[name] = temp return temp From 9a300bd4ab4896cfc4eb90703d155d84924be447 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 20 Jan 2021 20:27:27 -0800 Subject: [PATCH 108/211] Remove transaction from Deque.__init__ When initializing a Deque, a transaction was used to extend elements from the given iterable. The transaction is not used in Index.__init__ or in the FanoutCache.fromcache API. Users that want Deque.__init__ to use a transaction as before should use: d = Deque() with d.transact(): d.extend(iterable) The transaction is therefore explicit and consistent with other APIs. --- diskcache/persistent.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/diskcache/persistent.py b/diskcache/persistent.py index d0a452e..6fc072b 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -81,8 +81,7 @@ def __init__(self, iterable=(), directory=None): """ self._cache = Cache(directory, eviction_policy='none') - with self.transact(): - self.extend(iterable) + self.extend(iterable) @classmethod From 8b0f68b8c208ddd5a6c06e5c6a9d55ef37f5fb1a Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 20 Jan 2021 20:37:55 -0800 Subject: [PATCH 109/211] Use the same Disk in FanoutCache as in Index and Deque subdirs --- diskcache/fanout.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index b39c744..9870c06 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -652,9 +652,10 @@ def deque(self, name): except KeyError: parts = name.split('/') directory = op.join(self._directory, 'deque', *parts) - temp = Deque(directory=directory) - _deques[name] = temp - return temp + cache = Cache(directory=directory, disk=self._disk) + deque = Deque.fromcache(cache) + _deques[name] = deque + return deque def index(self, name): @@ -684,9 +685,10 @@ def index(self, name): except KeyError: parts = name.split('/') directory = op.join(self._directory, 'index', *parts) - temp = Index(directory) - _indexes[name] = temp - return temp + cache = Cache(directory=directory, disk=self._disk) + index = Index.fromcache(cache) + _indexes[name] = index + return index FanoutCache.memoize = Cache.memoize From fcd31646cf81b6762d1d31cffafb8fd97510f024 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 10:29:07 -0800 Subject: [PATCH 110/211] Remove travis and appveyor in favor of GitHub Actions --- .github/workflows/integration.yml | 52 ++++++++++++++++++ .github/workflows/release.yml | 38 +++++++++++++ .travis.yml | 19 ------- appveyor.yml | 22 -------- tox.ini | 88 ++++++++++++++++++++++++------- 5 files changed, 159 insertions(+), 60 deletions(-) create mode 100644 .github/workflows/integration.yml create mode 100644 .github/workflows/release.yml delete mode 100644 .travis.yml delete mode 100644 appveyor.yml diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml new file mode 100644 index 0000000..7dfc198 --- /dev/null +++ b/.github/workflows/integration.yml @@ -0,0 +1,52 @@ +name: integration + +on: [push] + +jobs: + + checks: + runs-on: ubuntu-latest + strategy: + max-parallel: 8 + matrix: + check: [bluecheck, doc8, docs, flake8, isortcheck, mypy, pylint, rstcheck] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + - name: Install dependencies + run: | + pip install --upgrade pip + pip install tox + - name: Run checks with tox + run: | + tox -e ${{ matrix.check }} + + tests: + needs: checks + runs-on: ${{ matrix.os }} + strategy: + max-parallel: 8 + matrix: + os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-16.04] + python-version: [3.6, 3.7, 3.8, 3.9] + + steps: + - name: Set up Python ${{ matrix.python-version }} x64 + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + architecture: x64 + + - uses: actions/checkout@v2 + + - name: Install tox + run: | + pip install --upgrade pip + pip install tox + + - name: Test with tox + run: tox -e py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..57c68e5 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,38 @@ +name: release + +on: + push: + tags: + - v* + +jobs: + + upload: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + + - name: Install dependencies + run: | + pip install --upgrade pip + pip install -r requirements.txt + + - name: Create source dist + run: python setup.py sdist + + - name: Create wheel dist + run: python setup.py bdist_wheel + + - name: Upload with twine + env: + TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} + TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} + run: | + ls -l dist/* + twine upload dist/* diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 5e5c760..0000000 --- a/.travis.yml +++ /dev/null @@ -1,19 +0,0 @@ -sudo: false -language: python -install: python -m pip install tox -script: python -m tox -e py -matrix: - include: - - python: 3.5 - env: TOXENV=py35 - - python: 3.6 - env: TOXENV=py36 - - python: 3.7 - dist: xenial - env: TOXENV=py37 - - python: 3.8 - dist: xenial - env: TOXENV=py38 - - python: 3.8 - dist: xenial - env: TOXENV=pylint diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 1f52a08..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,22 +0,0 @@ -environment: - - matrix: - - - PYTHON: "C:\\Python35" - - PYTHON: "C:\\Python36" - - PYTHON: "C:\\Python37" - - PYTHON: "C:\\Python38" - - PYTHON: "C:\\Python35-x64" - - PYTHON: "C:\\Python36-x64" - - PYTHON: "C:\\Python37-x64" - - PYTHON: "C:\\Python38-x64" - -install: - - - "%PYTHON%\\python.exe -m pip install tox" - -build: off - -test_script: - - - "%PYTHON%\\python.exe -m tox -e py" diff --git a/tox.ini b/tox.ini index b54e574..6bbe69c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,43 +1,93 @@ [tox] -envlist=py35,py36,py37,py38,pylint +envlist=bluecheck,doc8,docs,isortcheck,flake8,mypy,pylint,rstcheck,py36,py37,py38,py39 skip_missing_interpreters=True [testenv] +commands=pytest deps= django==2.2.* pytest pytest-django pytest-xdist -commands=python -m pytest setenv= DJANGO_SETTINGS_MODULE=tests.settings - PYTHONPATH={toxinidir} + +[testenv:blue] +commands=blue {toxinidir}/setup.py {toxinidir}/diskcache {toxinidir}/tests +deps=blue + +[testenv:bluecheck] +commands=blue --check {toxinidir}/setup.py {toxinidir}/diskcache {toxinidir}/tests +deps=blue + +[testenv:doc8] +deps=doc8 +commands=doc8 docs + +[testenv:docs] +allowlist_externals=make +changedir=docs +commands=make html +deps=sphinx + +[testenv:flake8] +commands=flake8 {toxinidir}/setup.py {toxinidir}/diskcache {toxinidir}/tests +deps=flake8 + +[testenv:isort] +commands=isort {toxinidir}/setup.py {toxinidir}/diskcache {toxinidir}/tests +deps=isort + +[testenv:isortcheck] +commands=isort --check {toxinidir}/setup.py {toxinidir}/diskcache {toxinidir}/tests +deps=isort + +[testenv:mypy] +commands=mypy {toxinidir}/diskcache +deps=mypy + +[testenv:pylint] +commands=pylint {toxinidir}/diskcache +deps=pylint + +[testenv:rstcheck] +commands=rstcheck {toxinidir}/README.rst +deps=rstcheck + +[testenv:uploaddocs] +allowlist_externals=rsync +changedir=docs +commands= + rsync -azP --stats --delete _build/html/ \ + grantjenks.com:/srv/www/www.grantjenks.com/public/docs/diskcache/ + +[isort] +multi_line_output = 3 +include_trailing_comma = True +force_grid_wrap = 0 +use_parentheses = True +ensure_newline_before_comments = True +line_length = 79 [pytest] addopts= -n auto + --cov-branch + --cov-fail-under=100 + --cov-report=term-missing + --cov=diskcache + --doctest-glob="*.rst" --ignore tests/benchmark_core.py --ignore tests/benchmark_djangocache.py --ignore tests/benchmark_glob.py --ignore tests/issue_85.py --ignore tests/plot.py -norecursedirs=site-packages -testpaths=docs diskcache tests - -[testenv:pylint] -deps= - django==2.2.* - flake8 - pylint -commands= - flake8 diskcache - pylint diskcache [doc8] -ignore=D000 +# ignore=D000 [flake8] -ignore= - E124 - E303 - W503 +# ignore= +# E124 +# E303 +# W503 From 8d2009abd1e92875a31b21bdf2799ea757fb6b24 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:19:45 -0800 Subject: [PATCH 111/211] Rewrite k/v store benchmarking to avoid IPython "magic" syntax --- tests/benchmark_kv_store.py | 110 +++++++++++++++++++++++------------- 1 file changed, 70 insertions(+), 40 deletions(-) diff --git a/tests/benchmark_kv_store.py b/tests/benchmark_kv_store.py index d1459e7..e214f1a 100644 --- a/tests/benchmark_kv_store.py +++ b/tests/benchmark_kv_store.py @@ -1,48 +1,78 @@ -import dbm +"""Benchmarking Key-Value Stores + +$ python -m IPython tests/benchmark_kv_store.py + +""" + import diskcache -import pickledb -import shelve -import sqlitedict -import timeit + +from IPython import get_ipython + +ipython = get_ipython() +assert ipython is not None, 'No IPython! Run with $ ipython ...' value = 'value' print('diskcache set') dc = diskcache.FanoutCache('/tmp/diskcache') -%timeit -n 100 -r 7 dc['key'] = value +ipython.magic("timeit -n 100 -r 7 dc['key'] = value") print('diskcache get') -%timeit -n 100 -r 7 dc['key'] +ipython.magic("timeit -n 100 -r 7 dc['key']") print('diskcache set/delete') -%timeit -n 100 -r 7 dc['key'] = value; del dc['key'] - -print('dbm set') -d = dbm.open('/tmp/dbm', 'c') -%timeit -n 100 -r 7 d['key'] = value; d.sync() -print('dbm get') -%timeit -n 100 -r 7 d['key'] -print('dbm set/delete') -%timeit -n 100 -r 7 d['key'] = value; del d['key']; d.sync() - -print('shelve set') -s = shelve.open('/tmp/shelve') -%timeit -n 100 -r 7 s['key'] = value; s.sync() -print('shelve get') -%timeit -n 100 -r 7 s['key'] -print('shelve set/delete') -%timeit -n 100 -r 7 s['key'] = value; del s['key']; s.sync() - -print('sqlitedict set') -sd = sqlitedict.SqliteDict('/tmp/sqlitedict', autocommit=True) -%timeit -n 100 -r 7 sd['key'] = value -print('sqlitedict get') -%timeit -n 100 -r 7 sd['key'] -print('sqlitedict set/delete') -%timeit -n 100 -r 7 sd['key'] = value; del sd['key'] - -print('pickledb set') -p = pickledb.load('/tmp/pickledb', True) -%timeit -n 100 -r 7 p['key'] = value -print('pickledb get') -%timeit -n 100 -r 7 p = pickledb.load('/tmp/pickledb', True); p['key'] -print('pickledb set/delete') -%timeit -n 100 -r 7 p['key'] = value; del p['key'] +ipython.magic("timeit -n 100 -r 7 dc['key'] = value; del dc['key']") + +try: + import dbm.gnu # Only trust GNU DBM +except ImportError: + print('Error: Cannot import dbm.gnu') + print('Error: Skipping import shelve') +else: + print('dbm set') + d = dbm.gnu.open('/tmp/dbm', 'c') + ipython.magic("timeit -n 100 -r 7 d['key'] = value; d.sync()") + print('dbm get') + ipython.magic("timeit -n 100 -r 7 d['key']") + print('dbm set/delete') + ipython.magic( + "timeit -n 100 -r 7 d['key'] = value; d.sync(); del d['key']; d.sync()" + ) + + import shelve + + print('shelve set') + s = shelve.open('/tmp/shelve') + ipython.magic("timeit -n 100 -r 7 s['key'] = value; s.sync()") + print('shelve get') + ipython.magic("timeit -n 100 -r 7 s['key']") + print('shelve set/delete') + ipython.magic( + "timeit -n 100 -r 7 s['key'] = value; s.sync(); del s['key']; s.sync()" + ) + +try: + import sqlitedict +except ImportError: + print('Error: Cannot import sqlitedict') +else: + print('sqlitedict set') + sd = sqlitedict.SqliteDict('/tmp/sqlitedict', autocommit=True) + ipython.magic("timeit -n 100 -r 7 sd['key'] = value") + print('sqlitedict get') + ipython.magic("timeit -n 100 -r 7 sd['key']") + print('sqlitedict set/delete') + ipython.magic("timeit -n 100 -r 7 sd['key'] = value; del sd['key']") + +try: + import pickledb +except ImportError: + print('Error: Cannot import pickledb') +else: + print('pickledb set') + p = pickledb.load('/tmp/pickledb', True) + ipython.magic("timeit -n 100 -r 7 p['key'] = value") + print('pickledb get') + ipython.magic( + "timeit -n 100 -r 7 p = pickledb.load('/tmp/pickledb', True); p['key']" + ) + print('pickledb set/delete') + ipython.magic("timeit -n 100 -r 7 p['key'] = value; del p['key']") From b7ecbe9e9d9d51d78b49058c6bea68400ff1ecf5 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:23:00 -0800 Subject: [PATCH 112/211] Update development requirements for editable install --- requirements.txt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 835f48a..a9023a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,8 +1,12 @@ +-e . +blue coverage django==2.2.* django_redis doc8 -gj +flake8 +ipython +pickleDB pylibmc pylint pytest @@ -12,6 +16,7 @@ pytest-env pytest-xdist rstcheck sphinx +sqlitedict tox twine wheel From 2ed7787817d1a7587dba077e972faea16fb6f5d8 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:25:53 -0800 Subject: [PATCH 113/211] I blue it --- diskcache/__init__.py | 8 +- diskcache/core.py | 323 +++++++++++++++++---------------- diskcache/djangocache.py | 89 +++++---- diskcache/fanout.py | 59 ++---- diskcache/persistent.py | 62 +------ diskcache/recipes.py | 63 +++++-- setup.py | 2 + tests/benchmark_core.py | 90 ++++++--- tests/benchmark_djangocache.py | 23 ++- tests/benchmark_glob.py | 6 +- tests/issue_109.py | 3 +- tests/issue_85.py | 1 + tests/models.py | 4 +- tests/plot.py | 23 ++- tests/plot_early_recompute.py | 8 + tests/settings_benchmark.py | 6 +- tests/stress_test_core.py | 116 +++++++++--- tests/stress_test_fanout.py | 116 +++++++++--- tests/test_core.py | 12 +- tests/test_djangocache.py | 245 ++++++++++++++++--------- tests/test_recipes.py | 6 + tests/utils.py | 23 ++- 22 files changed, 787 insertions(+), 501 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index e4d747b..0300123 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -7,7 +7,12 @@ """ from .core import ( - Cache, Disk, EmptyDirWarning, JSONDisk, UnknownFileWarning, Timeout + Cache, + Disk, + EmptyDirWarning, + JSONDisk, + UnknownFileWarning, + Timeout, ) from .core import DEFAULT_SETTINGS, ENOVAL, EVICTION_POLICY, UNKNOWN from .fanout import FanoutCache @@ -40,6 +45,7 @@ try: from .djangocache import DjangoCache # noqa + __all__.append('DjangoCache') except Exception: # pylint: disable=broad-except # Django not installed or not setup so ignore. diff --git a/diskcache/core.py b/diskcache/core.py index 1fae11b..419c64f 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -29,12 +29,14 @@ def full_name(func): try: WindowsError except NameError: + class WindowsError(Exception): "Windows error place-holder on platforms without support." class Constant(tuple): "Pretty display of immutable constant." + def __new__(cls, name): return tuple.__new__(cls, (name,)) @@ -54,15 +56,15 @@ def __repr__(self): DEFAULT_SETTINGS = { u'statistics': 0, # False - u'tag_index': 0, # False + u'tag_index': 0, # False u'eviction_policy': u'least-recently-stored', u'size_limit': 2 ** 30, # 1gb u'cull_limit': 10, - u'sqlite_auto_vacuum': 1, # FULL - u'sqlite_cache_size': 2 ** 13, # 8,192 pages + u'sqlite_auto_vacuum': 1, # FULL + u'sqlite_cache_size': 2 ** 13, # 8,192 pages u'sqlite_journal_mode': u'wal', - u'sqlite_mmap_size': 2 ** 26, # 64mb - u'sqlite_synchronous': 1, # NORMAL + u'sqlite_mmap_size': 2 ** 26, # 64mb + u'sqlite_synchronous': 1, # NORMAL u'disk_min_file_size': 2 ** 15, # 32kb u'disk_pickle_protocol': pickle.HIGHEST_PROTOCOL, } @@ -109,6 +111,7 @@ def __repr__(self): class Disk(object): "Cache key and value serialization for SQLite database and files." + def __init__(self, directory, min_file_size=0, pickle_protocol=0): """Initialize disk instance. @@ -121,7 +124,6 @@ def __init__(self, directory, min_file_size=0, pickle_protocol=0): self.min_file_size = min_file_size self.pickle_protocol = pickle_protocol - def hash(self, key): """Compute portable hash for `key`. @@ -143,7 +145,6 @@ def hash(self, key): assert type_disk_key is float return zlib.adler32(struct.pack('!d', disk_key)) & mask - def put(self, key): """Convert `key` to fields key and raw for Cache table. @@ -156,17 +157,20 @@ def put(self, key): if type_key is bytes: return sqlite3.Binary(key), True - elif ((type_key is str) - or (type_key is int - and -9223372036854775808 <= key <= 9223372036854775807) - or (type_key is float)): + elif ( + (type_key is str) + or ( + type_key is int + and -9223372036854775808 <= key <= 9223372036854775807 + ) + or (type_key is float) + ): return key, True else: data = pickle.dumps(key, protocol=self.pickle_protocol) result = pickletools.optimize(data) return sqlite3.Binary(result), False - def get(self, key, raw): """Convert fields `key` and `raw` from Cache table to key. @@ -181,7 +185,6 @@ def get(self, key, raw): else: return pickle.load(io.BytesIO(key)) - def store(self, value, read, key=UNKNOWN): """Convert `value` to fields size, mode, filename, and value for Cache table. @@ -196,10 +199,14 @@ def store(self, value, read, key=UNKNOWN): type_value = type(value) min_file_size = self.min_file_size - if ((type_value is str and len(value) < min_file_size) - or (type_value is int - and -9223372036854775808 <= value <= 9223372036854775807) - or (type_value is float)): + if ( + (type_value is str and len(value) < min_file_size) + or ( + type_value is int + and -9223372036854775808 <= value <= 9223372036854775807 + ) + or (type_value is float) + ): return 0, MODE_RAW, None, value elif type_value is bytes: if len(value) < min_file_size: @@ -243,7 +250,6 @@ def store(self, value, read, key=UNKNOWN): return len(result), MODE_PICKLE, filename, None - def fetch(self, mode, filename, value, read): """Convert fields `mode`, `filename`, and `value` from Cache table to value. @@ -275,7 +281,6 @@ def fetch(self, mode, filename, value, read): else: return pickle.load(io.BytesIO(value)) - def filename(self, key=UNKNOWN, value=UNKNOWN): """Return filename and full-path tuple for file storage. @@ -310,7 +315,6 @@ def filename(self, key=UNKNOWN, value=UNKNOWN): full_path = op.join(self._directory, filename) return filename, full_path - def remove(self, filename): """Remove a file given by `filename`. @@ -335,6 +339,7 @@ def remove(self, filename): class JSONDisk(Disk): "Cache key and value using JSON serialization with zlib compression." + def __init__(self, directory, compress_level=1, **kwargs): """Initialize JSON disk instance. @@ -351,25 +356,21 @@ def __init__(self, directory, compress_level=1, **kwargs): self.compress_level = compress_level super().__init__(directory, **kwargs) - def put(self, key): json_bytes = json.dumps(key).encode('utf-8') data = zlib.compress(json_bytes, self.compress_level) return super().put(data) - def get(self, key, raw): data = super().get(key, raw) return json.loads(zlib.decompress(data).decode('utf-8')) - def store(self, value, read, key=UNKNOWN): if not read: json_bytes = json.dumps(value).encode('utf-8') value = zlib.compress(json_bytes, self.compress_level) return super().store(value, read, key=key) - def fetch(self, mode, filename, value, read): data = super().fetch(mode, filename, value, read) if not read: @@ -419,6 +420,7 @@ def args_to_key(base, args, kwargs, typed): class Cache(object): "Disk and file backed cache." + def __init__(self, directory=None, timeout=60, disk=Disk, **settings): """Initialize cache instance. @@ -451,7 +453,7 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): raise EnvironmentError( error.errno, 'Cache directory "%s" does not exist' - ' and could not be created' % self._directory + ' and could not be created' % self._directory, ) from None sql = self._sql_retry @@ -459,9 +461,9 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): # Setup Settings table. try: - current_settings = dict(sql( - 'SELECT key, value FROM Settings' - ).fetchall()) + current_settings = dict( + sql('SELECT key, value FROM Settings').fetchall() + ) except sqlite3.OperationalError: current_settings = {} @@ -478,7 +480,8 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): if key.startswith('sqlite_'): self.reset(key, value, update=False) - sql('CREATE TABLE IF NOT EXISTS Settings (' + sql( + 'CREATE TABLE IF NOT EXISTS Settings (' ' key TEXT NOT NULL UNIQUE,' ' value)' ) @@ -486,7 +489,8 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): # Setup Disk object (must happen after settings initialized). kwargs = { - key[5:]: value for key, value in sets.items() + key[5:]: value + for key, value in sets.items() if key.startswith('disk_') } self._disk = disk(directory, **kwargs) @@ -503,11 +507,12 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): sql(query, (key, value)) self.reset(key) - (self._page_size,), = sql('PRAGMA page_size').fetchall() + ((self._page_size,),) = sql('PRAGMA page_size').fetchall() # Setup Cache table. - sql('CREATE TABLE IF NOT EXISTS Cache (' + sql( + 'CREATE TABLE IF NOT EXISTS Cache (' ' rowid INTEGER PRIMARY KEY,' ' key BLOB,' ' raw INTEGER,' @@ -522,11 +527,13 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): ' value BLOB)' ) - sql('CREATE UNIQUE INDEX IF NOT EXISTS Cache_key_raw ON' + sql( + 'CREATE UNIQUE INDEX IF NOT EXISTS Cache_key_raw ON' ' Cache(key, raw)' ) - sql('CREATE INDEX IF NOT EXISTS Cache_expire_time ON' + sql( + 'CREATE INDEX IF NOT EXISTS Cache_expire_time ON' ' Cache (expire_time)' ) @@ -537,32 +544,37 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): # Use triggers to keep Metadata updated. - sql('CREATE TRIGGER IF NOT EXISTS Settings_count_insert' + sql( + 'CREATE TRIGGER IF NOT EXISTS Settings_count_insert' ' AFTER INSERT ON Cache FOR EACH ROW BEGIN' ' UPDATE Settings SET value = value + 1' ' WHERE key = "count"; END' ) - sql('CREATE TRIGGER IF NOT EXISTS Settings_count_delete' + sql( + 'CREATE TRIGGER IF NOT EXISTS Settings_count_delete' ' AFTER DELETE ON Cache FOR EACH ROW BEGIN' ' UPDATE Settings SET value = value - 1' ' WHERE key = "count"; END' ) - sql('CREATE TRIGGER IF NOT EXISTS Settings_size_insert' + sql( + 'CREATE TRIGGER IF NOT EXISTS Settings_size_insert' ' AFTER INSERT ON Cache FOR EACH ROW BEGIN' ' UPDATE Settings SET value = value + NEW.size' ' WHERE key = "size"; END' ) - sql('CREATE TRIGGER IF NOT EXISTS Settings_size_update' + sql( + 'CREATE TRIGGER IF NOT EXISTS Settings_size_update' ' AFTER UPDATE ON Cache FOR EACH ROW BEGIN' ' UPDATE Settings' ' SET value = value + NEW.size - OLD.size' ' WHERE key = "size"; END' ) - sql('CREATE TRIGGER IF NOT EXISTS Settings_size_delete' + sql( + 'CREATE TRIGGER IF NOT EXISTS Settings_size_delete' ' AFTER DELETE ON Cache FOR EACH ROW BEGIN' ' UPDATE Settings SET value = value - OLD.size' ' WHERE key = "size"; END' @@ -581,25 +593,21 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): self._timeout = timeout self._sql # pylint: disable=pointless-statement - @property def directory(self): """Cache directory.""" return self._directory - @property def timeout(self): """SQLite connection timeout value in seconds.""" return self._timeout - @property def disk(self): """Disk used for serialization.""" return self._disk - @property def _con(self): # Check process ID to support process forking. If the process @@ -638,12 +646,10 @@ def _con(self): return con - @property def _sql(self): return self._con.execute - @property def _sql_retry(self): sql = self._sql @@ -671,7 +677,6 @@ def _execute_with_retry(statement, *args, **kwargs): return _execute_with_retry - @cl.contextmanager def transact(self, retry=False): """Context manager to perform a transaction by locking the cache. @@ -703,7 +708,6 @@ def transact(self, retry=False): with self._transact(retry=retry): yield - @cl.contextmanager def _transact(self, retry=False, filename=None): sql = self._sql @@ -745,7 +749,6 @@ def _transact(self, retry=False, filename=None): if name is not None: _disk_remove(name) - def set(self, key, value, expire=None, read=False, tag=None, retry=False): """Set `key` and `value` item in cache. @@ -801,7 +804,7 @@ def set(self, key, value, expire=None, read=False, tag=None, retry=False): ).fetchall() if rows: - (rowid, old_filename), = rows + ((rowid, old_filename),) = rows cleanup(old_filename) self._row_update(rowid, now, columns) else: @@ -811,7 +814,6 @@ def set(self, key, value, expire=None, read=False, tag=None, retry=False): return True - def __setitem__(self, key, value): """Set corresponding `value` for `key` in cache. @@ -823,11 +825,11 @@ def __setitem__(self, key, value): """ self.set(key, value, retry=True) - def _row_update(self, rowid, now, columns): sql = self._sql expire_time, tag, size, mode, filename, value = columns - sql('UPDATE Cache SET' + sql( + 'UPDATE Cache SET' ' store_time = ?,' ' expire_time = ?,' ' access_time = ?,' @@ -837,11 +839,12 @@ def _row_update(self, rowid, now, columns): ' mode = ?,' ' filename = ?,' ' value = ?' - ' WHERE rowid = ?', ( - now, # store_time + ' WHERE rowid = ?', + ( + now, # store_time expire_time, - now, # access_time - 0, # access_count + now, # access_time + 0, # access_count tag, size, mode, @@ -851,20 +854,21 @@ def _row_update(self, rowid, now, columns): ), ) - def _row_insert(self, key, raw, now, columns): sql = self._sql expire_time, tag, size, mode, filename, value = columns - sql('INSERT INTO Cache(' + sql( + 'INSERT INTO Cache(' ' key, raw, store_time, expire_time, access_time,' ' access_count, tag, size, mode, filename, value' - ') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', ( + ') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)', + ( key, raw, - now, # store_time + now, # store_time expire_time, - now, # access_time - 0, # access_count + now, # access_time + 0, # access_count tag, size, mode, @@ -873,7 +877,6 @@ def _row_insert(self, key, raw, now, columns): ), ) - def _cull(self, now, sql, cleanup, limit=None): cull_limit = self.cull_limit if limit is None else limit @@ -892,13 +895,12 @@ def _cull(self, now, sql, cleanup, limit=None): rows = sql(select_expired, (now, cull_limit)).fetchall() if rows: - delete_expired = ( - 'DELETE FROM Cache WHERE rowid IN (%s)' - % (select_expired_template % 'rowid') + delete_expired = 'DELETE FROM Cache WHERE rowid IN (%s)' % ( + select_expired_template % 'rowid' ) sql(delete_expired, (now, cull_limit)) - for filename, in rows: + for (filename,) in rows: cleanup(filename) cull_limit -= len(rows) @@ -917,16 +919,14 @@ def _cull(self, now, sql, cleanup, limit=None): rows = sql(select_filename, (cull_limit,)).fetchall() if rows: - delete = ( - 'DELETE FROM Cache WHERE rowid IN (%s)' - % (select_policy.format(fields='rowid', now=now)) + delete = 'DELETE FROM Cache WHERE rowid IN (%s)' % ( + select_policy.format(fields='rowid', now=now) ) sql(delete, (cull_limit,)) - for filename, in rows: + for (filename,) in rows: cleanup(filename) - def touch(self, key, expire=None, retry=False): """Touch `key` in cache and update `expire` time. @@ -953,17 +953,17 @@ def touch(self, key, expire=None, retry=False): ).fetchall() if rows: - (rowid, old_expire_time), = rows + ((rowid, old_expire_time),) = rows if old_expire_time is None or old_expire_time > now: - sql('UPDATE Cache SET expire_time = ? WHERE rowid = ?', + sql( + 'UPDATE Cache SET expire_time = ? WHERE rowid = ?', (expire_time, rowid), ) return True return False - def add(self, key, value, expire=None, read=False, tag=None, retry=False): """Add `key` and `value` item to cache. @@ -1003,7 +1003,7 @@ def add(self, key, value, expire=None, read=False, tag=None, retry=False): ).fetchall() if rows: - (rowid, old_filename, old_expire_time), = rows + ((rowid, old_filename, old_expire_time),) = rows if old_expire_time is None or old_expire_time > now: cleanup(filename) @@ -1018,7 +1018,6 @@ def add(self, key, value, expire=None, read=False, tag=None, retry=False): return True - def incr(self, key, delta=1, default=0, retry=False): """Increment value by delta for item with key. @@ -1059,22 +1058,22 @@ def incr(self, key, delta=1, default=0, retry=False): raise KeyError(key) value = default + delta - columns = ( - (None, None) + self._disk.store(value, False, key=key) + columns = (None, None) + self._disk.store( + value, False, key=key ) self._row_insert(db_key, raw, now, columns) self._cull(now, sql, cleanup) return value - (rowid, expire_time, filename, value), = rows + ((rowid, expire_time, filename, value),) = rows if expire_time is not None and expire_time < now: if default is None: raise KeyError(key) value = default + delta - columns = ( - (None, None) + self._disk.store(value, False, key=key) + columns = (None, None) + self._disk.store( + value, False, key=key ) self._row_update(rowid, now, columns) self._cull(now, sql, cleanup) @@ -1094,7 +1093,6 @@ def incr(self, key, delta=1, default=0, retry=False): return value - def decr(self, key, delta=1, default=0, retry=False): """Decrement value by delta for item with key. @@ -1125,9 +1123,15 @@ def decr(self, key, delta=1, default=0, retry=False): """ return self.incr(key, -delta, default, retry) - - def get(self, key, default=None, read=False, expire_time=False, tag=False, - retry=False): + def get( + self, + key, + default=None, + read=False, + expire_time=False, + tag=False, + retry=False, + ): """Retrieve value from cache. If `key` is missing, return `default`. Raises :exc:`Timeout` error when database timeout occurs and `retry` is @@ -1166,7 +1170,7 @@ def get(self, key, default=None, read=False, expire_time=False, tag=False, if not rows: return default - (rowid, db_expire_time, db_tag, mode, filename, db_value), = rows + ((rowid, db_expire_time, db_tag, mode, filename, db_value),) = rows try: value = self._disk.fetch(mode, filename, db_value, read) @@ -1190,8 +1194,9 @@ def get(self, key, default=None, read=False, expire_time=False, tag=False, sql(cache_miss) return default - (rowid, db_expire_time, db_tag, - mode, filename, db_value), = rows # noqa: E127 + ( + (rowid, db_expire_time, db_tag, mode, filename, db_value), + ) = rows # noqa: E127 try: value = self._disk.fetch(mode, filename, db_value, read) @@ -1222,7 +1227,6 @@ def get(self, key, default=None, read=False, expire_time=False, tag=False, else: return value - def __getitem__(self, key): """Return corresponding value for `key` from cache. @@ -1236,7 +1240,6 @@ def __getitem__(self, key): raise KeyError(key) return value - def read(self, key, retry=False): """Return file handle value corresponding to `key` from cache. @@ -1255,7 +1258,6 @@ def read(self, key, retry=False): raise KeyError(key) return handle - def __contains__(self, key): """Return `True` if `key` matching item is found in cache. @@ -1275,8 +1277,9 @@ def __contains__(self, key): return bool(rows) - - def pop(self, key, default=None, expire_time=False, tag=False, retry=False): # noqa: E501 + def pop( + self, key, default=None, expire_time=False, tag=False, retry=False + ): # noqa: E501 """Remove corresponding item for `key` from cache and return value. If `key` is missing, return `default`. @@ -1314,7 +1317,7 @@ def pop(self, key, default=None, expire_time=False, tag=False, retry=False): # if not rows: return default - (rowid, db_expire_time, db_tag, mode, filename, db_value), = rows + ((rowid, db_expire_time, db_tag, mode, filename, db_value),) = rows sql('DELETE FROM Cache WHERE rowid = ?', (rowid,)) @@ -1339,7 +1342,6 @@ def pop(self, key, default=None, expire_time=False, tag=False, retry=False): # else: return value - def __delitem__(self, key, retry=True): """Delete corresponding item for `key` from cache. @@ -1365,13 +1367,12 @@ def __delitem__(self, key, retry=True): if not rows: raise KeyError(key) - (rowid, filename), = rows + ((rowid, filename),) = rows sql('DELETE FROM Cache WHERE rowid = ?', (rowid,)) cleanup(filename) return True - def delete(self, key, retry=False): """Delete corresponding item for `key` from cache. @@ -1391,9 +1392,16 @@ def delete(self, key, retry=False): except KeyError: return False - - def push(self, value, prefix=None, side='back', expire=None, read=False, - tag=None, retry=False): + def push( + self, + value, + prefix=None, + side='back', + expire=None, + read=False, + tag=None, + retry=False, + ): """Push `value` onto `side` of queue identified by `prefix` in cache. When prefix is None, integer keys are used. Otherwise, string keys are @@ -1459,10 +1467,10 @@ def push(self, value, prefix=None, side='back', expire=None, read=False, rows = sql(select, (min_key, max_key, raw)).fetchall() if rows: - (key,), = rows + ((key,),) = rows if prefix is not None: - num = int(key[(key.rfind('-') + 1):]) + num = int(key[(key.rfind('-') + 1) :]) else: num = key @@ -1484,9 +1492,15 @@ def push(self, value, prefix=None, side='back', expire=None, read=False, return db_key - - def pull(self, prefix=None, default=(None, None), side='front', - expire_time=False, tag=False, retry=False): + def pull( + self, + prefix=None, + default=(None, None), + side='front', + expire_time=False, + tag=False, + retry=False, + ): """Pull key and value item pair from `side` of queue in cache. When prefix is None, integer keys are used. Otherwise, string keys are @@ -1567,8 +1581,9 @@ def pull(self, prefix=None, default=(None, None), side='front', if not rows: return default - (rowid, key, db_expire, db_tag, mode, name, - db_value), = rows + ( + (rowid, key, db_expire, db_tag, mode, name, db_value), + ) = rows sql('DELETE FROM Cache WHERE rowid = ?', (rowid,)) @@ -1598,9 +1613,15 @@ def pull(self, prefix=None, default=(None, None), side='front', else: return key, value - - def peek(self, prefix=None, default=(None, None), side='front', - expire_time=False, tag=False, retry=False): + def peek( + self, + prefix=None, + default=(None, None), + side='front', + expire_time=False, + tag=False, + retry=False, + ): """Peek at key and value item pair from `side` of queue in cache. When prefix is None, integer keys are used. Otherwise, string keys are @@ -1677,8 +1698,9 @@ def peek(self, prefix=None, default=(None, None), side='front', if not rows: return default - (rowid, key, db_expire, db_tag, mode, name, - db_value), = rows + ( + (rowid, key, db_expire, db_tag, mode, name, db_value), + ) = rows if db_expire is not None and db_expire < time.time(): sql('DELETE FROM Cache WHERE rowid = ?', (rowid,)) @@ -1707,7 +1729,6 @@ def peek(self, prefix=None, default=(None, None), side='front', else: return key, value - def peekitem(self, last=True, expire_time=False, tag=False, retry=False): """Peek at key and value item pair in cache based on iteration order. @@ -1749,8 +1770,18 @@ def peekitem(self, last=True, expire_time=False, tag=False, retry=False): if not rows: raise KeyError('dictionary is empty') - (rowid, db_key, raw, db_expire, db_tag, mode, name, - db_value), = rows + ( + ( + rowid, + db_key, + raw, + db_expire, + db_tag, + mode, + name, + db_value, + ), + ) = rows if db_expire is not None and db_expire < time.time(): sql('DELETE FROM Cache WHERE rowid = ?', (rowid,)) @@ -1778,7 +1809,6 @@ def peekitem(self, last=True, expire_time=False, tag=False, retry=False): else: return key, value - def memoize(self, name=None, typed=False, expire=None, tag=None): """Memoizing cache decorator. @@ -1871,7 +1901,6 @@ def __cache_key__(*args, **kwargs): return decorator - def check(self, fix=False, retry=False): """Check database and file system consistency. @@ -1901,7 +1930,7 @@ def check(self, fix=False, retry=False): rows = sql('PRAGMA integrity_check').fetchall() if len(rows) != 1 or rows[0][0] != u'ok': - for message, in rows: + for (message,) in rows: warnings.warn(message) if fix: @@ -1932,7 +1961,8 @@ def check(self, fix=False, retry=False): warnings.warn(message % args) if fix: - sql('UPDATE Cache SET size = ?' + sql( + 'UPDATE Cache SET size = ?' ' WHERE rowid = ?', (real_size, rowid), ) @@ -1973,14 +2003,15 @@ def check(self, fix=False, retry=False): # Check Settings.count against count of Cache rows. self.reset('count') - (count,), = sql('SELECT COUNT(key) FROM Cache').fetchall() + ((count,),) = sql('SELECT COUNT(key) FROM Cache').fetchall() if self.count != count: message = 'Settings.count != COUNT(Cache.key); %d != %d' warnings.warn(message % (self.count, count)) if fix: - sql('UPDATE Settings SET value = ? WHERE key = ?', + sql( + 'UPDATE Settings SET value = ? WHERE key = ?', (count, 'count'), ) @@ -1988,20 +2019,20 @@ def check(self, fix=False, retry=False): self.reset('size') select_size = 'SELECT COALESCE(SUM(size), 0) FROM Cache' - (size,), = sql(select_size).fetchall() + ((size,),) = sql(select_size).fetchall() if self.size != size: message = 'Settings.size != SUM(Cache.size); %d != %d' warnings.warn(message % (self.size, size)) if fix: - sql('UPDATE Settings SET value = ? WHERE key =?', + sql( + 'UPDATE Settings SET value = ? WHERE key =?', (size, 'size'), ) return warns - def create_tag_index(self): """Create tag index on cache database. @@ -2014,7 +2045,6 @@ def create_tag_index(self): sql('CREATE INDEX IF NOT EXISTS Cache_tag_rowid ON Cache(tag, rowid)') self.reset('tag_index', 1) - def drop_tag_index(self): """Drop tag index on cache database. @@ -2025,7 +2055,6 @@ def drop_tag_index(self): sql('DROP INDEX IF EXISTS Cache_tag_rowid') self.reset('tag_index', 0) - def evict(self, tag, retry=False): """Remove items with matching `tag` from cache. @@ -2053,7 +2082,6 @@ def evict(self, tag, retry=False): args = [tag, 0, 100] return self._select_delete(select, args, arg_index=1, retry=retry) - def expire(self, now=None, retry=False): """Remove expired items from cache. @@ -2081,7 +2109,6 @@ def expire(self, now=None, retry=False): args = [0, now or time.time(), 100] return self._select_delete(select, args, row_index=1, retry=retry) - def cull(self, retry=False): """Cull items from cache until volume is less than size limit. @@ -2130,14 +2157,13 @@ def cull(self, retry=False): ) sql(delete, (10,)) - for filename, in rows: + for (filename,) in rows: cleanup(filename) except Timeout: raise Timeout(count) from None return count - def clear(self, retry=False): """Remove all items from cache. @@ -2164,9 +2190,9 @@ def clear(self, retry=False): args = [0, 100] return self._select_delete(select, args, retry=retry) - - def _select_delete(self, select, args, row_index=0, arg_index=0, - retry=False): + def _select_delete( + self, select, args, row_index=0, arg_index=0, retry=False + ): count = 0 delete = 'DELETE FROM Cache WHERE rowid IN (%s)' @@ -2190,7 +2216,6 @@ def _select_delete(self, select, args, row_index=0, arg_index=0, return count - def iterkeys(self, reverse=False): """Iterate Cache keys in database sort order. @@ -2234,7 +2259,7 @@ def iterkeys(self, reverse=False): row = sql(select).fetchall() if row: - (key, raw), = row + ((key, raw),) = row else: return @@ -2249,11 +2274,10 @@ def iterkeys(self, reverse=False): for key, raw in rows: yield _disk_get(key, raw) - def _iter(self, ascending=True): sql = self._sql rows = sql('SELECT MAX(rowid) FROM Cache').fetchall() - (max_rowid,), = rows + ((max_rowid,),) = rows yield # Signal ready. if max_rowid is None: @@ -2283,21 +2307,18 @@ def _iter(self, ascending=True): for rowid, key, raw in rows: yield _disk_get(key, raw) - def __iter__(self): "Iterate keys in cache including expired items." iterator = self._iter() next(iterator) return iterator - def __reversed__(self): "Reverse iterate keys in cache including expired items." iterator = self._iter(ascending=False) next(iterator) return iterator - def stats(self, enable=True, reset=False): """Return cache statistics hits and misses. @@ -2317,22 +2338,18 @@ def stats(self, enable=True, reset=False): return result - def volume(self): """Return estimated total size of cache on disk. :return: size in bytes """ - (page_count,), = self._sql('PRAGMA page_count').fetchall() + ((page_count,),) = self._sql('PRAGMA page_count').fetchall() total_size = self._page_size * page_count + self.reset('size') return total_size - def close(self): - """Close database connection. - - """ + """Close database connection.""" con = getattr(self._local, 'con', None) if con is None: @@ -2345,31 +2362,25 @@ def close(self): except AttributeError: pass - def __enter__(self): # Create connection in thread. # pylint: disable=unused-variable connection = self._con # noqa return self - def __exit__(self, *exception): self.close() - def __len__(self): "Count of items in cache including expired items." return self.reset('count') - def __getstate__(self): return (self.directory, self.timeout, type(self.disk)) - def __setstate__(self, state): self.__init__(*state) - def reset(self, key, value=ENOVAL, update=True): """Reset `key` and `value` item from Settings table. @@ -2403,7 +2414,7 @@ def reset(self, key, value=ENOVAL, update=True): if value is ENOVAL: select = 'SELECT value FROM Settings WHERE key = ?' - (value,), = sql_retry(select, (key,)).fetchall() + ((value,),) = sql_retry(select, (key,)).fetchall() setattr(self, key, value) return value @@ -2431,7 +2442,9 @@ def reset(self, key, value=ENOVAL, update=True): while True: try: try: - (old_value,), = sql('PRAGMA %s' % (pragma)).fetchall() + ((old_value,),) = sql( + 'PRAGMA %s' % (pragma) + ).fetchall() update = old_value != value except ValueError: update = True diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index 329b966..2f1db07 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -15,6 +15,7 @@ class DjangoCache(BaseCache): "Django-compatible disk and file backed cache." + def __init__(self, directory, params): """Initialize DjangoCache instance. @@ -28,13 +29,11 @@ def __init__(self, directory, params): options = params.get('OPTIONS', {}) self._cache = FanoutCache(directory, shards, timeout, **options) - @property def directory(self): """Cache directory.""" return self._cache.directory - def cache(self, name): """Return Cache with given `name` in subdirectory. @@ -44,7 +43,6 @@ def cache(self, name): """ return self._cache.cache(name) - def deque(self, name): """Return Deque with given `name` in subdirectory. @@ -54,7 +52,6 @@ def deque(self, name): """ return self._cache.deque(name) - def index(self, name): """Return Index with given `name` in subdirectory. @@ -64,9 +61,16 @@ def index(self, name): """ return self._cache.index(name) - - def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None, - read=False, tag=None, retry=True): + def add( + self, + key, + value, + timeout=DEFAULT_TIMEOUT, + version=None, + read=False, + tag=None, + retry=True, + ): """Set a value in the cache if the key does not already exist. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. @@ -89,9 +93,16 @@ def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None, timeout = self.get_backend_timeout(timeout=timeout) return self._cache.add(key, value, timeout, read, tag, retry) - - def get(self, key, default=None, version=None, read=False, - expire_time=False, tag=False, retry=False): + def get( + self, + key, + default=None, + version=None, + read=False, + expire_time=False, + tag=False, + retry=False, + ): """Fetch a given key from the cache. If the key does not exist, return default, which itself defaults to None. @@ -111,7 +122,6 @@ def get(self, key, default=None, version=None, read=False, key = self.make_key(key, version=version) return self._cache.get(key, default, read, expire_time, tag, retry) - def read(self, key, version=None): """Return file handle corresponding to `key` from Cache. @@ -124,9 +134,16 @@ def read(self, key, version=None): key = self.make_key(key, version=version) return self._cache.read(key) - - def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None, - read=False, tag=None, retry=True): + def set( + self, + key, + value, + timeout=DEFAULT_TIMEOUT, + version=None, + read=False, + tag=None, + retry=True, + ): """Set a value in the cache. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. @@ -146,7 +163,6 @@ def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None, timeout = self.get_backend_timeout(timeout=timeout) return self._cache.set(key, value, timeout, read, tag, retry) - def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None, retry=True): """Touch a key in the cache. If timeout is given, that timeout will be used for the key; otherwise the default cache timeout will be used. @@ -164,9 +180,15 @@ def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None, retry=True): timeout = self.get_backend_timeout(timeout=timeout) return self._cache.touch(key, timeout, retry) - - def pop(self, key, default=None, version=None, expire_time=False, - tag=False, retry=True): + def pop( + self, + key, + default=None, + version=None, + expire_time=False, + tag=False, + retry=True, + ): """Remove corresponding item for `key` from cache and return value. If `key` is missing, return `default`. @@ -186,7 +208,6 @@ def pop(self, key, default=None, version=None, expire_time=False, key = self.make_key(key, version=version) return self._cache.pop(key, default, expire_time, tag, retry) - def delete(self, key, version=None, retry=True): """Delete a key from the cache, failing silently. @@ -200,7 +221,6 @@ def delete(self, key, version=None, retry=True): key = self.make_key(key, version=version) self._cache.delete(key, retry) - def incr(self, key, delta=1, version=None, default=None, retry=True): """Increment value by delta for item with key. @@ -230,7 +250,6 @@ def incr(self, key, delta=1, version=None, default=None, retry=True): except KeyError: raise ValueError("Key '%s' not found" % key) from None - def decr(self, key, delta=1, version=None, default=None, retry=True): """Decrement value by delta for item with key. @@ -259,7 +278,6 @@ def decr(self, key, delta=1, version=None, default=None, retry=True): # pylint: disable=arguments-differ return self.incr(key, -delta, version, default, retry) - def has_key(self, key, version=None): """Returns True if the key is in the cache and has not expired. @@ -271,7 +289,6 @@ def has_key(self, key, version=None): key = self.make_key(key, version=version) return key in self._cache - def expire(self): """Remove expired items from cache. @@ -280,7 +297,6 @@ def expire(self): """ return self._cache.expire() - def stats(self, enable=True, reset=False): """Return cache statistics hits and misses. @@ -291,7 +307,6 @@ def stats(self, enable=True, reset=False): """ return self._cache.stats(enable=enable, reset=reset) - def create_tag_index(self): """Create tag index on cache database. @@ -302,7 +317,6 @@ def create_tag_index(self): """ self._cache.create_tag_index() - def drop_tag_index(self): """Drop tag index on cache database. @@ -311,7 +325,6 @@ def drop_tag_index(self): """ self._cache.drop_tag_index() - def evict(self, tag): """Remove items with matching `tag` from cache. @@ -321,7 +334,6 @@ def evict(self, tag): """ return self._cache.evict(tag) - def cull(self): """Cull items from cache until volume is less than size limit. @@ -330,18 +342,15 @@ def cull(self): """ return self._cache.cull() - def clear(self): "Remove *all* values from the cache at once." return self._cache.clear() - def close(self, **kwargs): "Close the cache connection." # pylint: disable=unused-argument self._cache.close() - def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT): """Return seconds to expiration. @@ -356,9 +365,14 @@ def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT): timeout = -1 return None if timeout is None else timeout - - def memoize(self, name=None, timeout=DEFAULT_TIMEOUT, version=None, - typed=False, tag=None): + def memoize( + self, + name=None, + timeout=DEFAULT_TIMEOUT, + version=None, + typed=False, + tag=None, + ): """Memoizing cache decorator. Decorator to wrap callable with memoizing function using cache. @@ -418,7 +432,12 @@ def wrapper(*args, **kwargs): ) if valid_timeout: self.set( - key, result, timeout, version, tag=tag, retry=True, + key, + result, + timeout, + version, + tag=tag, + retry=True, ) return result diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 9870c06..cc40c07 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -15,8 +15,10 @@ class FanoutCache(object): "Cache that shards keys and values." - def __init__(self, directory=None, shards=8, timeout=0.010, disk=Disk, - **settings): + + def __init__( + self, directory=None, shards=8, timeout=0.010, disk=Disk, **settings + ): """Initialize cache instance. :param str directory: cache directory @@ -52,20 +54,17 @@ def __init__(self, directory=None, shards=8, timeout=0.010, disk=Disk, self._deques = {} self._indexes = {} - @property def directory(self): """Cache directory.""" return self._directory - def __getattr__(self, name): safe_names = {'timeout', 'disk'} valid_name = name in DEFAULT_SETTINGS or name in safe_names assert valid_name, 'cannot access {} in cache shard'.format(name) return getattr(self._shards[0], name) - @cl.contextmanager def transact(self, retry=True): """Context manager to perform a transaction by locking the cache. @@ -99,7 +98,6 @@ def transact(self, retry=True): stack.enter_context(shard_transaction) yield - def set(self, key, value, expire=None, read=False, tag=None, retry=False): """Set `key` and `value` item in cache. @@ -126,7 +124,6 @@ def set(self, key, value, expire=None, read=False, tag=None, retry=False): except Timeout: return False - def __setitem__(self, key, value): """Set `key` and `value` item in cache. @@ -140,7 +137,6 @@ def __setitem__(self, key, value): shard = self._shards[index] shard[key] = value - def touch(self, key, expire=None, retry=False): """Touch `key` in cache and update `expire` time. @@ -161,7 +157,6 @@ def touch(self, key, expire=None, retry=False): except Timeout: return False - def add(self, key, value, expire=None, read=False, tag=None, retry=False): """Add `key` and `value` item to cache. @@ -193,7 +188,6 @@ def add(self, key, value, expire=None, read=False, tag=None, retry=False): except Timeout: return False - def incr(self, key, delta=1, default=0, retry=False): """Increment value by delta for item with key. @@ -225,7 +219,6 @@ def incr(self, key, delta=1, default=0, retry=False): except Timeout: return None - def decr(self, key, delta=1, default=0, retry=False): """Decrement value by delta for item with key. @@ -260,9 +253,15 @@ def decr(self, key, delta=1, default=0, retry=False): except Timeout: return None - - def get(self, key, default=None, read=False, expire_time=False, tag=False, - retry=False): + def get( + self, + key, + default=None, + read=False, + expire_time=False, + tag=False, + retry=False, + ): """Retrieve value from cache. If `key` is missing, return `default`. If database timeout occurs then returns `default` unless `retry` is set @@ -286,7 +285,6 @@ def get(self, key, default=None, read=False, expire_time=False, tag=False, except (Timeout, sqlite3.OperationalError): return default - def __getitem__(self, key): """Return corresponding value for `key` from cache. @@ -301,7 +299,6 @@ def __getitem__(self, key): shard = self._shards[index] return shard[key] - def read(self, key): """Return file handle corresponding to `key` from cache. @@ -315,7 +312,6 @@ def read(self, key): raise KeyError(key) return handle - def __contains__(self, key): """Return `True` if `key` matching item is found in cache. @@ -327,8 +323,9 @@ def __contains__(self, key): shard = self._shards[index] return key in shard - - def pop(self, key, default=None, expire_time=False, tag=False, retry=False): # noqa: E501 + def pop( + self, key, default=None, expire_time=False, tag=False, retry=False + ): # noqa: E501 """Remove corresponding item for `key` from cache and return value. If `key` is missing, return `default`. @@ -354,7 +351,6 @@ def pop(self, key, default=None, expire_time=False, tag=False, retry=False): # except Timeout: return default - def delete(self, key, retry=False): """Delete corresponding item for `key` from cache. @@ -375,7 +371,6 @@ def delete(self, key, retry=False): except Timeout: return False - def __delitem__(self, key): """Delete corresponding item for `key` from cache. @@ -389,7 +384,6 @@ def __delitem__(self, key): shard = self._shards[index] del shard[key] - def check(self, fix=False, retry=False): """Check database and file system consistency. @@ -413,7 +407,6 @@ def check(self, fix=False, retry=False): warnings = (shard.check(fix, retry) for shard in self._shards) return functools.reduce(operator.iadd, warnings, []) - def expire(self, retry=False): """Remove expired items from cache. @@ -426,7 +419,6 @@ def expire(self, retry=False): """ return self._remove('expire', args=(time.time(),), retry=retry) - def create_tag_index(self): """Create tag index on cache database. @@ -438,7 +430,6 @@ def create_tag_index(self): for shard in self._shards: shard.create_tag_index() - def drop_tag_index(self): """Drop tag index on cache database. @@ -448,7 +439,6 @@ def drop_tag_index(self): for shard in self._shards: shard.drop_tag_index() - def evict(self, tag, retry=False): """Remove items with matching `tag` from cache. @@ -462,7 +452,6 @@ def evict(self, tag, retry=False): """ return self._remove('evict', args=(tag,), retry=retry) - def cull(self, retry=False): """Cull items from cache until volume is less than size limit. @@ -475,7 +464,6 @@ def cull(self, retry=False): """ return self._remove('cull', retry=retry) - def clear(self, retry=False): """Remove all items from cache. @@ -488,7 +476,6 @@ def clear(self, retry=False): """ return self._remove('clear', retry=retry) - def _remove(self, name, args=(), retry=False): total = 0 for shard in self._shards: @@ -503,7 +490,6 @@ def _remove(self, name, args=(), retry=False): break return total - def stats(self, enable=True, reset=False): """Return cache statistics hits and misses. @@ -517,7 +503,6 @@ def stats(self, enable=True, reset=False): total_misses = sum(misses for _, misses in results) return total_hits, total_misses - def volume(self): """Return estimated total size of cache on disk. @@ -526,7 +511,6 @@ def volume(self): """ return sum(shard.volume() for shard in self._shards) - def close(self): "Close database connection." for shard in self._shards: @@ -535,40 +519,32 @@ def close(self): self._deques.clear() self._indexes.clear() - def __enter__(self): return self - def __exit__(self, *exception): self.close() - def __getstate__(self): return (self._directory, self._count, self.timeout, type(self.disk)) - def __setstate__(self, state): self.__init__(*state) - def __iter__(self): "Iterate keys in cache including expired items." iterators = (iter(shard) for shard in self._shards) return it.chain.from_iterable(iterators) - def __reversed__(self): "Reverse iterate keys in cache including expired items." iterators = (reversed(shard) for shard in reversed(self._shards)) return it.chain.from_iterable(iterators) - def __len__(self): "Count of items in cache including expired items." return sum(len(shard) for shard in self._shards) - def reset(self, key, value=ENOVAL): """Reset `key` and `value` item from Settings table. @@ -597,7 +573,6 @@ def reset(self, key, value=ENOVAL): break return result - def cache(self, name): """Return Cache with given `name` in subdirectory. @@ -627,7 +602,6 @@ def cache(self, name): _caches[name] = temp return temp - def deque(self, name): """Return Deque with given `name` in subdirectory. @@ -657,7 +631,6 @@ def deque(self, name): _deques[name] = deque return deque - def index(self, name): """Return Index with given `name` in subdirectory. diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 6fc072b..ac5a5b0 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -15,6 +15,7 @@ def _make_compare(seq_op, doc): "Make compare method with Sequence semantics." + def compare(self, that): "Compare method for deque and sequence." if not isinstance(that, Sequence): @@ -70,6 +71,7 @@ class Deque(Sequence): [3, 2, 1, 0, 0, -1, -2, -3] """ + def __init__(self, iterable=(), directory=None): """Initialize deque instance. @@ -83,7 +85,6 @@ def __init__(self, iterable=(), directory=None): self._cache = Cache(directory, eviction_policy='none') self.extend(iterable) - @classmethod def fromcache(cls, cache, iterable=()): """Initialize deque using `cache`. @@ -110,19 +111,16 @@ def fromcache(cls, cache, iterable=()): self.extend(iterable) return self - @property def cache(self): "Cache used by deque." return self._cache - @property def directory(self): "Directory path where deque is stored." return self._cache.directory - def _index(self, index, func): len_self = len(self) @@ -153,7 +151,6 @@ def _index(self, index, func): raise IndexError('deque index out of range') - def __getitem__(self, index): """deque.__getitem__(index) <==> deque[index] @@ -176,7 +173,6 @@ def __getitem__(self, index): """ return self._index(index, self._cache.__getitem__) - def __setitem__(self, index, value): """deque.__setitem__(index, value) <==> deque[index] = value @@ -195,10 +191,11 @@ def __setitem__(self, index, value): :raises IndexError: if index out of range """ + def _set_value(key): return self._cache.__setitem__(key, value) - self._index(index, _set_value) + self._index(index, _set_value) def __delitem__(self, index): """deque.__delitem__(index) <==> del deque[index] @@ -219,7 +216,6 @@ def __delitem__(self, index): """ self._index(index, self._cache.__delitem__) - def __repr__(self): """deque.__repr__() <==> repr(deque) @@ -229,7 +225,6 @@ def __repr__(self): name = type(self).__name__ return '{0}(directory={1!r})'.format(name, self.directory) - __eq__ = _make_compare(op.eq, 'equal to') __ne__ = _make_compare(op.ne, 'not equal to') __lt__ = _make_compare(op.lt, 'less than') @@ -237,7 +232,6 @@ def __repr__(self): __le__ = _make_compare(op.le, 'less than or equal to') __ge__ = _make_compare(op.ge, 'greater than or equal to') - def __iadd__(self, iterable): """deque.__iadd__(iterable) <==> deque += iterable @@ -250,7 +244,6 @@ def __iadd__(self, iterable): self.extend(iterable) return self - def __iter__(self): """deque.__iter__() <==> iter(deque) @@ -265,7 +258,6 @@ def __iter__(self): except KeyError: pass - def __len__(self): """deque.__len__() <==> len(deque) @@ -274,7 +266,6 @@ def __len__(self): """ return len(self._cache) - def __reversed__(self): """deque.__reversed__() <==> reversed(deque) @@ -297,15 +288,12 @@ def __reversed__(self): except KeyError: pass - def __getstate__(self): return self.directory - def __setstate__(self, state): self.__init__(directory=state) - def append(self, value): """Add `value` to back of deque. @@ -321,7 +309,6 @@ def append(self, value): """ self._cache.push(value, retry=True) - def appendleft(self, value): """Add `value` to front of deque. @@ -337,7 +324,6 @@ def appendleft(self, value): """ self._cache.push(value, side='front', retry=True) - def clear(self): """Remove all elements from deque. @@ -351,7 +337,6 @@ def clear(self): """ self._cache.clear(retry=True) - def count(self, value): """Return number of occurrences of `value` in deque. @@ -370,7 +355,6 @@ def count(self, value): """ return sum(1 for item in self if value == item) - def extend(self, iterable): """Extend back side of deque with values from `iterable`. @@ -380,7 +364,6 @@ def extend(self, iterable): for value in iterable: self.append(value) - def extendleft(self, iterable): """Extend front side of deque with value from `iterable`. @@ -395,7 +378,6 @@ def extendleft(self, iterable): for value in iterable: self.appendleft(value) - def peek(self): """Peek at value at back of deque. @@ -422,7 +404,6 @@ def peek(self): raise IndexError('peek from an empty deque') return value - def peekleft(self): """Peek at value at front of deque. @@ -449,7 +430,6 @@ def peekleft(self): raise IndexError('peek from an empty deque') return value - def pop(self): """Remove and return value at back of deque. @@ -476,7 +456,6 @@ def pop(self): raise IndexError('pop from an empty deque') return value - def popleft(self): """Remove and return value at front of deque. @@ -501,7 +480,6 @@ def popleft(self): raise IndexError('pop from an empty deque') return value - def remove(self, value): """Remove first occurrence of `value` in deque. @@ -539,7 +517,6 @@ def remove(self, value): raise ValueError('deque.remove(value): value not in deque') - def reverse(self): """Reverse deque in place. @@ -562,7 +539,6 @@ def reverse(self): del temp rmtree(directory) - def rotate(self, steps=1): """Rotate deque right by `steps`. @@ -611,10 +587,8 @@ def rotate(self, steps=1): else: self.append(value) - __hash__ = None - @contextmanager def transact(self): """Context manager to perform a transaction by locking the deque. @@ -664,6 +638,7 @@ class Index(MutableMapping): ('c', 3) """ + def __init__(self, *args, **kwargs): """Initialize index in directory and update items. @@ -691,7 +666,6 @@ def __init__(self, *args, **kwargs): self._cache = Cache(directory, eviction_policy='none') self.update(*args, **kwargs) - @classmethod def fromcache(cls, cache, *args, **kwargs): """Initialize index using `cache` and update items. @@ -719,19 +693,16 @@ def fromcache(cls, cache, *args, **kwargs): self.update(*args, **kwargs) return self - @property def cache(self): "Cache used by index." return self._cache - @property def directory(self): "Directory path where items are stored." return self._cache.directory - def __getitem__(self, key): """index.__getitem__(key) <==> index[key] @@ -755,7 +726,6 @@ def __getitem__(self, key): """ return self._cache[key] - def __setitem__(self, key, value): """index.__setitem__(key, value) <==> index[key] = value @@ -773,7 +743,6 @@ def __setitem__(self, key, value): """ self._cache[key] = value - def __delitem__(self, key): """index.__delitem__(key) <==> del index[key] @@ -796,7 +765,6 @@ def __delitem__(self, key): """ del self._cache[key] - def setdefault(self, key, default=None): """Set and get value for `key` in index using `default`. @@ -821,7 +789,6 @@ def setdefault(self, key, default=None): except KeyError: _cache.add(key, default, retry=True) - def peekitem(self, last=True): """Peek at key and value item pair in index based on iteration order. @@ -840,7 +807,6 @@ def peekitem(self, last=True): """ return self._cache.peekitem(last, retry=True) - def pop(self, key, default=ENOVAL): """Remove corresponding item for `key` from index and return value. @@ -871,7 +837,6 @@ def pop(self, key, default=ENOVAL): raise KeyError(key) return value - def popitem(self, last=True): """Remove and return item pair. @@ -906,7 +871,6 @@ def popitem(self, last=True): return key, value - def push(self, value, prefix=None, side='back'): """Push `value` onto `side` of queue in index identified by `prefix`. @@ -938,7 +902,6 @@ def push(self, value, prefix=None, side='back'): """ return self._cache.push(value, prefix, side, retry=True) - def pull(self, prefix=None, default=(None, None), side='front'): """Pull key and value item pair from `side` of queue in index. @@ -979,7 +942,6 @@ def pull(self, prefix=None, default=(None, None), side='front'): """ return self._cache.pull(prefix, default, side, retry=True) - def clear(self): """Remove all items from index. @@ -993,7 +955,6 @@ def clear(self): """ self._cache.clear(retry=True) - def __iter__(self): """index.__iter__() <==> iter(index) @@ -1002,7 +963,6 @@ def __iter__(self): """ return iter(self._cache) - def __reversed__(self): """index.__reversed__() <==> reversed(index) @@ -1019,7 +979,6 @@ def __reversed__(self): """ return reversed(self._cache) - def __len__(self): """index.__len__() <==> len(index) @@ -1028,7 +987,6 @@ def __len__(self): """ return len(self._cache) - def keys(self): """Set-like object providing a view of index keys. @@ -1043,7 +1001,6 @@ def keys(self): """ return KeysView(self) - def values(self): """Set-like object providing a view of index values. @@ -1058,7 +1015,6 @@ def values(self): """ return ValuesView(self) - def items(self): """Set-like object providing a view of index items. @@ -1073,18 +1029,14 @@ def items(self): """ return ItemsView(self) - __hash__ = None - def __getstate__(self): return self.directory - def __setstate__(self, state): self.__init__(state) - def __eq__(self, other): """index.__eq__(other) <==> index == other @@ -1118,7 +1070,6 @@ def __eq__(self, other): else: return all(self[key] == other.get(key, ENOVAL) for key in self) - def __ne__(self, other): """index.__ne__(other) <==> index != other @@ -1142,7 +1093,6 @@ def __ne__(self, other): """ return not self == other - def memoize(self, name=None, typed=False): """Memoizing cache decorator. @@ -1199,7 +1149,6 @@ def memoize(self, name=None, typed=False): """ return self._cache.memoize(name, typed) - @contextmanager def transact(self): """Context manager to perform a transaction by locking the index. @@ -1227,7 +1176,6 @@ def transact(self): with self._cache.transact(retry=True): yield - def __repr__(self): """index.__repr__() <==> repr(index) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 85509c7..3acddd8 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -32,6 +32,7 @@ class Averager(object): None """ + def __init__(self, cache, key, expire=None, tag=None): self._cache = cache self._key = key @@ -45,7 +46,10 @@ def add(self, value): total += value count += 1 self._cache.set( - self._key, (total, count), expire=self._expire, tag=self._tag, + self._key, + (total, count), + expire=self._expire, + tag=self._tag, ) def get(self): @@ -71,6 +75,7 @@ class Lock(object): ... pass """ + def __init__(self, cache, key, expire=None, tag=None): self._cache = cache self._key = key @@ -124,6 +129,7 @@ class RLock(object): AssertionError: cannot release un-acquired lock """ + def __init__(self, cache, key, expire=None, tag=None): self._cache = cache self._key = key @@ -141,8 +147,10 @@ def acquire(self): value, count = self._cache.get(self._key, default=(None, 0)) if pid_tid == value or count == 0: self._cache.set( - self._key, (pid_tid, count + 1), - expire=self._expire, tag=self._tag, + self._key, + (pid_tid, count + 1), + expire=self._expire, + tag=self._tag, ) return time.sleep(0.001) @@ -158,8 +166,10 @@ def release(self): is_owned = pid_tid == value and count > 0 assert is_owned, 'cannot release un-acquired lock' self._cache.set( - self._key, (value, count - 1), - expire=self._expire, tag=self._tag, + self._key, + (value, count - 1), + expire=self._expire, + tag=self._tag, ) def __enter__(self): @@ -187,6 +197,7 @@ class BoundedSemaphore(object): AssertionError: cannot release un-acquired semaphore """ + def __init__(self, cache, key, value=1, expire=None, tag=None): self._cache = cache self._key = key @@ -201,8 +212,10 @@ def acquire(self): value = self._cache.get(self._key, default=self._value) if value > 0: self._cache.set( - self._key, value - 1, - expire=self._expire, tag=self._tag, + self._key, + value - 1, + expire=self._expire, + tag=self._tag, ) return time.sleep(0.001) @@ -214,7 +227,10 @@ def release(self): assert self._value > value, 'cannot release un-acquired semaphore' value += 1 self._cache.set( - self._key, value, expire=self._expire, tag=self._tag, + self._key, + value, + expire=self._expire, + tag=self._tag, ) def __enter__(self): @@ -224,8 +240,16 @@ def __exit__(self, *exc_info): self.release() -def throttle(cache, count, seconds, name=None, expire=None, tag=None, - time_func=time.time, sleep_func=time.sleep): +def throttle( + cache, + count, + seconds, + name=None, + expire=None, + tag=None, + time_func=time.time, + sleep_func=time.sleep, +): """Decorator to throttle calls to function. >>> import diskcache, time @@ -242,6 +266,7 @@ def throttle(cache, count, seconds, name=None, expire=None, tag=None, True """ + def decorator(func): rate = count / float(seconds) key = full_name(func) if name is None else name @@ -298,6 +323,7 @@ def barrier(cache, lock_factory, name=None, expire=None, tag=None): >>> pool.terminate() """ + def decorator(func): key = full_name(func) if name is None else name lock = lock_factory(cache, key, expire=expire, tag=tag) @@ -385,7 +411,10 @@ def wrapper(*args, **kwargs): "Wrapper for callable to cache arguments and return values." key = wrapper.__cache_key__(*args, **kwargs) pair, expire_time = cache.get( - key, default=ENOVAL, expire_time=True, retry=True, + key, + default=ENOVAL, + expire_time=True, + retry=True, ) if pair is not ENOVAL: @@ -400,7 +429,10 @@ def wrapper(*args, **kwargs): thread_key = key + (ENOVAL,) thread_added = cache.add( - thread_key, None, expire=delta, retry=True, + thread_key, + None, + expire=delta, + retry=True, ) if thread_added: @@ -409,8 +441,13 @@ def recompute(): with cache: pair = timer(*args, **kwargs) cache.set( - key, pair, expire=expire, tag=tag, retry=True, + key, + pair, + expire=expire, + tag=tag, + retry=True, ) + thread = threading.Thread(target=recompute) thread.daemon = True thread.start() diff --git a/setup.py b/setup.py index f8c465e..2d6daa3 100644 --- a/setup.py +++ b/setup.py @@ -10,8 +10,10 @@ def finalize_options(self): TestCommand.finalize_options(self) self.test_args = [] self.test_suite = True + def run_tests(self): import tox + errno = tox.cmdline(self.test_args) exit(errno) diff --git a/tests/benchmark_core.py b/tests/benchmark_core.py index 2eeae3b..a048b79 100644 --- a/tests/benchmark_core.py +++ b/tests/benchmark_core.py @@ -32,19 +32,30 @@ import diskcache -caches.append(('diskcache.Cache', diskcache.Cache, ('tmp',), {},)) -caches.append(( - 'diskcache.FanoutCache(shards=4, timeout=1.0)', - diskcache.FanoutCache, - ('tmp',), - {'shards': 4, 'timeout': 1.0} -)) -caches.append(( - 'diskcache.FanoutCache(shards=8, timeout=0.010)', - diskcache.FanoutCache, - ('tmp',), - {'shards': 8, 'timeout': 0.010} -)) +caches.append( + ( + 'diskcache.Cache', + diskcache.Cache, + ('tmp',), + {}, + ) +) +caches.append( + ( + 'diskcache.FanoutCache(shards=4, timeout=1.0)', + diskcache.FanoutCache, + ('tmp',), + {'shards': 4, 'timeout': 1.0}, + ) +) +caches.append( + ( + 'diskcache.FanoutCache(shards=8, timeout=0.010)', + diskcache.FanoutCache, + ('tmp',), + {'shards': 8, 'timeout': 0.010}, + ) +) ############################################################################### @@ -54,12 +65,17 @@ try: import pylibmc - caches.append(( - 'pylibmc.Client', - pylibmc.Client, - (['127.0.0.1'],), - {'binary': True, 'behaviors': {'tcp_nodelay': True, 'ketama': True}}, - )) + caches.append( + ( + 'pylibmc.Client', + pylibmc.Client, + (['127.0.0.1'],), + { + 'binary': True, + 'behaviors': {'tcp_nodelay': True, 'ketama': True}, + }, + ) + ) except ImportError: warnings.warn('skipping pylibmc') @@ -71,12 +87,14 @@ try: import redis - caches.append(( - 'redis.StrictRedis', - redis.StrictRedis, - (), - {'host': 'localhost', 'port': 6379, 'db': 0}, - )) + caches.append( + ( + 'redis.StrictRedis', + redis.StrictRedis, + (), + {'host': 'localhost', 'port': 6379, 'db': 0}, + ) + ) except ImportError: warnings.warn('skipping redis') @@ -84,7 +102,7 @@ def worker(num, kind, args, kwargs): random.seed(num) - time.sleep(0.01) # Let other processes start. + time.sleep(0.01) # Let other processes start. obj = kind(*args, **kwargs) @@ -173,19 +191,31 @@ def dispatch(): formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( - '-p', '--processes', type=int, default=PROCS, + '-p', + '--processes', + type=int, + default=PROCS, help='Number of processes to start', ) parser.add_argument( - '-n', '--operations', type=float, default=OPS, + '-n', + '--operations', + type=float, + default=OPS, help='Number of operations to perform', ) parser.add_argument( - '-r', '--range', type=int, default=RANGE, + '-r', + '--range', + type=int, + default=RANGE, help='Range of keys', ) parser.add_argument( - '-w', '--warmup', type=float, default=WARMUP, + '-w', + '--warmup', + type=float, + default=WARMUP, help='Number of warmup operations before timings', ) diff --git a/tests/benchmark_djangocache.py b/tests/benchmark_djangocache.py index 0512752..0de0424 100644 --- a/tests/benchmark_djangocache.py +++ b/tests/benchmark_djangocache.py @@ -27,6 +27,7 @@ def setup(): os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings_benchmark') import django + django.setup() @@ -41,7 +42,7 @@ def worker(num, name): timings = co.defaultdict(list) - time.sleep(0.01) # Let other processes start. + time.sleep(0.01) # Let other processes start. for count in range(OPS): key = str(random.randrange(RANGE)).encode('utf-8') @@ -140,19 +141,31 @@ def dispatch(): formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( - '-p', '--processes', type=int, default=PROCS, + '-p', + '--processes', + type=int, + default=PROCS, help='Number of processes to start', ) parser.add_argument( - '-n', '--operations', type=float, default=OPS, + '-n', + '--operations', + type=float, + default=OPS, help='Number of operations to perform', ) parser.add_argument( - '-r', '--range', type=int, default=RANGE, + '-r', + '--range', + type=int, + default=RANGE, help='Range of keys', ) parser.add_argument( - '-w', '--warmup', type=float, default=WARMUP, + '-w', + '--warmup', + type=float, + default=WARMUP, help='Number of warmup operations before timings', ) diff --git a/tests/benchmark_glob.py b/tests/benchmark_glob.py index 82237de..9c23104 100644 --- a/tests/benchmark_glob.py +++ b/tests/benchmark_glob.py @@ -26,11 +26,9 @@ for value in range(count): with open(op.join('tmp', '%s.tmp' % value), 'wb') as writer: pass - + delta = timeit.timeit( - stmt="glob.glob1('tmp', '*.tmp')", - setup='import glob', - number=100 + stmt="glob.glob1('tmp', '*.tmp')", setup='import glob', number=100 ) print(template % (count, secs(delta))) diff --git a/tests/issue_109.py b/tests/issue_109.py index a650b4c..a5355b1 100644 --- a/tests/issue_109.py +++ b/tests/issue_109.py @@ -8,6 +8,7 @@ def main(): import argparse + parser = argparse.ArgumentParser() parser.add_argument('--cache-dir', default='/tmp/test') parser.add_argument('--iterations', type=int, default=100) @@ -31,7 +32,7 @@ def main(): delays.append(diff) # Discard warmup delays, first two iterations. - del delays[:(len(values) * 2)] + del delays[: (len(values) * 2)] # Convert seconds to microseconds. delays = sorted(delay * 1e6 for delay in delays) diff --git a/tests/issue_85.py b/tests/issue_85.py index a52de97..db32046 100644 --- a/tests/issue_85.py +++ b/tests/issue_85.py @@ -26,6 +26,7 @@ def init_django(): os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') django.setup() from django.core.cache import cache + shard = cache._cache._shards[0] diff --git a/tests/models.py b/tests/models.py index 349fd87..a546822 100644 --- a/tests/models.py +++ b/tests/models.py @@ -10,4 +10,6 @@ def expensive_calculation(): class Poll(models.Model): question = models.CharField(max_length=200) answer = models.CharField(max_length=200) - pub_date = models.DateTimeField('date published', default=expensive_calculation) + pub_date = models.DateTimeField( + 'date published', default=expensive_calculation + ) diff --git a/tests/plot.py b/tests/plot.py index d8d1be0..2f83f14 100644 --- a/tests/plot.py +++ b/tests/plot.py @@ -93,12 +93,17 @@ def make_plot(data, action, save=False, show=False, limit=0.005): bars = [] for pos, (name, color) in enumerate(zip(names, colors)): - bars.append(ax.bar( - [val + pos * width for val in index], - [parse_timing(data[name][action][tick], limit) for tick in ticks], - width, - color=color, - )) + bars.append( + ax.bar( + [val + pos * width for val in index], + [ + parse_timing(data[name][action][tick], limit) + for tick in ticks + ], + width, + color=color, + ) + ) ax.set_ylabel('Time (microseconds)') ax.set_title('"%s" Time vs Percentile' % action) @@ -106,12 +111,14 @@ def make_plot(data, action, save=False, show=False, limit=0.005): ax.set_xticklabels(ticks) box = ax.get_position() - ax.set_position([box.x0, box.y0 + box.height * 0.2, box.width, box.height * 0.8]) + ax.set_position( + [box.x0, box.y0 + box.height * 0.2, box.width, box.height * 0.8] + ) ax.legend( [bar[0] for bar in bars], names, loc='lower center', - bbox_to_anchor=(0.5, -0.25) + bbox_to_anchor=(0.5, -0.25), ) if show: diff --git a/tests/plot_early_recompute.py b/tests/plot_early_recompute.py index bbebc1a..da46e3e 100644 --- a/tests/plot_early_recompute.py +++ b/tests/plot_early_recompute.py @@ -16,6 +16,7 @@ def make_timer(times): """ lock = threading.Lock() + def timer(func): @ft.wraps(func) def wrapper(*args, **kwargs): @@ -24,7 +25,9 @@ def wrapper(*args, **kwargs): pair = start, time.time() with lock: times.append(pair) + return wrapper + return timer @@ -33,9 +36,11 @@ def make_worker(times, delay=0.2): `delay` seconds. """ + @make_timer(times) def worker(): time.sleep(delay) + return worker @@ -44,11 +49,13 @@ def make_repeater(func, total=10, delay=0.01): repeatedly until `total` seconds have elapsed. """ + def repeat(num): start = time.time() while time.time() - start < total: func() time.sleep(delay) + return repeat @@ -62,6 +69,7 @@ def frange(start, stop, step=1e-3): def plot(option, filename, cache_times, worker_times): "Plot concurrent workers and latency." import matplotlib.pyplot as plt + fig, (workers, latency) = plt.subplots(2, sharex=True) fig.suptitle(option) diff --git a/tests/settings_benchmark.py b/tests/settings_benchmark.py index 5b614a5..1724cef 100644 --- a/tests/settings_benchmark.py +++ b/tests/settings_benchmark.py @@ -14,7 +14,7 @@ 'LOCATION': 'redis://127.0.0.1:6379/1', 'OPTIONS': { 'CLIENT_CLASS': 'django_redis.client.DefaultClient', - } + }, }, 'filebased': { 'BACKEND': 'django.core.cache.backends.filebased.FileBasedCache', @@ -22,7 +22,7 @@ 'OPTIONS': { 'CULL_FREQUENCY': 10, 'MAX_ENTRIES': 1000, - } + }, }, 'locmem': { 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', @@ -30,7 +30,7 @@ 'OPTIONS': { 'CULL_FREQUENCY': 10, 'MAX_ENTRIES': 1000, - } + }, }, 'diskcache': { 'BACKEND': 'diskcache.DjangoCache', diff --git a/tests/stress_test_core.py b/tests/stress_test_core.py index 68f2ebd..a36ae4e 100644 --- a/tests/stress_test_core.py +++ b/tests/stress_test_core.py @@ -33,13 +33,17 @@ def make_long(): def make_unicode(): word_size = random.randint(1, 26) - word = u''.join(random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size)) + word = u''.join( + random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + ) size = random.randint(1, int(200 / 13)) return word * size def make_bytes(): word_size = random.randint(1, 26) - word = u''.join(random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size)).encode('utf-8') + word = u''.join( + random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + ).encode('utf-8') size = random.randint(1, int(200 / 13)) return word * size @@ -49,7 +53,14 @@ def make_float(): def make_object(): return (make_float(),) * random.randint(1, 20) - funcs = [make_int, make_long, make_unicode, make_bytes, make_float, make_object] + funcs = [ + make_int, + make_long, + make_unicode, + make_bytes, + make_float, + make_object, + ] while True: func = random.choice(funcs) @@ -66,13 +77,17 @@ def make_long(): def make_unicode(): word_size = random.randint(1, 26) - word = u''.join(random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size)) + word = u''.join( + random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + ) size = random.randint(1, int(2 ** 16 / 13)) return word * size def make_bytes(): word_size = random.randint(1, 26) - word = u''.join(random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size)).encode('utf-8') + word = u''.join( + random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + ).encode('utf-8') size = random.randint(1, int(2 ** 16 / 13)) return word * size @@ -82,7 +97,14 @@ def make_float(): def make_object(): return [make_float()] * random.randint(1, int(2e3)) - funcs = [make_int, make_long, make_unicode, make_bytes, make_float, make_object] + funcs = [ + make_int, + make_long, + make_unicode, + make_bytes, + make_float, + make_object, + ] while True: func = random.choice(funcs) @@ -134,7 +156,12 @@ def worker(queue, eviction_policy, processes, threads): stop = time.time() - if action == 'get' and processes == 1 and threads == 1 and EXPIRE is None: + if ( + action == 'get' + and processes == 1 + and threads == 1 + and EXPIRE is None + ): assert result == value if index > WARMUP: @@ -155,8 +182,10 @@ def dispatch(num, eviction_policy, processes, threads): thread_queues = [queue.Queue() for _ in range(threads)] subthreads = [ threading.Thread( - target=worker, args=(thread_queue, eviction_policy, processes, threads) - ) for thread_queue in thread_queues + target=worker, + args=(thread_queue, eviction_policy, processes, threads), + ) + for thread_queue in thread_queues ] for index, triplet in enumerate(process_queue): @@ -201,9 +230,13 @@ def percentile(sequence, percent): return values[pos] -def stress_test(create=True, delete=True, - eviction_policy=u'least-recently-stored', - processes=1, threads=1): +def stress_test( + create=True, + delete=True, + eviction_policy=u'least-recently-stored', + processes=1, + threads=1, +): shutil.rmtree('tmp', ignore_errors=True) if processes == 1: @@ -285,51 +318,84 @@ def stress_test_mp(): formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( - '-n', '--operations', type=float, default=OPERATIONS, + '-n', + '--operations', + type=float, + default=OPERATIONS, help='Number of operations to perform', ) parser.add_argument( - '-g', '--get-average', type=float, default=GET_AVERAGE, + '-g', + '--get-average', + type=float, + default=GET_AVERAGE, help='Expected value of exponential variate used for GET count', ) parser.add_argument( - '-k', '--key-count', type=float, default=KEY_COUNT, - help='Number of unique keys' + '-k', + '--key-count', + type=float, + default=KEY_COUNT, + help='Number of unique keys', ) parser.add_argument( - '-d', '--del-chance', type=float, default=DEL_CHANCE, + '-d', + '--del-chance', + type=float, + default=DEL_CHANCE, help='Likelihood of a key deletion', ) parser.add_argument( - '-w', '--warmup', type=float, default=WARMUP, + '-w', + '--warmup', + type=float, + default=WARMUP, help='Number of warmup operations before timings', ) parser.add_argument( - '-e', '--expire', type=float, default=EXPIRE, + '-e', + '--expire', + type=float, + default=EXPIRE, help='Number of seconds before key expires', ) parser.add_argument( - '-t', '--threads', type=int, default=1, + '-t', + '--threads', + type=int, + default=1, help='Number of threads to start in each process', ) parser.add_argument( - '-p', '--processes', type=int, default=1, + '-p', + '--processes', + type=int, + default=1, help='Number of processes to start', ) parser.add_argument( - '-s', '--seed', type=int, default=0, + '-s', + '--seed', + type=int, + default=0, help='Random seed', ) parser.add_argument( - '--no-create', action='store_false', dest='create', + '--no-create', + action='store_false', + dest='create', help='Do not create operations data', ) parser.add_argument( - '--no-delete', action='store_false', dest='delete', + '--no-delete', + action='store_false', + dest='delete', help='Do not delete operations data', ) parser.add_argument( - '-v', '--eviction-policy', type=unicode, + '-v', + '--eviction-policy', + type=unicode, default=u'least-recently-stored', ) diff --git a/tests/stress_test_fanout.py b/tests/stress_test_fanout.py index 422e874..fc2de39 100644 --- a/tests/stress_test_fanout.py +++ b/tests/stress_test_fanout.py @@ -33,13 +33,17 @@ def make_long(): def make_unicode(): word_size = random.randint(1, 26) - word = u''.join(random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size)) + word = u''.join( + random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + ) size = random.randint(1, int(200 / 13)) return word * size def make_bytes(): word_size = random.randint(1, 26) - word = u''.join(random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size)).encode('utf-8') + word = u''.join( + random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + ).encode('utf-8') size = random.randint(1, int(200 / 13)) return word * size @@ -49,7 +53,14 @@ def make_float(): def make_object(): return (make_float(),) * random.randint(1, 20) - funcs = [make_int, make_long, make_unicode, make_bytes, make_float, make_object] + funcs = [ + make_int, + make_long, + make_unicode, + make_bytes, + make_float, + make_object, + ] while True: func = random.choice(funcs) @@ -66,13 +77,17 @@ def make_long(): def make_unicode(): word_size = random.randint(1, 26) - word = u''.join(random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size)) + word = u''.join( + random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + ) size = random.randint(1, int(2 ** 16 / 13)) return word * size def make_bytes(): word_size = random.randint(1, 26) - word = u''.join(random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size)).encode('utf-8') + word = u''.join( + random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + ).encode('utf-8') size = random.randint(1, int(2 ** 16 / 13)) return word * size @@ -82,7 +97,14 @@ def make_float(): def make_object(): return [make_float()] * random.randint(1, int(2e3)) - funcs = [make_int, make_long, make_unicode, make_bytes, make_float, make_object] + funcs = [ + make_int, + make_long, + make_unicode, + make_bytes, + make_float, + make_object, + ] while True: func = random.choice(funcs) @@ -129,7 +151,12 @@ def worker(queue, eviction_policy, processes, threads): stop = time.time() - if action == 'get' and processes == 1 and threads == 1 and EXPIRE is None: + if ( + action == 'get' + and processes == 1 + and threads == 1 + and EXPIRE is None + ): assert result == value if index > WARMUP: @@ -147,8 +174,10 @@ def dispatch(num, eviction_policy, processes, threads): thread_queues = [queue.Queue() for _ in range(threads)] subthreads = [ threading.Thread( - target=worker, args=(thread_queue, eviction_policy, processes, threads) - ) for thread_queue in thread_queues + target=worker, + args=(thread_queue, eviction_policy, processes, threads), + ) + for thread_queue in thread_queues ] for index, triplet in enumerate(process_queue): @@ -193,9 +222,13 @@ def percentile(sequence, percent): return values[pos] -def stress_test(create=True, delete=True, - eviction_policy=u'least-recently-stored', - processes=1, threads=1): +def stress_test( + create=True, + delete=True, + eviction_policy=u'least-recently-stored', + processes=1, + threads=1, +): shutil.rmtree('tmp', ignore_errors=True) if processes == 1: @@ -277,51 +310,84 @@ def stress_test_mp(): formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( - '-n', '--operations', type=float, default=OPERATIONS, + '-n', + '--operations', + type=float, + default=OPERATIONS, help='Number of operations to perform', ) parser.add_argument( - '-g', '--get-average', type=float, default=GET_AVERAGE, + '-g', + '--get-average', + type=float, + default=GET_AVERAGE, help='Expected value of exponential variate used for GET count', ) parser.add_argument( - '-k', '--key-count', type=float, default=KEY_COUNT, - help='Number of unique keys' + '-k', + '--key-count', + type=float, + default=KEY_COUNT, + help='Number of unique keys', ) parser.add_argument( - '-d', '--del-chance', type=float, default=DEL_CHANCE, + '-d', + '--del-chance', + type=float, + default=DEL_CHANCE, help='Likelihood of a key deletion', ) parser.add_argument( - '-w', '--warmup', type=float, default=WARMUP, + '-w', + '--warmup', + type=float, + default=WARMUP, help='Number of warmup operations before timings', ) parser.add_argument( - '-e', '--expire', type=float, default=EXPIRE, + '-e', + '--expire', + type=float, + default=EXPIRE, help='Number of seconds before key expires', ) parser.add_argument( - '-t', '--threads', type=int, default=1, + '-t', + '--threads', + type=int, + default=1, help='Number of threads to start in each process', ) parser.add_argument( - '-p', '--processes', type=int, default=1, + '-p', + '--processes', + type=int, + default=1, help='Number of processes to start', ) parser.add_argument( - '-s', '--seed', type=int, default=0, + '-s', + '--seed', + type=int, + default=0, help='Random seed', ) parser.add_argument( - '--no-create', action='store_false', dest='create', + '--no-create', + action='store_false', + dest='create', help='Do not create operations data', ) parser.add_argument( - '--no-delete', action='store_false', dest='delete', + '--no-delete', + action='store_false', + dest='delete', help='Do not delete operations data', ) parser.add_argument( - '-v', '--eviction-policy', type=unicode, + '-v', + '--eviction-policy', + type=unicode, default=u'least-recently-stored', ) diff --git a/tests/test_core.py b/tests/test_core.py index 31b4034..cb4e34f 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -26,6 +26,7 @@ pytestmark = pytest.mark.filterwarnings('ignore', category=dc.EmptyDirWarning) + @pytest.fixture def cache(): with dc.Cache() as cache: @@ -161,6 +162,7 @@ def test_close_error(cache): class LocalTest(object): def __init__(self): self._calls = 0 + def __getattr__(self, name): if self._calls: raise AttributeError @@ -303,6 +305,7 @@ def test_get(cache): assert cache.get(0, tag=True) == (0, u'number') assert cache.get(0, expire_time=True, tag=True) == (0, None, u'number') + def test_get_expired_fast_path(cache): assert cache.set(0, 0, expire=0.001) time.sleep(0.01) @@ -642,7 +645,7 @@ def test_check(cache): cache.check() cache.check(fix=True) - assert len(cache.check()) == 0 # Should display no warnings. + assert len(cache.check()) == 0 # Should display no warnings. def test_integrity_check(cache): @@ -653,7 +656,7 @@ def test_integrity_check(cache): with io.open(op.join(cache.directory, 'cache.db'), 'r+b') as writer: writer.seek(52) - writer.write(b'\x00\x01') # Should be 0, change it. + writer.write(b'\x00\x01') # Should be 0, change it. cache = dc.Cache(cache.directory) @@ -1262,8 +1265,8 @@ def test_cull_timeout(cache): def test_key_roundtrip(cache): - key_part_0 = u"part0" - key_part_1 = u"part1" + key_part_0 = u'part0' + key_part_1 = u'part1' to_test = [ (key_part_0, key_part_1), [key_part_0, key_part_1], @@ -1281,6 +1284,7 @@ def test_key_roundtrip(cache): def test_constant(): import diskcache.core + assert repr(diskcache.core.ENOVAL) == 'ENOVAL' diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index 84d2f95..5cb0047 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -20,28 +20,43 @@ from django.conf import settings from django.core import management, signals from django.core.cache import ( - DEFAULT_CACHE_ALIAS, CacheKeyWarning, InvalidCacheKey, cache, caches, + DEFAULT_CACHE_ALIAS, + CacheKeyWarning, + InvalidCacheKey, + cache, + caches, ) from django.core.cache.utils import make_template_fragment_key from django.db import close_old_connections, connection, connections from django.http import ( - HttpRequest, HttpResponse, HttpResponseNotModified, StreamingHttpResponse, + HttpRequest, + HttpResponse, + HttpResponseNotModified, + StreamingHttpResponse, ) from django.middleware.cache import ( - CacheMiddleware, FetchFromCacheMiddleware, UpdateCacheMiddleware, + CacheMiddleware, + FetchFromCacheMiddleware, + UpdateCacheMiddleware, ) from django.middleware.csrf import CsrfViewMiddleware from django.template import engines from django.template.context_processors import csrf from django.template.response import TemplateResponse from django.test import ( - RequestFactory, SimpleTestCase, TestCase, TransactionTestCase, + RequestFactory, + SimpleTestCase, + TestCase, + TransactionTestCase, override_settings, ) from django.test.signals import setting_changed from django.utils import timezone, translation from django.utils.cache import ( - get_cache_key, learn_cache_key, patch_cache_control, patch_vary_headers, + get_cache_key, + learn_cache_key, + patch_cache_control, + patch_vary_headers, ) from django.utils.encoding import force_text from django.views.decorators.cache import cache_page @@ -53,6 +68,7 @@ os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tests.settings') import django + django.setup() from .models import Poll, expensive_calculation @@ -81,7 +97,7 @@ def __getstate__(self): class UnpicklableType(object): # Unpicklable using the default pickling protocol on Python 2. - __slots__ = 'a', + __slots__ = ('a',) def custom_key_func(key, key_prefix, version): @@ -110,7 +126,9 @@ def caches_setting_for_tests(base=None, exclude=None, **params): # params -> _caches_setting_base -> base base = base or {} exclude = exclude or set() - setting = {k: base.copy() for k in _caches_setting_base if k not in exclude} + setting = { + k: base.copy() for k in _caches_setting_base if k not in exclude + } for key, cache_params in setting.items(): cache_params.update(_caches_setting_base[key]) cache_params.update(params) @@ -126,15 +144,15 @@ def tearDown(self): def test_simple(self): # Simple cache set/get works - cache.set("key", "value") - self.assertEqual(cache.get("key"), "value") + cache.set('key', 'value') + self.assertEqual(cache.get('key'), 'value') def test_add(self): # A key can be added to a cache - cache.add("addkey1", "value") - result = cache.add("addkey1", "newvalue") + cache.add('addkey1', 'value') + result = cache.add('addkey1', 'newvalue') self.assertFalse(result) - self.assertEqual(cache.get("addkey1"), "value") + self.assertEqual(cache.get('addkey1'), 'value') def test_prefix(self): # Test for same cache key conflicts between shared backend @@ -150,37 +168,41 @@ def test_prefix(self): def test_non_existent(self): """Nonexistent cache keys return as None/default.""" - self.assertIsNone(cache.get("does_not_exist")) - self.assertEqual(cache.get("does_not_exist", "bang!"), "bang!") + self.assertIsNone(cache.get('does_not_exist')) + self.assertEqual(cache.get('does_not_exist', 'bang!'), 'bang!') def test_get_many(self): # Multiple cache keys can be returned using get_many cache.set_many({'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd'}) - self.assertEqual(cache.get_many(['a', 'c', 'd']), {'a': 'a', 'c': 'c', 'd': 'd'}) + self.assertEqual( + cache.get_many(['a', 'c', 'd']), {'a': 'a', 'c': 'c', 'd': 'd'} + ) self.assertEqual(cache.get_many(['a', 'b', 'e']), {'a': 'a', 'b': 'b'}) - self.assertEqual(cache.get_many(iter(['a', 'b', 'e'])), {'a': 'a', 'b': 'b'}) + self.assertEqual( + cache.get_many(iter(['a', 'b', 'e'])), {'a': 'a', 'b': 'b'} + ) def test_delete(self): # Cache keys can be deleted cache.set_many({'key1': 'spam', 'key2': 'eggs'}) - self.assertEqual(cache.get("key1"), "spam") - cache.delete("key1") - self.assertIsNone(cache.get("key1")) - self.assertEqual(cache.get("key2"), "eggs") + self.assertEqual(cache.get('key1'), 'spam') + cache.delete('key1') + self.assertIsNone(cache.get('key1')) + self.assertEqual(cache.get('key2'), 'eggs') def test_has_key(self): # The cache can be inspected for cache keys - cache.set("hello1", "goodbye1") - self.assertTrue(cache.has_key("hello1")) - self.assertFalse(cache.has_key("goodbye1")) - cache.set("no_expiry", "here", None) - self.assertTrue(cache.has_key("no_expiry")) + cache.set('hello1', 'goodbye1') + self.assertTrue(cache.has_key('hello1')) + self.assertFalse(cache.has_key('goodbye1')) + cache.set('no_expiry', 'here', None) + self.assertTrue(cache.has_key('no_expiry')) def test_in(self): # The in operator can be used to inspect cache contents - cache.set("hello2", "goodbye2") - self.assertIn("hello2", cache) - self.assertNotIn("goodbye2", cache) + cache.set('hello2', 'goodbye2') + self.assertIn('hello2', cache) + self.assertNotIn('goodbye2', cache) def test_incr(self): # Cache values can be incremented @@ -219,14 +241,14 @@ def test_data_types(self): 'function': f, 'class': C, } - cache.set("stuff", stuff) - self.assertEqual(cache.get("stuff"), stuff) + cache.set('stuff', stuff) + self.assertEqual(cache.get('stuff'), stuff) def test_cache_read_for_model_instance(self): # Don't want fields with callable as default to be called on cache read expensive_calculation.num_runs = 0 Poll.objects.all().delete() - my_poll = Poll.objects.create(question="Well?") + my_poll = Poll.objects.create(question='Well?') self.assertEqual(Poll.objects.count(), 1) pub_date = my_poll.pub_date cache.set('question', my_poll) @@ -239,7 +261,7 @@ def test_cache_write_for_model_instance_with_deferred(self): # Don't want fields with callable as default to be called on cache write expensive_calculation.num_runs = 0 Poll.objects.all().delete() - Poll.objects.create(question="What?") + Poll.objects.create(question='What?') self.assertEqual(expensive_calculation.num_runs, 1) defer_qs = Poll.objects.all().defer('question') self.assertEqual(defer_qs.count(), 1) @@ -252,7 +274,7 @@ def test_cache_read_for_model_instance_with_deferred(self): # Don't want fields with callable as default to be called on cache read expensive_calculation.num_runs = 0 Poll.objects.all().delete() - Poll.objects.create(question="What?") + Poll.objects.create(question='What?') self.assertEqual(expensive_calculation.num_runs, 1) defer_qs = Poll.objects.all().defer('question') self.assertEqual(defer_qs.count(), 1) @@ -261,7 +283,9 @@ def test_cache_read_for_model_instance_with_deferred(self): runs_before_cache_read = expensive_calculation.num_runs cache.get('deferred_queryset') # We only want the default expensive calculation run on creation and set - self.assertEqual(expensive_calculation.num_runs, runs_before_cache_read) + self.assertEqual( + expensive_calculation.num_runs, runs_before_cache_read + ) def test_expiration(self): # Cache values can be set to expire @@ -270,11 +294,11 @@ def test_expiration(self): cache.set('expire3', 'very quickly', 1) time.sleep(2) - self.assertIsNone(cache.get("expire1")) + self.assertIsNone(cache.get('expire1')) - cache.add("expire2", "newvalue") - self.assertEqual(cache.get("expire2"), "newvalue") - self.assertFalse(cache.has_key("expire3")) + cache.add('expire2', 'newvalue') + self.assertEqual(cache.get('expire2'), 'newvalue') + self.assertFalse(cache.has_key('expire3')) def test_touch(self): # cache.touch() updates the timeout. @@ -299,7 +323,7 @@ def test_unicode(self): 'ascii': 'ascii_value', 'unicode_ascii': 'Iñtërnâtiônàlizætiøn1', 'Iñtërnâtiônàlizætiøn': 'Iñtërnâtiônàlizætiøn2', - 'ascii2': {'x': 1} + 'ascii2': {'x': 1}, } # Test `set` for (key, value) in stuff.items(): @@ -325,6 +349,7 @@ def test_unicode(self): def test_binary_string(self): # Binary strings should be cacheable from zlib import compress, decompress + value = 'value_to_be_compressed' compressed_value = compress(value.encode()) @@ -348,9 +373,9 @@ def test_binary_string(self): def test_set_many(self): # Multiple keys can be set using set_many - cache.set_many({"key1": "spam", "key2": "eggs"}) - self.assertEqual(cache.get("key1"), "spam") - self.assertEqual(cache.get("key2"), "eggs") + cache.set_many({'key1': 'spam', 'key2': 'eggs'}) + self.assertEqual(cache.get('key1'), 'spam') + self.assertEqual(cache.get('key2'), 'eggs') def test_set_many_returns_empty_list_on_success(self): """set_many() returns an empty list when all keys are inserted.""" @@ -359,25 +384,25 @@ def test_set_many_returns_empty_list_on_success(self): def test_set_many_expiration(self): # set_many takes a second ``timeout`` parameter - cache.set_many({"key1": "spam", "key2": "eggs"}, 1) + cache.set_many({'key1': 'spam', 'key2': 'eggs'}, 1) time.sleep(2) - self.assertIsNone(cache.get("key1")) - self.assertIsNone(cache.get("key2")) + self.assertIsNone(cache.get('key1')) + self.assertIsNone(cache.get('key2')) def test_delete_many(self): # Multiple keys can be deleted using delete_many cache.set_many({'key1': 'spam', 'key2': 'eggs', 'key3': 'ham'}) - cache.delete_many(["key1", "key2"]) - self.assertIsNone(cache.get("key1")) - self.assertIsNone(cache.get("key2")) - self.assertEqual(cache.get("key3"), "ham") + cache.delete_many(['key1', 'key2']) + self.assertIsNone(cache.get('key1')) + self.assertIsNone(cache.get('key2')) + self.assertEqual(cache.get('key3'), 'ham') def test_clear(self): # The cache can be emptied using clear cache.set_many({'key1': 'spam', 'key2': 'eggs'}) cache.clear() - self.assertIsNone(cache.get("key1")) - self.assertIsNone(cache.get("key2")) + self.assertIsNone(cache.get('key1')) + self.assertIsNone(cache.get('key2')) def test_long_timeout(self): """ @@ -391,7 +416,10 @@ def test_long_timeout(self): cache.add('key2', 'ham', 60 * 60 * 24 * 30 + 1) self.assertEqual(cache.get('key2'), 'ham') - cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, 60 * 60 * 24 * 30 + 1) + cache.set_many( + {'key3': 'sausage', 'key4': 'lobster bisque'}, + 60 * 60 * 24 * 30 + 1, + ) self.assertEqual(cache.get('key3'), 'sausage') self.assertEqual(cache.get('key4'), 'lobster bisque') @@ -437,8 +465,8 @@ def test_zero_timeout(self): def test_float_timeout(self): # Make sure a timeout given as a float doesn't crash anything. - cache.set("key1", "spam", 100.2) - self.assertEqual(cache.get("key1"), "spam") + cache.set('key1', 'spam', 100.2) + self.assertEqual(cache.get('key1'), 'spam') def _perform_cull_test(self, cull_cache, initial_count, final_count): # Create initial cache key entries. This will overflow the cache, @@ -483,7 +511,9 @@ def func(key, *args): def test_invalid_key_characters(self): # memcached doesn't allow whitespace or control characters in keys. key = 'key with spaces and 清' - self._perform_invalid_key_test(key, KEY_ERRORS_WITH_MEMCACHED_MSG % key) + self._perform_invalid_key_test( + key, KEY_ERRORS_WITH_MEMCACHED_MSG % key + ) def test_invalid_key_length(self): # memcached limits key length to 250. @@ -653,43 +683,85 @@ def test_cache_versioning_incr_decr(self): def test_cache_versioning_get_set_many(self): # set, using default version = 1 cache.set_many({'ford1': 37, 'arthur1': 42}) - self.assertEqual(cache.get_many(['ford1', 'arthur1']), {'ford1': 37, 'arthur1': 42}) - self.assertEqual(cache.get_many(['ford1', 'arthur1'], version=1), {'ford1': 37, 'arthur1': 42}) + self.assertEqual( + cache.get_many(['ford1', 'arthur1']), {'ford1': 37, 'arthur1': 42} + ) + self.assertEqual( + cache.get_many(['ford1', 'arthur1'], version=1), + {'ford1': 37, 'arthur1': 42}, + ) self.assertEqual(cache.get_many(['ford1', 'arthur1'], version=2), {}) self.assertEqual(caches['v2'].get_many(['ford1', 'arthur1']), {}) - self.assertEqual(caches['v2'].get_many(['ford1', 'arthur1'], version=1), {'ford1': 37, 'arthur1': 42}) - self.assertEqual(caches['v2'].get_many(['ford1', 'arthur1'], version=2), {}) + self.assertEqual( + caches['v2'].get_many(['ford1', 'arthur1'], version=1), + {'ford1': 37, 'arthur1': 42}, + ) + self.assertEqual( + caches['v2'].get_many(['ford1', 'arthur1'], version=2), {} + ) # set, default version = 1, but manually override version = 2 cache.set_many({'ford2': 37, 'arthur2': 42}, version=2) self.assertEqual(cache.get_many(['ford2', 'arthur2']), {}) self.assertEqual(cache.get_many(['ford2', 'arthur2'], version=1), {}) - self.assertEqual(cache.get_many(['ford2', 'arthur2'], version=2), {'ford2': 37, 'arthur2': 42}) + self.assertEqual( + cache.get_many(['ford2', 'arthur2'], version=2), + {'ford2': 37, 'arthur2': 42}, + ) - self.assertEqual(caches['v2'].get_many(['ford2', 'arthur2']), {'ford2': 37, 'arthur2': 42}) - self.assertEqual(caches['v2'].get_many(['ford2', 'arthur2'], version=1), {}) - self.assertEqual(caches['v2'].get_many(['ford2', 'arthur2'], version=2), {'ford2': 37, 'arthur2': 42}) + self.assertEqual( + caches['v2'].get_many(['ford2', 'arthur2']), + {'ford2': 37, 'arthur2': 42}, + ) + self.assertEqual( + caches['v2'].get_many(['ford2', 'arthur2'], version=1), {} + ) + self.assertEqual( + caches['v2'].get_many(['ford2', 'arthur2'], version=2), + {'ford2': 37, 'arthur2': 42}, + ) # v2 set, using default version = 2 caches['v2'].set_many({'ford3': 37, 'arthur3': 42}) self.assertEqual(cache.get_many(['ford3', 'arthur3']), {}) self.assertEqual(cache.get_many(['ford3', 'arthur3'], version=1), {}) - self.assertEqual(cache.get_many(['ford3', 'arthur3'], version=2), {'ford3': 37, 'arthur3': 42}) + self.assertEqual( + cache.get_many(['ford3', 'arthur3'], version=2), + {'ford3': 37, 'arthur3': 42}, + ) - self.assertEqual(caches['v2'].get_many(['ford3', 'arthur3']), {'ford3': 37, 'arthur3': 42}) - self.assertEqual(caches['v2'].get_many(['ford3', 'arthur3'], version=1), {}) - self.assertEqual(caches['v2'].get_many(['ford3', 'arthur3'], version=2), {'ford3': 37, 'arthur3': 42}) + self.assertEqual( + caches['v2'].get_many(['ford3', 'arthur3']), + {'ford3': 37, 'arthur3': 42}, + ) + self.assertEqual( + caches['v2'].get_many(['ford3', 'arthur3'], version=1), {} + ) + self.assertEqual( + caches['v2'].get_many(['ford3', 'arthur3'], version=2), + {'ford3': 37, 'arthur3': 42}, + ) # v2 set, default version = 2, but manually override version = 1 caches['v2'].set_many({'ford4': 37, 'arthur4': 42}, version=1) - self.assertEqual(cache.get_many(['ford4', 'arthur4']), {'ford4': 37, 'arthur4': 42}) - self.assertEqual(cache.get_many(['ford4', 'arthur4'], version=1), {'ford4': 37, 'arthur4': 42}) + self.assertEqual( + cache.get_many(['ford4', 'arthur4']), {'ford4': 37, 'arthur4': 42} + ) + self.assertEqual( + cache.get_many(['ford4', 'arthur4'], version=1), + {'ford4': 37, 'arthur4': 42}, + ) self.assertEqual(cache.get_many(['ford4', 'arthur4'], version=2), {}) self.assertEqual(caches['v2'].get_many(['ford4', 'arthur4']), {}) - self.assertEqual(caches['v2'].get_many(['ford4', 'arthur4'], version=1), {'ford4': 37, 'arthur4': 42}) - self.assertEqual(caches['v2'].get_many(['ford4', 'arthur4'], version=2), {}) + self.assertEqual( + caches['v2'].get_many(['ford4', 'arthur4'], version=1), + {'ford4': 37, 'arthur4': 42}, + ) + self.assertEqual( + caches['v2'].get_many(['ford4', 'arthur4'], version=2), {} + ) def test_incr_version(self): cache.set('answer', 42, version=2) @@ -825,7 +897,9 @@ def test_get_or_set_version(self): self.assertIsNone(cache.get('brian', version=3)) def test_get_or_set_racing(self): - with mock.patch('%s.%s' % (settings.CACHES['default']['BACKEND'], 'add')) as cache_add: + with mock.patch( + '%s.%s' % (settings.CACHES['default']['BACKEND'], 'add') + ) as cache_add: # Simulate cache.add() failing to add a value. In that case, the # default value should be returned. cache_add.return_value = False @@ -833,7 +907,6 @@ def test_get_or_set_racing(self): class PicklingSideEffect: - def __init__(self, cache): self.cache = cache self.locked = False @@ -843,11 +916,14 @@ def __getstate__(self): return {} -@override_settings(CACHES=caches_setting_for_tests( - BACKEND='diskcache.DjangoCache', -)) +@override_settings( + CACHES=caches_setting_for_tests( + BACKEND='diskcache.DjangoCache', + ) +) class DiskCacheTests(BaseCacheTests, TestCase): "Specific test cases for diskcache.DjangoCache." + def setUp(self): super().setUp() self.dirname = tempfile.mkdtemp() @@ -868,14 +944,18 @@ def test_ignores_non_cache_files(self): with open(fname, 'w'): os.utime(fname, None) cache.clear() - self.assertTrue(os.path.exists(fname), - 'Expected cache.clear to ignore non cache files') + self.assertTrue( + os.path.exists(fname), + 'Expected cache.clear to ignore non cache files', + ) os.remove(fname) def test_clear_does_not_remove_cache_dir(self): cache.clear() - self.assertTrue(os.path.exists(self.dirname), - 'Expected cache.clear to keep the cache dir') + self.assertTrue( + os.path.exists(self.dirname), + 'Expected cache.clear to keep the cache dir', + ) def test_cache_write_unpicklable_type(self): # This fails if not using the highest pickling protocol on Python 2. @@ -942,7 +1022,9 @@ def test_pop(self): self.assertEqual(cache.pop(0, default=1), 1) self.assertEqual(cache.pop(1, expire_time=True), (1, None)) self.assertEqual(cache.pop(2, tag=True), (2, None)) - self.assertEqual(cache.pop(3, expire_time=True, tag=True), (3, None, None)) + self.assertEqual( + cache.pop(3, expire_time=True, tag=True), (3, None, None) + ) self.assertEqual(cache.pop(4, retry=False), 4) def test_pickle(self): @@ -975,6 +1057,7 @@ def test_index(self): def test_memoize(self): with self.assertRaises(TypeError): + @cache.memoize # <-- Missing parens! def test(): pass diff --git a/tests/test_recipes.py b/tests/test_recipes.py index b26a239..2d53d49 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -31,11 +31,13 @@ def test_averager(cache): def test_rlock(cache): state = {'num': 0} rlock = dc.RLock(cache, 'demo') + def worker(): state['num'] += 1 with rlock: state['num'] += 1 time.sleep(0.1) + with rlock: thread = threading.Thread(target=worker) thread.start() @@ -48,11 +50,13 @@ def worker(): def test_semaphore(cache): state = {'num': 0} semaphore = dc.BoundedSemaphore(cache, 'demo', value=3) + def worker(): state['num'] += 1 with semaphore: state['num'] += 1 time.sleep(0.1) + semaphore.acquire() semaphore.acquire() with semaphore: @@ -68,11 +72,13 @@ def worker(): def test_memoize_stampede(cache): state = {'num': 0} + @dc.memoize_stampede(cache, 0.1) def worker(num): time.sleep(0.01) state['num'] += 1 return num + start = time.time() while (time.time() - start) < 1: worker(100) diff --git a/tests/utils.py b/tests/utils.py index 47da791..504b930 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -96,16 +96,19 @@ def display(name, timings): len_total += len(values) sum_total += sum(values) - print(template % ( - action, - len(values), - len(timings.get(action + '-miss', [])), - secs(percentile(values, 0.5)), - secs(percentile(values, 0.9)), - secs(percentile(values, 0.99)), - secs(percentile(values, 1.0)), - secs(sum(values)), - )) + print( + template + % ( + action, + len(values), + len(timings.get(action + '-miss', [])), + secs(percentile(values, 0.5)), + secs(percentile(values, 0.9)), + secs(percentile(values, 0.99)), + secs(percentile(values, 1.0)), + secs(sum(values)), + ) + ) totals = ('Total', len_total, '', '', '', '', '', secs(sum_total)) print(template % totals) From 3b87dde40b4d0b2a7f96534110b5103ce7c9e1fc Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:27:24 -0800 Subject: [PATCH 114/211] Make imports consistent with isort --- diskcache/__init__.py | 18 ++++++++++++++---- diskcache/djangocache.py | 1 + diskcache/fanout.py | 2 +- diskcache/persistent.py | 12 ++++++++---- setup.py | 1 + tests/benchmark_kv_store.py | 4 ++-- tests/issue_109.py | 1 + tests/issue_85.py | 3 ++- tests/plot.py | 3 ++- tests/plot_early_recompute.py | 3 ++- tests/stress_test_core.py | 3 ++- tests/stress_test_fanout.py | 3 ++- tests/test_core.py | 4 ++-- tests/test_deque.py | 4 ++-- tests/test_djangocache.py | 1 - tests/test_fanout.py | 4 ++-- tests/test_index.py | 4 ++-- tests/test_recipes.py | 7 ++++--- 18 files changed, 50 insertions(+), 28 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 0300123..befed76 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -7,18 +7,28 @@ """ from .core import ( + DEFAULT_SETTINGS, + ENOVAL, + EVICTION_POLICY, + UNKNOWN, Cache, Disk, EmptyDirWarning, JSONDisk, - UnknownFileWarning, Timeout, + UnknownFileWarning, ) -from .core import DEFAULT_SETTINGS, ENOVAL, EVICTION_POLICY, UNKNOWN from .fanout import FanoutCache from .persistent import Deque, Index -from .recipes import Averager, BoundedSemaphore, Lock, RLock -from .recipes import barrier, memoize_stampede, throttle +from .recipes import ( + Averager, + BoundedSemaphore, + Lock, + RLock, + barrier, + memoize_stampede, + throttle, +) __all__ = [ 'Averager', diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index 2f1db07..44f673d 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -1,6 +1,7 @@ "Django-compatible disk and file backed cache." from functools import wraps + from django.core.cache.backends.base import BaseCache try: diff --git a/diskcache/fanout.py b/diskcache/fanout.py index cc40c07..e7987b4 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -9,7 +9,7 @@ import tempfile import time -from .core import ENOVAL, DEFAULT_SETTINGS, Cache, Disk, Timeout +from .core import DEFAULT_SETTINGS, ENOVAL, Cache, Disk, Timeout from .persistent import Deque, Index diff --git a/diskcache/persistent.py b/diskcache/persistent.py index ac5a5b0..5ff1939 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -3,14 +3,18 @@ """ import operator as op - from collections import OrderedDict -from collections.abc import MutableMapping, Sequence -from collections.abc import KeysView, ValuesView, ItemsView +from collections.abc import ( + ItemsView, + KeysView, + MutableMapping, + Sequence, + ValuesView, +) from contextlib import contextmanager from shutil import rmtree -from .core import Cache, ENOVAL +from .core import ENOVAL, Cache def _make_compare(seq_op, doc): diff --git a/setup.py b/setup.py index 2d6daa3..5f3cd88 100644 --- a/setup.py +++ b/setup.py @@ -1,4 +1,5 @@ from io import open + from setuptools import setup from setuptools.command.test import test as TestCommand diff --git a/tests/benchmark_kv_store.py b/tests/benchmark_kv_store.py index e214f1a..e141a36 100644 --- a/tests/benchmark_kv_store.py +++ b/tests/benchmark_kv_store.py @@ -4,10 +4,10 @@ """ -import diskcache - from IPython import get_ipython +import diskcache + ipython = get_ipython() assert ipython is not None, 'No IPython! Run with $ ipython ...' diff --git a/tests/issue_109.py b/tests/issue_109.py index a5355b1..c10a81f 100644 --- a/tests/issue_109.py +++ b/tests/issue_109.py @@ -3,6 +3,7 @@ """ import time + import diskcache as dc diff --git a/tests/issue_85.py b/tests/issue_85.py index db32046..b325df0 100644 --- a/tests/issue_85.py +++ b/tests/issue_85.py @@ -6,7 +6,6 @@ """ import collections -import django import os import random import shutil @@ -14,6 +13,8 @@ import threading import time +import django + def remove_cache_dir(): print('REMOVING CACHE DIRECTORY') diff --git a/tests/plot.py b/tests/plot.py index 2f83f14..670bf49 100644 --- a/tests/plot.py +++ b/tests/plot.py @@ -7,10 +7,11 @@ import argparse import collections as co -import matplotlib.pyplot as plt import re import sys +import matplotlib.pyplot as plt + def parse_timing(timing, limit): "Parse timing." diff --git a/tests/plot_early_recompute.py b/tests/plot_early_recompute.py index da46e3e..7096ea0 100644 --- a/tests/plot_early_recompute.py +++ b/tests/plot_early_recompute.py @@ -2,13 +2,14 @@ """ -import diskcache as dc import functools as ft import multiprocessing.pool import shutil import threading import time +import diskcache as dc + def make_timer(times): """Make a decorator which accumulates (start, end) in `times` for function diff --git a/tests/stress_test_core.py b/tests/stress_test_core.py index a36ae4e..6c48d5c 100644 --- a/tests/stress_test_core.py +++ b/tests/stress_test_core.py @@ -1,7 +1,6 @@ "Stress test diskcache.core.Cache." import collections as co -from diskcache import Cache, UnknownFileWarning, EmptyDirWarning, Timeout import multiprocessing as mp import os import pickle @@ -13,6 +12,8 @@ import time import warnings +from diskcache import Cache, EmptyDirWarning, Timeout, UnknownFileWarning + from .utils import display OPERATIONS = int(1e4) diff --git a/tests/stress_test_fanout.py b/tests/stress_test_fanout.py index fc2de39..1f29987 100644 --- a/tests/stress_test_fanout.py +++ b/tests/stress_test_fanout.py @@ -1,7 +1,6 @@ "Stress test diskcache.core.Cache." import collections as co -from diskcache import FanoutCache, UnknownFileWarning, EmptyDirWarning import multiprocessing as mp import os import pickle @@ -13,6 +12,8 @@ import time import warnings +from diskcache import EmptyDirWarning, FanoutCache, UnknownFileWarning + from .utils import display OPERATIONS = int(1e4) diff --git a/tests/test_core.py b/tests/test_core.py index cb4e34f..6fe50c1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -8,7 +8,6 @@ import os import os.path as op import pickle -import pytest import random import shutil import sqlite3 @@ -19,9 +18,10 @@ import time import unittest import warnings - from unittest import mock +import pytest + import diskcache as dc pytestmark = pytest.mark.filterwarnings('ignore', category=dc.EmptyDirWarning) diff --git a/tests/test_deque.py b/tests/test_deque.py index 1ca966d..bfb9e7d 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -2,12 +2,12 @@ import functools as ft import pickle -import pytest import shutil import tempfile - from unittest import mock +import pytest + import diskcache as dc from diskcache.core import ENOVAL diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index 5cb0047..2c216fe 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -14,7 +14,6 @@ import time import unittest import warnings - from unittest import mock from django.conf import settings diff --git a/tests/test_fanout.py b/tests/test_fanout.py index aa3c833..effe47d 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -8,7 +8,6 @@ import os import os.path as op import pickle -import pytest import random import shutil import sqlite3 @@ -18,9 +17,10 @@ import threading import time import warnings - from unittest import mock +import pytest + import diskcache as dc warnings.simplefilter('error') diff --git a/tests/test_index.py b/tests/test_index.py index 575558c..8852e99 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -2,13 +2,13 @@ import functools as ft import pickle -import pytest import shutil import sys import tempfile - from unittest import mock +import pytest + import diskcache as dc diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 2d53d49..efaa47c 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -1,13 +1,14 @@ "Test diskcache.recipes." -import diskcache as dc -import pytest import shutil import threading import time - from unittest import mock +import pytest + +import diskcache as dc + @pytest.fixture def cache(): From c40b57facd5d9d167a645cb55a4b69257ef20299 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:50:33 -0800 Subject: [PATCH 115/211] Increase max attributes to 8 --- .pylintrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pylintrc b/.pylintrc index 3314897..8458ed4 100644 --- a/.pylintrc +++ b/.pylintrc @@ -541,7 +541,7 @@ valid-metaclass-classmethod-first-arg=cls max-args=8 # Maximum number of attributes for a class (see R0902). -max-attributes=7 +max-attributes=8 # Maximum number of boolean expressions in an if statement. max-bool-expr=5 From 58227d18dbdd885fd27ba74a9a34e74fb72222d3 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:50:46 -0800 Subject: [PATCH 116/211] Tell mypy to ignore django --- mypy.ini | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..053b283 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,4 @@ +[mypy] + +[mypy-django.*] +ignore_missing_imports = True From db76f111e43ebacc392443b861565bfed14e87b7 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:50:58 -0800 Subject: [PATCH 117/211] Remove useless `dataset` target --- README.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/README.rst b/README.rst index 847b2cf..ba2bf3e 100644 --- a/README.rst +++ b/README.rst @@ -314,7 +314,6 @@ Object Relational Mappings (ORM) .. _`Django ORM`: https://docs.djangoproject.com/en/dev/topics/db/ .. _`SQLAlchemy`: https://www.sqlalchemy.org/ .. _`Peewee`: http://docs.peewee-orm.com/ -.. _`dataset`: https://dataset.readthedocs.io/ .. _`SQLObject`: http://sqlobject.org/ .. _`Pony ORM`: https://ponyorm.com/ From b2c0ae421ccdf332698431f1dd29eb6e9936d375 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:51:57 -0800 Subject: [PATCH 118/211] Ignore type errors when setting class attributes --- diskcache/fanout.py | 2 +- diskcache/persistent.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index e7987b4..6a13d6e 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -664,4 +664,4 @@ def index(self, name): return index -FanoutCache.memoize = Cache.memoize +FanoutCache.memoize = Cache.memoize # type: ignore diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 5ff1939..44f9cc7 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -591,7 +591,7 @@ def rotate(self, steps=1): else: self.append(value) - __hash__ = None + __hash__ = None # type: ignore @contextmanager def transact(self): @@ -1033,7 +1033,7 @@ def items(self): """ return ItemsView(self) - __hash__ = None + __hash__ = None # type: ignore def __getstate__(self): return self.directory From 585f47f1084581e8d2fa0aed8ce0dd8a2e0a9162 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:52:58 -0800 Subject: [PATCH 119/211] Add django to deps for sphinx and pylint --- tox.ini | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 6bbe69c..356a576 100644 --- a/tox.ini +++ b/tox.ini @@ -28,7 +28,9 @@ commands=doc8 docs allowlist_externals=make changedir=docs commands=make html -deps=sphinx +deps= + django==2.2.* + sphinx [testenv:flake8] commands=flake8 {toxinidir}/setup.py {toxinidir}/diskcache {toxinidir}/tests @@ -48,7 +50,9 @@ deps=mypy [testenv:pylint] commands=pylint {toxinidir}/diskcache -deps=pylint +deps= + django==2.2.* + pylint [testenv:rstcheck] commands=rstcheck {toxinidir}/README.rst From 945ee10ca54871032196a0b468ce17d8e0711dcb Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:53:14 -0800 Subject: [PATCH 120/211] Tell doc8 to ignore docs/_build dir --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 356a576..de47de5 100644 --- a/tox.ini +++ b/tox.ini @@ -22,7 +22,7 @@ deps=blue [testenv:doc8] deps=doc8 -commands=doc8 docs +commands=doc8 docs --ignore-path docs/_build [testenv:docs] allowlist_externals=make From 4bbe73fb251830a2b2e7d78c21a67f2ab9b18804 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:53:36 -0800 Subject: [PATCH 121/211] Update flake8 configs --- tox.ini | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/tox.ini b/tox.ini index de47de5..d5f43c0 100644 --- a/tox.ini +++ b/tox.ini @@ -91,7 +91,6 @@ addopts= # ignore=D000 [flake8] -# ignore= -# E124 -# E303 -# W503 +exclude=tests/test_djangocache.py +extend-ignore=E203 +max-line-length=120 From 99f0ca2439f6c98ded51d555658b7d538f09010c Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 11:53:46 -0800 Subject: [PATCH 122/211] Flake8 fixes (mostly removing useless module imports) --- tests/benchmark_core.py | 9 ++++----- tests/benchmark_djangocache.py | 10 ++++------ tests/issue_85.py | 2 +- tests/plot.py | 2 +- tests/plot_early_recompute.py | 2 +- tests/settings_benchmark.py | 4 ++-- tests/stress_test_core.py | 7 +++---- tests/stress_test_deque.py | 1 - tests/stress_test_deque_mp.py | 1 - tests/stress_test_fanout.py | 4 +--- tests/test_core.py | 9 ++------- tests/test_deque.py | 5 ----- tests/test_doctest.py | 2 -- tests/test_fanout.py | 9 ++------- tests/test_index.py | 3 --- tests/test_recipes.py | 1 - tests/utils.py | 19 ------------------- 17 files changed, 21 insertions(+), 69 deletions(-) diff --git a/tests/benchmark_core.py b/tests/benchmark_core.py index a048b79..282ce2a 100644 --- a/tests/benchmark_core.py +++ b/tests/benchmark_core.py @@ -12,7 +12,6 @@ import pickle import random import shutil -import sys import time import warnings @@ -30,7 +29,7 @@ # Disk Cache Benchmarks ############################################################################### -import diskcache +import diskcache # noqa caches.append( ( @@ -123,13 +122,13 @@ def worker(num, kind, args, kwargs): start = time.time() result = obj.set(key, value) end = time.time() - miss = result == False + miss = result is False action = 'set' else: start = time.time() result = obj.delete(key) end = time.time() - miss = result == False + miss = result is False action = 'delete' if count > WARMUP: @@ -154,7 +153,7 @@ def dispatch(): try: obj.close() - except: + except Exception: pass processes = [ diff --git a/tests/benchmark_djangocache.py b/tests/benchmark_djangocache.py index 0de0424..9dbbcd7 100644 --- a/tests/benchmark_djangocache.py +++ b/tests/benchmark_djangocache.py @@ -12,9 +12,7 @@ import pickle import random import shutil -import sys import time -import warnings from utils import display @@ -59,13 +57,13 @@ def worker(num, name): start = time.time() result = obj.set(key, value) end = time.time() - miss = result == False + miss = result is False action = 'set' else: start = time.time() result = obj.delete(key) end = time.time() - miss = result == False + miss = result is False action = 'delete' if count > WARMUP: @@ -91,14 +89,14 @@ def prepare(name): try: obj.close() - except: + except Exception: pass def dispatch(): setup() - from django.core.cache import caches + from django.core.cache import caches # noqa for name in ['locmem', 'memcached', 'redis', 'diskcache', 'filebased']: shutil.rmtree('tmp', ignore_errors=True) diff --git a/tests/issue_85.py b/tests/issue_85.py index b325df0..723406b 100644 --- a/tests/issue_85.py +++ b/tests/issue_85.py @@ -109,7 +109,7 @@ def run(statements): shard._sql(statement) if index == 0: values.append(('BEGIN', ident)) - except sqlite3.OperationalError as exc: + except sqlite3.OperationalError: values.append(('ERROR', ident)) diff --git a/tests/plot.py b/tests/plot.py index 670bf49..2138659 100644 --- a/tests/plot.py +++ b/tests/plot.py @@ -48,7 +48,7 @@ def parse_data(infile): if blocks.match(line): try: name = title.match(lines[index + 1]).group(1) - except: + except Exception: index += 1 continue diff --git a/tests/plot_early_recompute.py b/tests/plot_early_recompute.py index 7096ea0..e58f580 100644 --- a/tests/plot_early_recompute.py +++ b/tests/plot_early_recompute.py @@ -22,7 +22,7 @@ def timer(func): @ft.wraps(func) def wrapper(*args, **kwargs): start = time.time() - result = func(*args, **kwargs) + func(*args, **kwargs) pair = start, time.time() with lock: times.append(pair) diff --git a/tests/settings_benchmark.py b/tests/settings_benchmark.py index 1724cef..c734e68 100644 --- a/tests/settings_benchmark.py +++ b/tests/settings_benchmark.py @@ -1,9 +1,9 @@ -from .settings import * +from .settings import * # noqa CACHES = { 'default': { 'BACKEND': 'diskcache.DjangoCache', - 'LOCATION': CACHE_DIR, + 'LOCATION': CACHE_DIR, # noqa }, 'memcached': { 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache', diff --git a/tests/stress_test_core.py b/tests/stress_test_core.py index 6c48d5c..6fd0991 100644 --- a/tests/stress_test_core.py +++ b/tests/stress_test_core.py @@ -7,7 +7,6 @@ import queue import random import shutil -import sys import threading import time import warnings @@ -196,7 +195,7 @@ def dispatch(num, eviction_policy, processes, threads): for thread_queue in thread_queues: thread_queue.put(None) - start = time.time() + # start = time.time() for thread in subthreads: thread.start() @@ -204,7 +203,7 @@ def dispatch(num, eviction_policy, processes, threads): for thread in subthreads: thread.join() - stop = time.time() + # stop = time.time() timings = co.defaultdict(list) @@ -396,7 +395,7 @@ def stress_test_mp(): parser.add_argument( '-v', '--eviction-policy', - type=unicode, + type=str, default=u'least-recently-stored', ) diff --git a/tests/stress_test_deque.py b/tests/stress_test_deque.py index 7b3ac2f..845b2c2 100644 --- a/tests/stress_test_deque.py +++ b/tests/stress_test_deque.py @@ -2,7 +2,6 @@ import collections as co import functools as ft -import itertools as it import random import diskcache as dc diff --git a/tests/stress_test_deque_mp.py b/tests/stress_test_deque_mp.py index db7b5c4..091ff0e 100644 --- a/tests/stress_test_deque_mp.py +++ b/tests/stress_test_deque_mp.py @@ -1,6 +1,5 @@ """Stress test diskcache.persistent.Deque.""" -import functools as ft import itertools as it import multiprocessing as mp import os diff --git a/tests/stress_test_fanout.py b/tests/stress_test_fanout.py index 1f29987..58708c9 100644 --- a/tests/stress_test_fanout.py +++ b/tests/stress_test_fanout.py @@ -1,13 +1,11 @@ "Stress test diskcache.core.Cache." -import collections as co import multiprocessing as mp import os import pickle import queue import random import shutil -import sys import threading import time import warnings @@ -388,7 +386,7 @@ def stress_test_mp(): parser.add_argument( '-v', '--eviction-policy', - type=unicode, + type=str, default=u'least-recently-stored', ) diff --git a/tests/test_core.py b/tests/test_core.py index 6fe50c1..bcc79e1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,22 +1,17 @@ "Test diskcache.core.Cache." -import collections as co import errno -import functools as ft import hashlib import io import os import os.path as op import pickle -import random import shutil import sqlite3 import subprocess as sp -import sys import tempfile import threading import time -import unittest import warnings from unittest import mock @@ -130,7 +125,7 @@ def test_init_makedirs(): with pytest.raises(EnvironmentError): try: with mock.patch('os.makedirs', makedirs): - cache = dc.Cache(cache_dir) + dc.Cache(cache_dir) except EnvironmentError: shutil.rmtree(cache_dir, ignore_errors=True) raise @@ -244,7 +239,7 @@ def test_read(cache): def test_read_keyerror(cache): with pytest.raises(KeyError): - with cache.read(0) as reader: + with cache.read(0): pass diff --git a/tests/test_deque.py b/tests/test_deque.py index bfb9e7d..8113dfe 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -1,6 +1,5 @@ "Test diskcache.persistent.Deque." -import functools as ft import pickle import shutil import tempfile @@ -278,7 +277,3 @@ def test_rotate_indexerror_negative(deque): with mock.patch.object(deque, '_cache', cache): deque.rotate(-1) - - -def test_repr(deque): - assert repr(deque).startswith('Deque(') diff --git a/tests/test_doctest.py b/tests/test_doctest.py index 70fa61c..822d8db 100644 --- a/tests/test_doctest.py +++ b/tests/test_doctest.py @@ -1,6 +1,4 @@ import doctest -import shutil -import sys import diskcache.core import diskcache.djangocache diff --git a/tests/test_fanout.py b/tests/test_fanout.py index effe47d..8918af3 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -1,18 +1,13 @@ "Test diskcache.fanout.FanoutCache." import collections as co -import errno -import functools as ft import hashlib import io import os import os.path as op import pickle -import random import shutil -import sqlite3 import subprocess as sp -import sys import tempfile import threading import time @@ -67,7 +62,7 @@ def test_set_get_delete(cache): for value in range(100): assert cache.delete(value) - assert cache.delete(100) == False + assert cache.delete(100) is False cache.check() @@ -353,7 +348,7 @@ def test_read(cache): def test_read_keyerror(cache): with pytest.raises(KeyError): - with cache.read(0) as reader: + with cache.read(0): pass diff --git a/tests/test_index.py b/tests/test_index.py index 8852e99..27639f7 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,11 +1,8 @@ "Test diskcache.persistent.Index." -import functools as ft import pickle import shutil -import sys import tempfile -from unittest import mock import pytest diff --git a/tests/test_recipes.py b/tests/test_recipes.py index efaa47c..8d13f2b 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -3,7 +3,6 @@ import shutil import threading import time -from unittest import mock import pytest diff --git a/tests/utils.py b/tests/utils.py index 504b930..5b41ce9 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -18,7 +18,6 @@ def percentile(sequence, percent): def secs(value): units = ['s ', 'ms', 'us', 'ns'] - pos = 0 if value is None: return ' 0.000ns' @@ -60,24 +59,6 @@ def unmount_ramdisk(dev_path, path): run('rm', '-r', path) -def retry(sql, query): - pause = 0.001 - error = sqlite3.OperationalError - - for _ in range(int(LIMITS[u'timeout'] / pause)): - try: - sql(query).fetchone() - except sqlite3.OperationalError as exc: - error = exc - time.sleep(pause) - else: - break - else: - raise error - - del error - - def display(name, timings): cols = ('Action', 'Count', 'Miss', 'Median', 'P90', 'P99', 'Max', 'Total') template = ' '.join(['%9s'] * len(cols)) From 4b0fc2342d87f59ca1a024d3f74332dd53e3cb9e Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 12:16:28 -0800 Subject: [PATCH 123/211] Update pylint and fix code --- .pylintrc | 90 ++++++++++++++++++++++++++------------------ diskcache/core.py | 6 +-- diskcache/fanout.py | 2 +- diskcache/recipes.py | 8 ++-- 4 files changed, 61 insertions(+), 45 deletions(-) diff --git a/.pylintrc b/.pylintrc index 8458ed4..158fe34 100644 --- a/.pylintrc +++ b/.pylintrc @@ -5,6 +5,9 @@ # run arbitrary code. extension-pkg-whitelist= +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10.0 + # Add files or directories to the blacklist. They should be base names, not # paths. ignore=CVS @@ -26,16 +29,13 @@ jobs=1 # complex, nested conditions. limit-inference-results=100 -# List of plugins (as comma separated values of python modules names) to load, +# List of plugins (as comma separated values of python module names) to load, # usually to register additional checkers. load-plugins= # Pickle collected data for later comparisons. persistent=yes -# Specify a configuration file. -#rcfile= - # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes @@ -139,12 +139,10 @@ disable=print-statement, deprecated-sys-function, exception-escape, comprehension-escape, - no-else-return, no-member, - useless-object-inheritance, + no-else-return, + duplicate-code, inconsistent-return-statements, - ungrouped-imports, - not-callable, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option @@ -155,11 +153,11 @@ enable=c-extension-no-member [REPORTS] -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) # Template used to display messages. This is a python new-style format string @@ -181,7 +179,7 @@ score=yes [REFACTORING] # Maximum number of nested blocks for function / method body -max-nested-blocks=6 +max-nested-blocks=5 # Complete name of functions that never returns. When checking for # inconsistent-return-statements if a never returning function is called then @@ -192,8 +190,8 @@ never-returning-functions=sys.exit [LOGGING] -# Format style used to check logging format string. `old` means using % -# formatting, while `new` is for `{}` formatting. +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. logging-format-style=old # Logging modules to check that the string format arguments are in logging @@ -206,18 +204,18 @@ logging-modules=logging # Limits count of emitted suggestions for spelling mistakes. max-spelling-suggestions=4 -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package.. +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. spelling-dict= # List of comma separated words that should not be checked. spelling-ignore-words= -# A path to a file that contains private dictionary; one word per line. +# A path to a file that contains the private dictionary; one word per line. spelling-private-dict-file= -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. spelling-store-unknown-words=no @@ -228,6 +226,9 @@ notes=FIXME, XXX, TODO +# Regular expression of note tags to take in consideration. +#notes-rgx= + [TYPECHECK] @@ -264,7 +265,7 @@ ignored-classes=optparse.Values,thread._local,_thread._local # List of module names for which member attributes should not be checked # (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It +# and thus existing member attributes cannot be deduced by static analysis). It # supports qualified module names, as well as Unix pattern matching. ignored-modules= @@ -280,6 +281,9 @@ missing-member-hint-distance=1 # showing a hint for a missing member. missing-member-max-choices=1 +# List of decorators that change the signature of a decorated function. +signature-mutators= + [VARIABLES] @@ -330,14 +334,7 @@ indent-string=' ' max-line-length=100 # Maximum number of lines in a module. -max-module-lines=2500 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator +max-module-lines=3000 # Allow the body of a class to be on the same line as the declaration if body # contains single statement. @@ -357,10 +354,10 @@ ignore-comments=yes ignore-docstrings=yes # Ignore imports when computing similarities. -ignore-imports=no +ignore-imports=yes # Minimum lines number of a similarity. -min-similarity-lines=9 +min-similarity-lines=4 [BASIC] @@ -387,6 +384,10 @@ bad-names=foo, tutu, tata +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + # Naming style matching correct class attribute names. class-attribute-naming-style=any @@ -427,6 +428,10 @@ good-names=i, Run, _ +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + # Include a hint for the correct naming format with invalid-name. include-naming-hint=no @@ -474,14 +479,21 @@ variable-naming-style=snake_case [STRING] -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. check-str-concat-over-line-jumps=no [IMPORTS] +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + # Allow wildcard imports from modules that define __all__. allow-wildcard-with-all=no @@ -512,13 +524,17 @@ known-standard-library= # Force import order to recognize a module as part of a third party library. known-third-party=enchant +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + [CLASSES] # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, - setUp + setUp, + __post_init__ # List of member names, which should be excluded from the protected access # warning. @@ -543,7 +559,7 @@ max-args=8 # Maximum number of attributes for a class (see R0902). max-attributes=8 -# Maximum number of boolean expressions in an if statement. +# Maximum number of boolean expressions in an if statement (see R0916). max-bool-expr=5 # Maximum number of branch for function / method body. diff --git a/diskcache/core.py b/diskcache/core.py index 419c64f..da4d884 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -109,7 +109,7 @@ def __repr__(self): } -class Disk(object): +class Disk: "Cache key and value serialization for SQLite database and files." def __init__(self, directory, min_file_size=0, pickle_protocol=0): @@ -418,7 +418,7 @@ def args_to_key(base, args, kwargs, typed): return key -class Cache(object): +class Cache: "Disk and file backed cache." def __init__(self, directory=None, timeout=60, disk=Disk, **settings): @@ -2138,7 +2138,7 @@ def cull(self, retry=False): select_policy = EVICTION_POLICY[self.eviction_policy]['cull'] if select_policy is None: - return + return 0 select_filename = select_policy.format(fields='filename', now=now) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 6a13d6e..99384a0 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -13,7 +13,7 @@ from .persistent import Deque, Index -class FanoutCache(object): +class FanoutCache: "Cache that shards keys and values." def __init__( diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 3acddd8..8f7bd32 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -12,7 +12,7 @@ from .core import ENOVAL, args_to_key, full_name -class Averager(object): +class Averager: """Recipe for calculating a running average. Sometimes known as "online statistics," the running average maintains the @@ -63,7 +63,7 @@ def pop(self): return None if count == 0 else total / count -class Lock(object): +class Lock: """Recipe for cross-process and cross-thread lock. >>> import diskcache @@ -111,7 +111,7 @@ def __exit__(self, *exc_info): self.release() -class RLock(object): +class RLock: """Recipe for cross-process and cross-thread re-entrant lock. >>> import diskcache @@ -179,7 +179,7 @@ def __exit__(self, *exc_info): self.release() -class BoundedSemaphore(object): +class BoundedSemaphore: """Recipe for cross-process and cross-thread bounded semaphore. >>> import diskcache From c654aeda6a969eeadc5fd81e1de5faab24a832a4 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 12:31:18 -0800 Subject: [PATCH 124/211] Update Sphinx and re-gen conf.py --- docs/Makefile | 222 +++-------------------------------------- docs/conf.py | 271 +++++--------------------------------------------- docs/make.bat | 246 ++------------------------------------------- 3 files changed, 46 insertions(+), 693 deletions(-) diff --git a/docs/Makefile b/docs/Makefile index e3bd50b..d4bb2cb 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,216 +1,20 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . BUILDDIR = _build -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " applehelp to make an Apple Help Book" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - @echo " coverage to run coverage check of the documentation (if enabled)" - -.PHONY: clean -clean: - rm -rf $(BUILDDIR)/* - -.PHONY: html -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -.PHONY: dirhtml -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -.PHONY: singlehtml -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -.PHONY: pickle -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -.PHONY: json -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -.PHONY: htmlhelp -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -.PHONY: qthelp -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DiskCache.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DiskCache.qhc" - -.PHONY: applehelp -applehelp: - $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp - @echo - @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." - @echo "N.B. You won't be able to view it unless you put it in" \ - "~/Library/Documentation/Help or install it in your application" \ - "bundle." - -.PHONY: devhelp -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/DiskCache" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DiskCache" - @echo "# devhelp" - -.PHONY: epub -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -.PHONY: latex -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -.PHONY: latexpdf -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: latexpdfja -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -.PHONY: text -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -.PHONY: man -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -.PHONY: texinfo -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -.PHONY: info -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -.PHONY: gettext -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -.PHONY: changes -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -.PHONY: linkcheck -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -.PHONY: doctest -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -.PHONY: coverage -coverage: - $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage - @echo "Testing of coverage in the sources finished, look at the " \ - "results in $(BUILDDIR)/coverage/python.txt." + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -.PHONY: xml -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." +.PHONY: help Makefile -.PHONY: pseudoxml -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py index 2556188..402d3ad 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,31 +1,33 @@ -# -*- coding: utf-8 -*- +# Configuration file for the Sphinx documentation builder. # -# DiskCache documentation build configuration file, created by -# sphinx-quickstart on Wed Feb 10 20:20:15 2016. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html -import sys -import os +# -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. + +import os +import sys sys.path.insert(0, os.path.abspath('..')) + import diskcache -from diskcache import __version__ -# -- General configuration ------------------------------------------------ -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# -- Project information ----------------------------------------------------- + +project = 'DiskCache' +copyright = '2021, Grant Jenks' +author = 'Grant Jenks' + +# The full version, including alpha/beta/rc tags +release = diskcache.__version__ + + +# -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -38,77 +40,13 @@ # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'DiskCache' -copyright = u'2020, Grant Jenks' -author = u'Grant Jenks' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = __version__ -# The full version, including alpha/beta/rc tags. -release = version - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] - -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = True +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# -- Options for HTML output ---------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. @@ -130,43 +68,11 @@ 'github_type': 'star', } -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - # Custom sidebar templates, maps document names to template names. html_sidebars = { '**': [ @@ -178,134 +84,5 @@ ] } -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Language to be used for generating the HTML full-text search index. -# Sphinx supports the following languages: -# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' -# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' -#html_search_language = 'en' - -# A dictionary with options for the search language support, empty by default. -# Now only 'ja' uses this config value -#html_search_options = {'type': 'default'} - -# The name of a javascript file (relative to the configuration directory) that -# implements a search results scorer. If empty, the default will be used. -#html_search_scorer = 'scorer.js' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'DiskCacheDoc' - def setup(app): - app.add_stylesheet('custom.css') - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', - -# Latex figure (float) alignment -#'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'DiskCache.tex', u'DiskCache Documentation', - u'Grant Jenks', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'diskcache', u'DiskCache Documentation', - [author], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'DiskCache', u'DiskCache Documentation', - author, 'DiskCache', 'Disk and file backed cache.', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False + app.add_css_file('custom.css') diff --git a/docs/make.bat b/docs/make.bat index e1a063b..2119f51 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,62 +1,18 @@ @ECHO OFF +pushd %~dp0 + REM Command file for Sphinx documentation if "%SPHINXBUILD%" == "" ( set SPHINXBUILD=sphinx-build ) +set SOURCEDIR=. set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -set I18NSPHINXOPTS=%SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) if "%1" == "" goto help -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - echo. coverage to run coverage check of the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -REM Check if sphinx-build is available and fallback to Python version if any -%SPHINXBUILD% 1>NUL 2>NUL -if errorlevel 9009 goto sphinx_python -goto sphinx_ok - -:sphinx_python - -set SPHINXBUILD=python -m sphinx.__init__ -%SPHINXBUILD% 2> nul +%SPHINXBUILD% >NUL 2>NUL if errorlevel 9009 ( echo. echo.The 'sphinx-build' command was not found. Make sure you have Sphinx @@ -69,195 +25,11 @@ if errorlevel 9009 ( exit /b 1 ) -:sphinx_ok - - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DiskCache.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DiskCache.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %~dp0 - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "coverage" ( - %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage - if errorlevel 1 exit /b 1 - echo. - echo.Testing of coverage in the sources finished, look at the ^ -results in %BUILDDIR%/coverage/python.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% :end +popd From d1cf8c528ae21fc8d2dff7afe91299df7e5cd1f7 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 12:32:45 -0800 Subject: [PATCH 125/211] Update copyright year --- LICENSE | 2 +- README.rst | 4 ++-- diskcache/__init__.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index d31985f..ca80a22 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2016-2020 Grant Jenks +Copyright 2016-2021 Grant Jenks Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the diff --git a/README.rst b/README.rst index ba2bf3e..969ea10 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ DiskCache: Disk Backed Cache `DiskCache`_ is an Apache2 licensed disk and file backed cache library, written in pure-Python, and compatible with Django. -The cloud-based computing of 2020 puts a premium on memory. Gigabytes of empty +The cloud-based computing of 2021 puts a premium on memory. Gigabytes of empty space is left on disks as processes vie for memory. Among these processes is Memcached (and sometimes Redis) which is used as a cache. Wouldn't it be nice to leverage empty disk space for caching? @@ -387,7 +387,7 @@ Reference License ------- -Copyright 2016-2020 Grant Jenks +Copyright 2016-2021 Grant Jenks Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the diff --git a/diskcache/__init__.py b/diskcache/__init__.py index befed76..c050a17 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -66,4 +66,4 @@ __build__ = 0x050100 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' -__copyright__ = 'Copyright 2016-2020 Grant Jenks' +__copyright__ = 'Copyright 2016-2021 Grant Jenks' From 6df650543aa3834af84346aefd4be85b74d993af Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 12:34:12 -0800 Subject: [PATCH 126/211] Update readme badges and CI notes --- README.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 969ea10..b7e1ee2 100644 --- a/README.rst +++ b/README.rst @@ -77,16 +77,16 @@ Features - Thread-safe and process-safe - Supports multiple eviction policies (LRU and LFU included) - Keys support "tag" metadata and eviction -- Developed on Python 3.8 -- Tested on CPython 3.5, 3.6, 3.7, 3.8 +- Developed on Python 3.9 +- Tested on CPython 3.6, 3.7, 3.8, 3.9 - Tested on Linux, Mac OS X, and Windows -- Tested using Travis CI and AppVeyor CI +- Tested using GitHub Actions -.. image:: https://api.travis-ci.org/grantjenks/python-diskcache.svg?branch=master - :target: http://www.grantjenks.com/docs/diskcache/ +.. image:: https://github.com/grantjenks/python-diskcache/workflows/integration/badge.svg + :target: https://github.com/grantjenks/python-diskcache/actions?query=workflow%3Aintegration -.. image:: https://ci.appveyor.com/api/projects/status/github/grantjenks/python-diskcache?branch=master&svg=true - :target: http://www.grantjenks.com/docs/diskcache/ +.. image:: https://github.com/grantjenks/python-diskcache/workflows/release/badge.svg + :target: https://github.com/grantjenks/python-diskcache/actions?query=workflow%3Arelease Quickstart ---------- From 72127020640a383149b643933e89c7ebc577cc5f Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 13:11:21 -0800 Subject: [PATCH 127/211] Pin jedi for ipython --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index a9023a1..2ab91c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ django_redis doc8 flake8 ipython +jedi==0.17.* # Remove after IPython bug fixed. pickleDB pylibmc pylint From e49358a20ee359614a39d94047b27b9cfaf4ab1a Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 13:33:58 -0800 Subject: [PATCH 128/211] Skip help() examples when running doctest --- README.rst | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index b7e1ee2..acf6803 100644 --- a/README.rst +++ b/README.rst @@ -99,7 +99,7 @@ You can access documentation in the interpreter with Python's built-in help function:: >>> import diskcache - >>> help(diskcache) + >>> help(diskcache) # doctest: +SKIP The core of `DiskCache`_ is three data types intended for caching. `Cache`_ objects manage a SQLite database and filesystem directory to store key and @@ -107,26 +107,26 @@ value pairs. `FanoutCache`_ provides a sharding layer to utilize multiple caches and `DjangoCache`_ integrates that with `Django`_:: >>> from diskcache import Cache, FanoutCache, DjangoCache - >>> help(Cache) - >>> help(FanoutCache) - >>> help(DjangoCache) + >>> help(Cache) # doctest: +SKIP + >>> help(FanoutCache) # doctest: +SKIP + >>> help(DjangoCache) # doctest: +SKIP Built atop the caching data types, are `Deque`_ and `Index`_ which work as a cross-process, persistent replacements for Python's ``collections.deque`` and ``dict``. These implement the sequence and mapping container base classes:: >>> from diskcache import Deque, Index - >>> help(Deque) - >>> help(Index) + >>> help(Deque) # doctest: +SKIP + >>> help(Index) # doctest: +SKIP Finally, a number of `recipes`_ for cross-process synchronization are provided using an underlying cache. Features like memoization with cache stampede prevention, cross-process locking, and cross-process throttling are available:: >>> from diskcache import memoize_stampede, Lock, throttle - >>> help(memoize_stampede) - >>> help(Lock) - >>> help(throttle) + >>> help(memoize_stampede) # doctest: +SKIP + >>> help(Lock) # doctest: +SKIP + >>> help(throttle) # doctest: +SKIP Python's docstrings are a quick way to get started but not intended as a replacement for the `DiskCache Tutorial`_ and `DiskCache API Reference`_. From 0d41722a030695f4d988ac21383513c52f2aa073 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 13:34:44 -0800 Subject: [PATCH 129/211] Fix configs for pytest --- tox.ini | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tox.ini b/tox.ini index d5f43c0..acf7b22 100644 --- a/tox.ini +++ b/tox.ini @@ -7,10 +7,12 @@ commands=pytest deps= django==2.2.* pytest + pytest-cov pytest-django pytest-xdist setenv= DJANGO_SETTINGS_MODULE=tests.settings + PYTHONPATH={toxinidir} [testenv:blue] commands=blue {toxinidir}/setup.py {toxinidir}/diskcache {toxinidir}/tests @@ -81,6 +83,8 @@ addopts= --cov-report=term-missing --cov=diskcache --doctest-glob="*.rst" + --ignore docs/case-study-web-crawler.rst + --ignore docs/sf-python-2017-meetup-talk.rst --ignore tests/benchmark_core.py --ignore tests/benchmark_djangocache.py --ignore tests/benchmark_glob.py From a5d1c73e38fbc917c5503e2e8bcd9a2f834efc3f Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 13:39:33 -0800 Subject: [PATCH 130/211] Add branch coverage and decrease coverage minimum to 96 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index acf7b22..04b7382 100644 --- a/tox.ini +++ b/tox.ini @@ -79,7 +79,7 @@ line_length = 79 addopts= -n auto --cov-branch - --cov-fail-under=100 + --cov-fail-under=96 --cov-report=term-missing --cov=diskcache --doctest-glob="*.rst" From fc38bd97009bd9ce352e8bf01950a2a22b5604b5 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 13:43:31 -0800 Subject: [PATCH 131/211] Ignore more .coverage files --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index c496721..67157b8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,7 +7,7 @@ # test files/directories /.cache/ -.coverage +.coverage* .pytest_cache/ /.tox/ From 6b6944750acdaf80c6ce28eb8e7a35320ea3a3c0 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 13:57:25 -0800 Subject: [PATCH 132/211] Bump version to 5.2.0 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index c050a17..d1021cd 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -62,8 +62,8 @@ pass __title__ = 'diskcache' -__version__ = '5.1.0' -__build__ = 0x050100 +__version__ = '5.2.0' +__build__ = 0x050200 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2021 Grant Jenks' From 619eb6904810440ef952ea425421315a7353ba12 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 18:46:28 -0800 Subject: [PATCH 133/211] Install libmemcached-dev for release action --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 57c68e5..2a07787 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,11 @@ jobs: steps: - uses: actions/checkout@v2 + - name: Install libmemcached-dev + run: | + sudo apt-get update + sudo apt-get install libmemcached-dev + - name: Set up Python uses: actions/setup-python@v2 with: From e1d7c4aaa6729178ca3216f4c8a75b835f963022 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Fri, 22 Jan 2021 18:47:55 -0800 Subject: [PATCH 134/211] Bump version to 5.2.1 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index d1021cd..428eb30 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -62,8 +62,8 @@ pass __title__ = 'diskcache' -__version__ = '5.2.0' -__build__ = 0x050200 +__version__ = '5.2.1' +__build__ = 0x050201 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2021 Grant Jenks' From 12c1db268d03699de86cf114facd2a5091bfc257 Mon Sep 17 00:00:00 2001 From: Omer Katz Date: Thu, 28 Jan 2021 11:11:33 +0200 Subject: [PATCH 135/211] Add Python 3.9 support trove classifier. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 5f3cd88..7f0561c 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ def run_tests(self): 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', ), ) From 660e53d3afe5789babcf461e4cf7764e90dcf15a Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sat, 30 Jan 2021 12:20:10 -0800 Subject: [PATCH 136/211] Run integration on pull requests --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 7dfc198..0a44f89 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -1,6 +1,6 @@ name: integration -on: [push] +on: [push, pull_request] jobs: From 2ba659ece1cb7a2b99430cc805c5631f79ec9e8a Mon Sep 17 00:00:00 2001 From: Joakim Nordling Date: Sun, 28 Mar 2021 06:59:44 +0300 Subject: [PATCH 137/211] Fix typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index acf6803..c4aa8e9 100644 --- a/README.rst +++ b/README.rst @@ -185,7 +185,7 @@ other projects are shown in the tables below. access. Keys are arbitrary strings, values arbitrary pickle-able objects. * `pickleDB`_ is a lightweight and simple key-value store. It is built upon Python's simplejson module and was inspired by Redis. It is licensed with the - BSD three-caluse license. + BSD three-clause license. .. _`dbm`: https://docs.python.org/3/library/dbm.html .. _`shelve`: https://docs.python.org/3/library/shelve.html From 12400ec12eb32be37c41a4940b545797aae036a6 Mon Sep 17 00:00:00 2001 From: Abhilash Raj Date: Fri, 21 May 2021 14:44:18 -0700 Subject: [PATCH 138/211] Fix the URL to Django documentation for cache. --- docs/tutorial.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 8f8dc0f..58220b2 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -535,7 +535,7 @@ they are guaranteed to be stored in files. The full path is available on the file handle in the `name` attribute. Remember to also include the `Content-Type` header if known. -.. _`Django documentation on caching`: https://docs.djangoproject.com/en/1.9/topics/cache/#the-low-level-cache-api +.. _`Django documentation on caching`: https://docs.djangoproject.com/en/3.2/topics/cache/#the-low-level-cache-api Deque ----- From b460e7409d8d299e208500d640d69e55c1faccec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Gmach?= Date: Mon, 7 Jun 2021 15:11:34 +0200 Subject: [PATCH 139/211] remove leftovers from Travis and AppVeyor Both were removed in favor of GitHub actions. --- docs/conf.py | 1 - tests/stress_test_deque_mp.py | 6 ------ tests/stress_test_index_mp.py | 6 ------ 3 files changed, 13 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 402d3ad..d725198 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,7 +59,6 @@ 'logo': 'gj-logo.png', 'logo_name': True, 'logo_text_align': 'center', - 'travis_button': True, 'analytics_id': 'UA-19364636-2', 'show_powered_by': False, 'show_related': True, diff --git a/tests/stress_test_deque_mp.py b/tests/stress_test_deque_mp.py index 091ff0e..d6bb56a 100644 --- a/tests/stress_test_deque_mp.py +++ b/tests/stress_test_deque_mp.py @@ -107,12 +107,6 @@ def stress(seed, deque): def test(status=False): - if os.environ.get('TRAVIS') == 'true': - return - - if os.environ.get('APPVEYOR') == 'True': - return - random.seed(SEED) deque = dc.Deque(range(SIZE)) processes = [] diff --git a/tests/stress_test_index_mp.py b/tests/stress_test_index_mp.py index f8718f0..2d290ec 100644 --- a/tests/stress_test_index_mp.py +++ b/tests/stress_test_index_mp.py @@ -94,12 +94,6 @@ def stress(seed, index): def test(status=False): - if os.environ.get('TRAVIS') == 'true': - return - - if os.environ.get('APPVEYOR') == 'True': - return - random.seed(SEED) index = dc.Index(enumerate(range(KEYS))) processes = [] From d20ddb3b2273dee41e73aa72ba182f4331ccba83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=BCrgen=20Gmach?= Date: Tue, 8 Jun 2021 11:05:03 +0200 Subject: [PATCH 140/211] remove unused imports --- tests/stress_test_deque_mp.py | 1 - tests/stress_test_index_mp.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/stress_test_deque_mp.py b/tests/stress_test_deque_mp.py index d6bb56a..f3b8a48 100644 --- a/tests/stress_test_deque_mp.py +++ b/tests/stress_test_deque_mp.py @@ -2,7 +2,6 @@ import itertools as it import multiprocessing as mp -import os import random import time diff --git a/tests/stress_test_index_mp.py b/tests/stress_test_index_mp.py index 2d290ec..06ed102 100644 --- a/tests/stress_test_index_mp.py +++ b/tests/stress_test_index_mp.py @@ -2,7 +2,6 @@ import itertools as it import multiprocessing as mp -import os import random import time From d9b9d06614a41beff9ebce9b1bc8038d5540394e Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 8 Jun 2021 21:42:54 -0700 Subject: [PATCH 141/211] Ignore pylint's consider-using-with in Disk.fetch --- diskcache/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diskcache/core.py b/diskcache/core.py index da4d884..4d0ae05 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -261,7 +261,7 @@ def fetch(self, mode, filename, value, read): :return: corresponding Python value """ - # pylint: disable=no-self-use,unidiomatic-typecheck + # pylint: disable=no-self-use,unidiomatic-typecheck,consider-using-with if mode == MODE_RAW: return bytes(value) if type(value) is sqlite3.Binary else value elif mode == MODE_BINARY: From fac9ad3ffdc289336b7c280e50ca26712f65f8ea Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 30 Aug 2021 22:07:25 -0700 Subject: [PATCH 142/211] Simplify ENOENT handling around fetch() and remove() --- diskcache/core.py | 53 ++++++++++++++--------------------------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 4d0ae05..1c915b1 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -26,14 +26,6 @@ def full_name(func): return func.__module__ + '.' + func.__qualname__ -try: - WindowsError -except NameError: - - class WindowsError(Exception): - "Windows error place-holder on platforms without support." - - class Constant(tuple): "Pretty display of immutable constant." @@ -328,13 +320,10 @@ def remove(self, filename): try: os.remove(full_path) - except WindowsError: + except OSError: + # OSError may occur if two caches attempt to delete the same + # file at the same time. pass - except OSError as error: - if error.errno != errno.ENOENT: - # ENOENT may occur if two caches attempt to delete the same - # file at the same time. - raise class JSONDisk(Disk): @@ -1201,13 +1190,10 @@ def get( try: value = self._disk.fetch(mode, filename, db_value, read) except IOError as error: - if error.errno == errno.ENOENT: - # Key was deleted before we could retrieve result. - if self.statistics: - sql(cache_miss) - return default - else: - raise + # Key was deleted before we could retrieve result. + if self.statistics: + sql(cache_miss) + return default if self.statistics: sql(cache_hit) @@ -1324,11 +1310,8 @@ def pop( try: value = self._disk.fetch(mode, filename, db_value, False) except IOError as error: - if error.errno == errno.ENOENT: - # Key was deleted before we could retrieve result. - return default - else: - raise + # Key was deleted before we could retrieve result. + return default finally: if filename is not None: self._disk.remove(filename) @@ -1595,10 +1578,8 @@ def pull( try: value = self._disk.fetch(mode, name, db_value, False) except IOError as error: - if error.errno == errno.ENOENT: - # Key was deleted before we could retrieve result. - continue - raise + # Key was deleted before we could retrieve result. + continue finally: if name is not None: self._disk.remove(name) @@ -1711,10 +1692,8 @@ def peek( try: value = self._disk.fetch(mode, name, db_value, False) except IOError as error: - if error.errno == errno.ENOENT: - # Key was deleted before we could retrieve result. - continue - raise + # Key was deleted before we could retrieve result. + continue finally: if name is not None: self._disk.remove(name) @@ -1794,10 +1773,8 @@ def peekitem(self, last=True, expire_time=False, tag=False, retry=False): try: value = self._disk.fetch(mode, name, db_value, False) except IOError as error: - if error.errno == errno.ENOENT: - # Key was deleted before we could retrieve result. - continue - raise + # Key was deleted before we could retrieve result. + continue break if expire_time and tag: From ceff81cde5ecacaf7ff79b50bd90250e988a5db0 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 30 Aug 2021 22:11:33 -0700 Subject: [PATCH 143/211] Add doc about IOError --- diskcache/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/diskcache/core.py b/diskcache/core.py index 1c915b1..efa175b 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -251,6 +251,7 @@ def fetch(self, mode, filename, value, read): :param value: database value :param bool read: when True, return an open file handle :return: corresponding Python value + :raises: IOError if the value cannot be read """ # pylint: disable=no-self-use,unidiomatic-typecheck,consider-using-with From aefb2feda735b1f602eee290cac3a1baa95c8c8c Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 30 Aug 2021 22:12:36 -0700 Subject: [PATCH 144/211] Add notes about changes to store() and remove() --- diskcache/core.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/diskcache/core.py b/diskcache/core.py index efa175b..2cbcdad 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -187,6 +187,7 @@ def store(self, value, read, key=UNKNOWN): :return: (size, mode, filename, value) tuple for Cache table """ + # TODO: Retry mkdirs!!! # pylint: disable=unidiomatic-typecheck type_value = type(value) min_file_size = self.min_file_size @@ -317,6 +318,7 @@ def remove(self, filename): :param str filename: relative path to file """ + # TODO: Delete dir if empty!!! full_path = op.join(self._directory, filename) try: From 49c5979190bd33b134ab8acfe5b5c95823d567e2 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 31 Aug 2021 22:33:14 -0700 Subject: [PATCH 145/211] Update remove to cleanup parent dirs --- diskcache/core.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 2cbcdad..13a1b01 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -309,24 +309,26 @@ def filename(self, key=UNKNOWN, value=UNKNOWN): full_path = op.join(self._directory, filename) return filename, full_path - def remove(self, filename): - """Remove a file given by `filename`. + def remove(self, file_path): + """Remove a file given by `file_path`. - This method is cross-thread and cross-process safe. If an "error no - entry" occurs, it is suppressed. + This method is cross-thread and cross-process safe. If an OSError + occurs, it is suppressed. - :param str filename: relative path to file + :param str file_path: relative path to file """ - # TODO: Delete dir if empty!!! - full_path = op.join(self._directory, filename) + full_path = op.join(self._directory, file_path) + full_dir, _ = op.split(full_path) - try: + # Suppress OSError that may occur if two caches attempt to delete the + # same file or directory at the same time. + + with cl.suppress(OSError): os.remove(full_path) - except OSError: - # OSError may occur if two caches attempt to delete the same - # file at the same time. - pass + + with cl.suppress(OSError): + os.removedirs(full_dir) class JSONDisk(Disk): From 1acab3b7b0809180b7c5b0863751e18662662cc3 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 31 Aug 2021 22:33:38 -0700 Subject: [PATCH 146/211] Remove logic from filename() for creating directories --- diskcache/core.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 13a1b01..ac266e3 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -297,14 +297,6 @@ def filename(self, key=UNKNOWN, value=UNKNOWN): hex_name = codecs.encode(os.urandom(16), 'hex').decode('utf-8') sub_dir = op.join(hex_name[:2], hex_name[2:4]) name = hex_name[4:] + '.val' - directory = op.join(self._directory, sub_dir) - - try: - os.makedirs(directory) - except OSError as error: - if error.errno != errno.EEXIST: - raise - filename = op.join(sub_dir, name) full_path = op.join(self._directory, filename) return filename, full_path From b86aa9e0ba840821f2b5bcbc064a1283fd4d3e1e Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 31 Aug 2021 22:34:03 -0700 Subject: [PATCH 147/211] Modify store() to create the subdirs when writing the file (1 of 4) --- diskcache/core.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index ac266e3..9040fb4 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -206,9 +206,26 @@ def store(self, value, read, key=UNKNOWN): return 0, MODE_RAW, None, sqlite3.Binary(value) else: filename, full_path = self.filename(key, value) + full_dir, _ = op.split(full_path) - with open(full_path, 'xb') as writer: - writer.write(value) + for count in range(11): + with cl.suppress(OSError): + os.makedirs(full_dir) + + try: + # Another cache may have deleted the directory before + # the file could be opened. + writer = open(full_path, 'xb') + except OSError: + if count == 10: + # Give up after 10 tries to open the file. + raise + continue + + with writer: + writer.write(value) + + break return len(value), MODE_BINARY, filename, None elif type_value is str: From 879a65a1932377d017cc187661fd880346459b0d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 6 Sep 2021 22:02:21 -0700 Subject: [PATCH 148/211] Refactor file writing logic to retry makedirs --- diskcache/core.py | 66 +++++++++++++++++++++-------------------------- 1 file changed, 30 insertions(+), 36 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 9040fb4..332276a 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -187,7 +187,6 @@ def store(self, value, read, key=UNKNOWN): :return: (size, mode, filename, value) tuple for Cache table """ - # TODO: Retry mkdirs!!! # pylint: disable=unidiomatic-typecheck type_value = type(value) min_file_size = self.min_file_size @@ -206,46 +205,18 @@ def store(self, value, read, key=UNKNOWN): return 0, MODE_RAW, None, sqlite3.Binary(value) else: filename, full_path = self.filename(key, value) - full_dir, _ = op.split(full_path) - - for count in range(11): - with cl.suppress(OSError): - os.makedirs(full_dir) - - try: - # Another cache may have deleted the directory before - # the file could be opened. - writer = open(full_path, 'xb') - except OSError: - if count == 10: - # Give up after 10 tries to open the file. - raise - continue - - with writer: - writer.write(value) - - break - + self._write(full_path, io.BytesIO(value), 'xb') return len(value), MODE_BINARY, filename, None elif type_value is str: filename, full_path = self.filename(key, value) - - with open(full_path, 'x', encoding='UTF-8') as writer: - writer.write(value) - + self._write(full_path, io.StringIO(value), 'x', 'UTF-8') size = op.getsize(full_path) return size, MODE_TEXT, filename, None elif read: - size = 0 reader = ft.partial(value.read, 2 ** 22) filename, full_path = self.filename(key, value) - - with open(full_path, 'xb') as writer: - for chunk in iter(reader, b''): - size += len(chunk) - writer.write(chunk) - + iterator = iter(reader, b'') + size = self._write(full_path, iterator, 'xb') return size, MODE_BINARY, filename, None else: result = pickle.dumps(value, protocol=self.pickle_protocol) @@ -254,11 +225,34 @@ def store(self, value, read, key=UNKNOWN): return 0, MODE_PICKLE, None, sqlite3.Binary(result) else: filename, full_path = self.filename(key, value) + self._write(full_path, io.BytesIO(result), 'xb') + return len(result), MODE_PICKLE, filename, None + + def _write(self, full_path, iterator, mode, encoding=None): + full_dir, _ = op.split(full_path) - with open(full_path, 'xb') as writer: - writer.write(result) + for count in range(1, 11): + with cl.suppress(OSError): + os.makedirs(full_dir) - return len(result), MODE_PICKLE, filename, None + try: + # Another cache may have deleted the directory before + # the file could be opened. + writer = open(full_path, mode, encoding=encoding) + except OSError: + if count == 10: + # Give up after 10 tries to open the file. + raise + continue + + with writer: + size = 0 + for chunk in iterator: + size += len(chunk) + writer.write(chunk) + return size + + break def fetch(self, mode, filename, value, read): """Convert fields `mode`, `filename`, and `value` from Cache table to From 226a5cfd2ec68a726a1f6d535d6bc5c055cf226f Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 6 Sep 2021 22:39:29 -0700 Subject: [PATCH 149/211] Add test for Lock.locked() --- tests/test_recipes.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 8d13f2b..88612cf 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -28,6 +28,26 @@ def test_averager(cache): assert nums.pop() == 9.5 +def test_lock(cache): + state = {'num': 0} + lock = dc.Lock(cache, 'demo') + + def worker(): + state['num'] += 1 + with lock: + assert lock.locked() + state['num'] += 1 + time.sleep(0.1) + + with lock: + thread = threading.Thread(target=worker) + thread.start() + time.sleep(0.1) + assert state['num'] == 1 + thread.join() + assert state['num'] == 2 + + def test_rlock(cache): state = {'num': 0} rlock = dc.RLock(cache, 'demo') From 9d5c0b7ebf43bea02eae968f75a9104a699fba46 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 6 Sep 2021 22:39:47 -0700 Subject: [PATCH 150/211] Test re-entrancy of "rlock" --- tests/test_recipes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 88612cf..3f330a6 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -55,8 +55,9 @@ def test_rlock(cache): def worker(): state['num'] += 1 with rlock: - state['num'] += 1 - time.sleep(0.1) + with rlock: + state['num'] += 1 + time.sleep(0.1) with rlock: thread = threading.Thread(target=worker) From 14094b31006d2951b64a8b87f2a07bd57efa6525 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 6 Sep 2021 22:40:23 -0700 Subject: [PATCH 151/211] Delete EACCESS error tests --- tests/test_core.py | 121 --------------------------------------------- 1 file changed, 121 deletions(-) diff --git a/tests/test_core.py b/tests/test_core.py index bcc79e1..a113443 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -333,26 +333,6 @@ def test_get_expired_slow_path(cache): assert cache.get(0) is None -def test_get_ioerror_slow_path(cache): - cache.reset('eviction_policy', 'least-recently-used') - cache.set(0, 0) - - disk = mock.Mock() - put = mock.Mock() - fetch = mock.Mock() - - disk.put = put - put.side_effect = [(0, True)] - disk.fetch = fetch - io_error = IOError() - io_error.errno = errno.EACCES - fetch.side_effect = io_error - - with mock.patch.object(cache, '_disk', disk): - with pytest.raises(IOError): - cache.get(0) - - def test_pop(cache): assert cache.incr('alpha') == 1 assert cache.pop('alpha') == 1 @@ -396,25 +376,6 @@ def test_pop_ioerror(cache): assert cache.pop(0) is None -def test_pop_ioerror_eacces(cache): - assert cache.set(0, 0) - - disk = mock.Mock() - put = mock.Mock() - fetch = mock.Mock() - - disk.put = put - put.side_effect = [(0, True)] - disk.fetch = fetch - io_error = IOError() - io_error.errno = errno.EACCES - fetch.side_effect = io_error - - with mock.patch.object(cache, '_disk', disk): - with pytest.raises(IOError): - cache.pop(0) - - def test_delete(cache): cache[0] = 0 assert cache.delete(0) @@ -591,29 +552,6 @@ def test_least_frequently_used(cache): assert len(cache.check()) == 0 -def test_filename_error(cache): - func = mock.Mock(side_effect=OSError(errno.EACCES)) - - with mock.patch('os.makedirs', func): - with pytest.raises(OSError): - cache._disk.filename() - - -def test_remove_error(cache): - func = mock.Mock(side_effect=OSError(errno.EACCES)) - - try: - with mock.patch('os.remove', func): - cache._disk.remove('ab/cd/efg.val') - except OSError: - pass - else: - if os.name == 'nt': - pass # File delete errors ignored on Windows. - else: - raise Exception('test_remove_error failed') - - def test_check(cache): blob = b'a' * 2 ** 20 keys = (0, 1, 1234, 56.78, u'hello', b'world', None) @@ -1028,44 +966,6 @@ def test_peek_ioerror(cache): assert value == 0 -def test_pull_ioerror_eacces(cache): - assert cache.push(0) == 500000000000000 - - disk = mock.Mock() - put = mock.Mock() - fetch = mock.Mock() - - disk.put = put - put.side_effect = [(0, True)] - disk.fetch = fetch - io_error = IOError() - io_error.errno = errno.EACCES - fetch.side_effect = io_error - - with mock.patch.object(cache, '_disk', disk): - with pytest.raises(IOError): - cache.pull() - - -def test_peek_ioerror_eacces(cache): - assert cache.push(0) == 500000000000000 - - disk = mock.Mock() - put = mock.Mock() - fetch = mock.Mock() - - disk.put = put - put.side_effect = [(0, True)] - disk.fetch = fetch - io_error = IOError() - io_error.errno = errno.EACCES - fetch.side_effect = io_error - - with mock.patch.object(cache, '_disk', disk): - with pytest.raises(IOError): - cache.peek() - - def test_peekitem_extras(cache): with pytest.raises(KeyError): cache.peekitem() @@ -1117,27 +1017,6 @@ def test_peekitem_ioerror(cache): assert value == 2 -def test_peekitem_ioerror_eacces(cache): - assert cache.set('a', 0) - assert cache.set('b', 1) - assert cache.set('c', 2) - - disk = mock.Mock() - put = mock.Mock() - fetch = mock.Mock() - - disk.put = put - put.side_effect = [(0, True)] - disk.fetch = fetch - io_error = IOError() - io_error.errno = errno.EACCES - fetch.side_effect = io_error - - with mock.patch.object(cache, '_disk', disk): - with pytest.raises(IOError): - cache.peekitem() - - def test_iterkeys(cache): assert list(cache.iterkeys()) == [] From 2f18867705a536d8a6f54404856f029923204f08 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 6 Sep 2021 22:40:36 -0700 Subject: [PATCH 152/211] Test Cache.memoize() with typed kwargs --- tests/test_core.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index a113443..518f99e 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1338,3 +1338,10 @@ def fibrec(num): assert hits2 == (hits1 + count) assert misses2 == misses1 + + +def test_memoize_kwargs(cache): + @cache.memoize(typed=True) + def foo(*args, **kwargs): + return args, kwargs + assert foo(1, 2, 3, a=4, b=5) == ((1, 2, 3), {'a': 4, 'b': 5}) From 094b873e1a780004d6b07dd3ebbb216898d803e4 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 6 Sep 2021 22:40:53 -0700 Subject: [PATCH 153/211] Test JSONDisk.get by iterating cache --- tests/test_core.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index 518f99e..efe3e24 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -89,6 +89,9 @@ def test_custom_disk(): for value in values: assert cache[value] == value + for key, value in zip(cache, values): + assert key == value + shutil.rmtree(cache.directory, ignore_errors=True) From b92f2fd3abaf9c30a921ee7d287f573f2d6546a6 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 6 Sep 2021 22:41:07 -0700 Subject: [PATCH 154/211] Increase coverage to 97% --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 04b7382..926a2b3 100644 --- a/tox.ini +++ b/tox.ini @@ -79,7 +79,7 @@ line_length = 79 addopts= -n auto --cov-branch - --cov-fail-under=96 + --cov-fail-under=97 --cov-report=term-missing --cov=diskcache --doctest-glob="*.rst" From f9503321a78ecffabac670bdd147bb6026af78fa Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 6 Sep 2021 22:46:16 -0700 Subject: [PATCH 155/211] Add test for cleaning up dirs --- tests/test_core.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index efe3e24..cb44da6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1348,3 +1348,16 @@ def test_memoize_kwargs(cache): def foo(*args, **kwargs): return args, kwargs assert foo(1, 2, 3, a=4, b=5) == ((1, 2, 3), {'a': 4, 'b': 5}) + + +def test_cleanup_dirs(cache): + value = b'\0' * 2**20 + start_count = len(os.listdir(cache.directory)) + for i in range(10): + cache[i] = value + set_count = len(os.listdir(cache.directory)) + assert set_count > start_count + for i in range(10): + del cache[i] + del_count = len(os.listdir(cache.directory)) + assert start_count == del_count From 587f00d97724ef9bd2f66fcfa4935fecfc1f86a3 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 6 Sep 2021 22:47:21 -0700 Subject: [PATCH 156/211] Add TODO for testing Disk._write --- tests/test_core.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_core.py b/tests/test_core.py index cb44da6..2d09f94 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1361,3 +1361,11 @@ def test_cleanup_dirs(cache): del cache[i] del_count = len(os.listdir(cache.directory)) assert start_count == del_count + + +# TODO: Add tests for Disk._write +# diskcache/core.py +## Disk._write +# - 234->exit +# - 242-246 +# - 255 From 28aa595a662754e88a0d2665b4a9c5eaeadd93fd Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 20:25:34 -0700 Subject: [PATCH 157/211] Add tests for Disk._write --- diskcache/core.py | 2 -- tests/test_core.py | 11 +++++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 332276a..b836e84 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -252,8 +252,6 @@ def _write(self, full_path, iterator, mode, encoding=None): writer.write(chunk) return size - break - def fetch(self, mode, filename, value, read): """Convert fields `mode`, `filename`, and `value` from Cache table to value. diff --git a/tests/test_core.py b/tests/test_core.py index 2d09f94..41cfe51 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1363,9 +1363,8 @@ def test_cleanup_dirs(cache): assert start_count == del_count -# TODO: Add tests for Disk._write -# diskcache/core.py -## Disk._write -# - 234->exit -# - 242-246 -# - 255 +def test_disk_write_os_error(cache): + func = mock.Mock(side_effect=[OSError] * 10) + with mock.patch('diskcache.core.open', func): + with pytest.raises(OSError): + cache[0] = '\0' * 2**20 From a2e461ad93f7fa99a0d861db614cea2f7e521a6c Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 20:38:35 -0700 Subject: [PATCH 158/211] Add a pragma "no cover" statements and increase threshold to 98 --- diskcache/__init__.py | 2 +- diskcache/djangocache.py | 2 +- tox.ini | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 428eb30..b5a6218 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -57,7 +57,7 @@ from .djangocache import DjangoCache # noqa __all__.append('DjangoCache') -except Exception: # pylint: disable=broad-except +except Exception: # pylint: disable=broad-except # pragma: no cover # Django not installed or not setup so ignore. pass diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index 44f673d..bf9f4d4 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -6,7 +6,7 @@ try: from django.core.cache.backends.base import DEFAULT_TIMEOUT -except ImportError: +except ImportError: # pragma: no cover # For older versions of Django simply use 300 seconds. DEFAULT_TIMEOUT = 300 diff --git a/tox.ini b/tox.ini index 926a2b3..104a084 100644 --- a/tox.ini +++ b/tox.ini @@ -79,7 +79,7 @@ line_length = 79 addopts= -n auto --cov-branch - --cov-fail-under=97 + --cov-fail-under=98 --cov-report=term-missing --cov=diskcache --doctest-glob="*.rst" From ab1484daf149039a468ed087b5073198bc4f21c1 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 20:42:06 -0700 Subject: [PATCH 159/211] Blue fixes (mostly docstring triple quotes) --- diskcache/__init__.py | 1 - diskcache/cli.py | 2 +- diskcache/core.py | 29 ++++++++++++++--------------- diskcache/djangocache.py | 14 +++++++------- diskcache/fanout.py | 12 ++++++------ diskcache/persistent.py | 13 ++++++------- diskcache/recipes.py | 29 ++++++++++++++--------------- tests/benchmark_core.py | 1 - tests/benchmark_djangocache.py | 2 -- tests/benchmark_glob.py | 2 +- tests/benchmark_incr.py | 5 ++--- tests/benchmark_kv_store.py | 1 - tests/issue_109.py | 1 - tests/issue_85.py | 1 - tests/plot.py | 9 ++++----- tests/plot_early_recompute.py | 5 ++--- tests/stress_test_core.py | 10 +++++----- tests/stress_test_fanout.py | 10 +++++----- tests/test_core.py | 7 ++++--- tests/test_deque.py | 2 +- tests/test_djangocache.py | 4 ++-- tests/test_fanout.py | 2 +- tests/test_index.py | 2 +- tests/test_recipes.py | 2 +- tests/utils.py | 6 +++--- 25 files changed, 80 insertions(+), 92 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index b5a6218..934a3a7 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -3,7 +3,6 @@ ======================= The :doc:`tutorial` provides a helpful walkthrough of most methods. - """ from .core import ( diff --git a/diskcache/cli.py b/diskcache/cli.py index 44bffeb..6a39f60 100644 --- a/diskcache/cli.py +++ b/diskcache/cli.py @@ -1 +1 @@ -"Command line interface to disk cache." +"""Command line interface to disk cache.""" diff --git a/diskcache/core.py b/diskcache/core.py index b836e84..4cbf67f 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -1,5 +1,4 @@ """Core disk and file backed cache API. - """ import codecs @@ -22,12 +21,12 @@ def full_name(func): - "Return full name of `func` by adding the module and function name." + """Return full name of `func` by adding the module and function name.""" return func.__module__ + '.' + func.__qualname__ class Constant(tuple): - "Pretty display of immutable constant." + """Pretty display of immutable constant.""" def __new__(cls, name): return tuple.__new__(cls, (name,)) @@ -102,7 +101,7 @@ def __repr__(self): class Disk: - "Cache key and value serialization for SQLite database and files." + """Cache key and value serialization for SQLite database and files.""" def __init__(self, directory, min_file_size=0, pickle_protocol=0): """Initialize disk instance. @@ -333,7 +332,7 @@ def remove(self, file_path): class JSONDisk(Disk): - "Cache key and value using JSON serialization with zlib compression." + """Cache key and value using JSON serialization with zlib compression.""" def __init__(self, directory, compress_level=1, **kwargs): """Initialize JSON disk instance. @@ -374,15 +373,15 @@ def fetch(self, mode, filename, value, read): class Timeout(Exception): - "Database timeout expired." + """Database timeout expired.""" class UnknownFileWarning(UserWarning): - "Warning used by Cache.check for unknown files." + """Warning used by Cache.check for unknown files.""" class EmptyDirWarning(UserWarning): - "Warning used by Cache.check for empty directories." + """Warning used by Cache.check for empty directories.""" def args_to_key(base, args, kwargs, typed): @@ -414,7 +413,7 @@ def args_to_key(base, args, kwargs, typed): class Cache: - "Disk and file backed cache." + """Disk and file backed cache.""" def __init__(self, directory=None, timeout=60, disk=Disk, **settings): """Initialize cache instance. @@ -1859,12 +1858,12 @@ def memoize(self, name=None, typed=False, expire=None, tag=None): raise TypeError('name cannot be callable') def decorator(func): - "Decorator created by memoize() for callable `func`." + """Decorator created by memoize() for callable `func`.""" base = (full_name(func),) if name is None else (name,) @ft.wraps(func) def wrapper(*args, **kwargs): - "Wrapper for callable to cache arguments and return values." + """Wrapper for callable to cache arguments and return values.""" key = wrapper.__cache_key__(*args, **kwargs) result = self.get(key, default=ENOVAL, retry=True) @@ -1876,7 +1875,7 @@ def wrapper(*args, **kwargs): return result def __cache_key__(*args, **kwargs): - "Make key for cache given function arguments." + """Make key for cache given function arguments.""" return args_to_key(base, args, kwargs, typed) wrapper.__cache_key__ = __cache_key__ @@ -2291,13 +2290,13 @@ def _iter(self, ascending=True): yield _disk_get(key, raw) def __iter__(self): - "Iterate keys in cache including expired items." + """Iterate keys in cache including expired items.""" iterator = self._iter() next(iterator) return iterator def __reversed__(self): - "Reverse iterate keys in cache including expired items." + """Reverse iterate keys in cache including expired items.""" iterator = self._iter(ascending=False) next(iterator) return iterator @@ -2355,7 +2354,7 @@ def __exit__(self, *exception): self.close() def __len__(self): - "Count of items in cache including expired items." + """Count of items in cache including expired items.""" return self.reset('count') def __getstate__(self): diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index bf9f4d4..449f3a0 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -1,4 +1,4 @@ -"Django-compatible disk and file backed cache." +"""Django-compatible disk and file backed cache.""" from functools import wraps @@ -15,7 +15,7 @@ class DjangoCache(BaseCache): - "Django-compatible disk and file backed cache." + """Django-compatible disk and file backed cache.""" def __init__(self, directory, params): """Initialize DjangoCache instance. @@ -344,11 +344,11 @@ def cull(self): return self._cache.cull() def clear(self): - "Remove *all* values from the cache at once." + """Remove *all* values from the cache at once.""" return self._cache.clear() def close(self, **kwargs): - "Close the cache connection." + """Close the cache connection.""" # pylint: disable=unused-argument self._cache.close() @@ -415,12 +415,12 @@ def memoize( raise TypeError('name cannot be callable') def decorator(func): - "Decorator created by memoize() for callable `func`." + """Decorator created by memoize() for callable `func`.""" base = (full_name(func),) if name is None else (name,) @wraps(func) def wrapper(*args, **kwargs): - "Wrapper for callable to cache arguments and return values." + """Wrapper for callable to cache arguments and return values.""" key = wrapper.__cache_key__(*args, **kwargs) result = self.get(key, ENOVAL, version, retry=True) @@ -444,7 +444,7 @@ def wrapper(*args, **kwargs): return result def __cache_key__(*args, **kwargs): - "Make key for cache given function arguments." + """Make key for cache given function arguments.""" return args_to_key(base, args, kwargs, typed) wrapper.__cache_key__ = __cache_key__ diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 99384a0..dc5240c 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -1,4 +1,4 @@ -"Fanout cache automatically shards keys and values." +"""Fanout cache automatically shards keys and values.""" import contextlib as cl import functools @@ -14,7 +14,7 @@ class FanoutCache: - "Cache that shards keys and values." + """Cache that shards keys and values.""" def __init__( self, directory=None, shards=8, timeout=0.010, disk=Disk, **settings @@ -512,7 +512,7 @@ def volume(self): return sum(shard.volume() for shard in self._shards) def close(self): - "Close database connection." + """Close database connection.""" for shard in self._shards: shard.close() self._caches.clear() @@ -532,17 +532,17 @@ def __setstate__(self, state): self.__init__(*state) def __iter__(self): - "Iterate keys in cache including expired items." + """Iterate keys in cache including expired items.""" iterators = (iter(shard) for shard in self._shards) return it.chain.from_iterable(iterators) def __reversed__(self): - "Reverse iterate keys in cache including expired items." + """Reverse iterate keys in cache including expired items.""" iterators = (reversed(shard) for shard in reversed(self._shards)) return it.chain.from_iterable(iterators) def __len__(self): - "Count of items in cache including expired items." + """Count of items in cache including expired items.""" return sum(len(shard) for shard in self._shards) def reset(self, key, value=ENOVAL): diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 44f9cc7..89c3899 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -1,5 +1,4 @@ """Persistent Data Types - """ import operator as op @@ -18,10 +17,10 @@ def _make_compare(seq_op, doc): - "Make compare method with Sequence semantics." + """Make compare method with Sequence semantics.""" def compare(self, that): - "Compare method for deque and sequence." + """Compare method for deque and sequence.""" if not isinstance(that, Sequence): return NotImplemented @@ -117,12 +116,12 @@ def fromcache(cls, cache, iterable=()): @property def cache(self): - "Cache used by deque." + """Cache used by deque.""" return self._cache @property def directory(self): - "Directory path where deque is stored." + """Directory path where deque is stored.""" return self._cache.directory def _index(self, index, func): @@ -699,12 +698,12 @@ def fromcache(cls, cache, *args, **kwargs): @property def cache(self): - "Cache used by index." + """Cache used by index.""" return self._cache @property def directory(self): - "Directory path where items are stored." + """Directory path where items are stored.""" return self._cache.directory def __getitem__(self, key): diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 8f7bd32..0c02dd7 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -1,5 +1,4 @@ """Disk Cache Recipes - """ import functools @@ -40,7 +39,7 @@ def __init__(self, cache, key, expire=None, tag=None): self._tag = tag def add(self, value): - "Add `value` to average." + """Add `value` to average.""" with self._cache.transact(retry=True): total, count = self._cache.get(self._key, default=(0.0, 0)) total += value @@ -53,12 +52,12 @@ def add(self, value): ) def get(self): - "Get current average or return `None` if count equals zero." + """Get current average or return `None` if count equals zero.""" total, count = self._cache.get(self._key, default=(0.0, 0), retry=True) return None if count == 0 else total / count def pop(self): - "Return current average and delete key." + """Return current average and delete key.""" total, count = self._cache.pop(self._key, default=(0.0, 0), retry=True) return None if count == 0 else total / count @@ -83,7 +82,7 @@ def __init__(self, cache, key, expire=None, tag=None): self._tag = tag def acquire(self): - "Acquire lock using spin-lock algorithm." + """Acquire lock using spin-lock algorithm.""" while True: added = self._cache.add( self._key, @@ -97,11 +96,11 @@ def acquire(self): time.sleep(0.001) def release(self): - "Release lock by deleting key." + """Release lock by deleting key.""" self._cache.delete(self._key, retry=True) def locked(self): - "Return true if the lock is acquired." + """Return true if the lock is acquired.""" return self._key in self._cache def __enter__(self): @@ -137,7 +136,7 @@ def __init__(self, cache, key, expire=None, tag=None): self._tag = tag def acquire(self): - "Acquire lock by incrementing count using spin-lock algorithm." + """Acquire lock by incrementing count using spin-lock algorithm.""" pid = os.getpid() tid = threading.get_ident() pid_tid = '{}-{}'.format(pid, tid) @@ -156,7 +155,7 @@ def acquire(self): time.sleep(0.001) def release(self): - "Release lock by decrementing count." + """Release lock by decrementing count.""" pid = os.getpid() tid = threading.get_ident() pid_tid = '{}-{}'.format(pid, tid) @@ -206,7 +205,7 @@ def __init__(self, cache, key, value=1, expire=None, tag=None): self._tag = tag def acquire(self): - "Acquire semaphore by decrementing value using spin-lock algorithm." + """Acquire semaphore by decrementing value using spin-lock algorithm.""" while True: with self._cache.transact(retry=True): value = self._cache.get(self._key, default=self._value) @@ -221,7 +220,7 @@ def acquire(self): time.sleep(0.001) def release(self): - "Release semaphore by incrementing value." + """Release semaphore by incrementing value.""" with self._cache.transact(retry=True): value = self._cache.get(self._key, default=self._value) assert self._value > value, 'cannot release un-acquired semaphore' @@ -396,11 +395,11 @@ def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1): """ # Caution: Nearly identical code exists in Cache.memoize def decorator(func): - "Decorator created by memoize call for callable." + """Decorator created by memoize call for callable.""" base = (full_name(func),) if name is None else (name,) def timer(*args, **kwargs): - "Time execution of `func` and return result and time delta." + """Time execution of `func` and return result and time delta.""" start = time.time() result = func(*args, **kwargs) delta = time.time() - start @@ -408,7 +407,7 @@ def timer(*args, **kwargs): @functools.wraps(func) def wrapper(*args, **kwargs): - "Wrapper for callable to cache arguments and return values." + """Wrapper for callable to cache arguments and return values.""" key = wrapper.__cache_key__(*args, **kwargs) pair, expire_time = cache.get( key, @@ -459,7 +458,7 @@ def recompute(): return pair[0] def __cache_key__(*args, **kwargs): - "Make key for cache given function arguments." + """Make key for cache given function arguments.""" return args_to_key(base, args, kwargs, typed) wrapper.__cache_key__ = __cache_key__ diff --git a/tests/benchmark_core.py b/tests/benchmark_core.py index 282ce2a..7d64595 100644 --- a/tests/benchmark_core.py +++ b/tests/benchmark_core.py @@ -3,7 +3,6 @@ $ export PYTHONPATH=/Users/grantj/repos/python-diskcache $ python tests/benchmark_core.py -p 1 > tests/timings_core_p1.txt $ python tests/benchmark_core.py -p 8 > tests/timings_core_p8.txt - """ import collections as co diff --git a/tests/benchmark_djangocache.py b/tests/benchmark_djangocache.py index 9dbbcd7..61a80bf 100644 --- a/tests/benchmark_djangocache.py +++ b/tests/benchmark_djangocache.py @@ -2,8 +2,6 @@ $ export PYTHONPATH=/Users/grantj/repos/python-diskcache $ python tests/benchmark_djangocache.py > tests/timings_djangocache.txt - - """ import collections as co diff --git a/tests/benchmark_glob.py b/tests/benchmark_glob.py index 9c23104..7f0bf7c 100644 --- a/tests/benchmark_glob.py +++ b/tests/benchmark_glob.py @@ -1,4 +1,4 @@ -"Benchmark glob.glob1 as used by django.core.cache.backends.filebased." +"""Benchmark glob.glob1 as used by django.core.cache.backends.filebased.""" import os import os.path as op diff --git a/tests/benchmark_incr.py b/tests/benchmark_incr.py index 9c8e2fa..4f758aa 100644 --- a/tests/benchmark_incr.py +++ b/tests/benchmark_incr.py @@ -1,5 +1,4 @@ """Benchmark cache.incr method. - """ import json @@ -16,7 +15,7 @@ def worker(num): - "Rapidly increment key and time operation." + """Rapidly increment key and time operation.""" time.sleep(0.1) # Let other workers start. cache = dc.Cache('tmp') @@ -33,7 +32,7 @@ def worker(num): def main(): - "Run workers and print percentile results." + """Run workers and print percentile results.""" shutil.rmtree('tmp', ignore_errors=True) processes = [ diff --git a/tests/benchmark_kv_store.py b/tests/benchmark_kv_store.py index e141a36..7015470 100644 --- a/tests/benchmark_kv_store.py +++ b/tests/benchmark_kv_store.py @@ -1,7 +1,6 @@ """Benchmarking Key-Value Stores $ python -m IPython tests/benchmark_kv_store.py - """ from IPython import get_ipython diff --git a/tests/issue_109.py b/tests/issue_109.py index c10a81f..a649c58 100644 --- a/tests/issue_109.py +++ b/tests/issue_109.py @@ -1,5 +1,4 @@ """Benchmark for Issue #109 - """ import time diff --git a/tests/issue_85.py b/tests/issue_85.py index 723406b..cb8789b 100644 --- a/tests/issue_85.py +++ b/tests/issue_85.py @@ -2,7 +2,6 @@ $ export PYTHONPATH=`pwd` $ python tests/issue_85.py - """ import collections diff --git a/tests/plot.py b/tests/plot.py index 2138659..fcac0bc 100644 --- a/tests/plot.py +++ b/tests/plot.py @@ -2,7 +2,6 @@ $ export PYTHONPATH=/Users/grantj/repos/python-diskcache $ python tests/plot.py --show tests/timings_core_p1.txt - """ import argparse @@ -14,7 +13,7 @@ def parse_timing(timing, limit): - "Parse timing." + """Parse timing.""" if timing.endswith('ms'): value = float(timing[:-2]) * 1e-3 elif timing.endswith('us'): @@ -26,12 +25,12 @@ def parse_timing(timing, limit): def parse_row(row, line): - "Parse row." + """Parse row.""" return [val.strip() for val in row.match(line).groups()] def parse_data(infile): - "Parse data from `infile`." + """Parse data from `infile`.""" blocks = re.compile(' '.join(['=' * 9] * 8)) dashes = re.compile('^-{79}$') title = re.compile('^Timings for (.*)$') @@ -83,7 +82,7 @@ def parse_data(infile): def make_plot(data, action, save=False, show=False, limit=0.005): - "Make plot." + """Make plot.""" fig, ax = plt.subplots(figsize=(8, 10)) colors = ['#ff7f00', '#377eb8', '#4daf4a', '#984ea3', '#e41a1c'] width = 0.15 diff --git a/tests/plot_early_recompute.py b/tests/plot_early_recompute.py index e58f580..1508c45 100644 --- a/tests/plot_early_recompute.py +++ b/tests/plot_early_recompute.py @@ -1,5 +1,4 @@ """Early Recomputation Measurements - """ import functools as ft @@ -61,14 +60,14 @@ def repeat(num): def frange(start, stop, step=1e-3): - "Generator for floating point values from `start` to `stop` by `step`." + """Generator for floating point values from `start` to `stop` by `step`.""" while start < stop: yield start start += step def plot(option, filename, cache_times, worker_times): - "Plot concurrent workers and latency." + """Plot concurrent workers and latency.""" import matplotlib.pyplot as plt fig, (workers, latency) = plt.subplots(2, sharex=True) diff --git a/tests/stress_test_core.py b/tests/stress_test_core.py index 6fd0991..c30fa3f 100644 --- a/tests/stress_test_core.py +++ b/tests/stress_test_core.py @@ -1,4 +1,4 @@ -"Stress test diskcache.core.Cache." +"""Stress test diskcache.core.Cache.""" import collections as co import multiprocessing as mp @@ -292,22 +292,22 @@ def stress_test( def stress_test_lru(): - "Stress test least-recently-used eviction policy." + """Stress test least-recently-used eviction policy.""" stress_test(eviction_policy=u'least-recently-used') def stress_test_lfu(): - "Stress test least-frequently-used eviction policy." + """Stress test least-frequently-used eviction policy.""" stress_test(eviction_policy=u'least-frequently-used') def stress_test_none(): - "Stress test 'none' eviction policy." + """Stress test 'none' eviction policy.""" stress_test(eviction_policy=u'none') def stress_test_mp(): - "Stress test multiple threads and processes." + """Stress test multiple threads and processes.""" stress_test(processes=4, threads=4) diff --git a/tests/stress_test_fanout.py b/tests/stress_test_fanout.py index 58708c9..d3b67e3 100644 --- a/tests/stress_test_fanout.py +++ b/tests/stress_test_fanout.py @@ -1,4 +1,4 @@ -"Stress test diskcache.core.Cache." +"""Stress test diskcache.core.Cache.""" import multiprocessing as mp import os @@ -283,22 +283,22 @@ def stress_test( def stress_test_lru(): - "Stress test least-recently-used eviction policy." + """Stress test least-recently-used eviction policy.""" stress_test(eviction_policy=u'least-recently-used') def stress_test_lfu(): - "Stress test least-frequently-used eviction policy." + """Stress test least-frequently-used eviction policy.""" stress_test(eviction_policy=u'least-frequently-used') def stress_test_none(): - "Stress test 'none' eviction policy." + """Stress test 'none' eviction policy.""" stress_test(eviction_policy=u'none') def stress_test_mp(): - "Stress test multiple threads and processes." + """Stress test multiple threads and processes.""" stress_test(processes=4, threads=4) diff --git a/tests/test_core.py b/tests/test_core.py index 41cfe51..c1e7a4a 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1,4 +1,4 @@ -"Test diskcache.core.Cache." +"""Test diskcache.core.Cache.""" import errno import hashlib @@ -1347,11 +1347,12 @@ def test_memoize_kwargs(cache): @cache.memoize(typed=True) def foo(*args, **kwargs): return args, kwargs + assert foo(1, 2, 3, a=4, b=5) == ((1, 2, 3), {'a': 4, 'b': 5}) def test_cleanup_dirs(cache): - value = b'\0' * 2**20 + value = b'\0' * 2 ** 20 start_count = len(os.listdir(cache.directory)) for i in range(10): cache[i] = value @@ -1367,4 +1368,4 @@ def test_disk_write_os_error(cache): func = mock.Mock(side_effect=[OSError] * 10) with mock.patch('diskcache.core.open', func): with pytest.raises(OSError): - cache[0] = '\0' * 2**20 + cache[0] = '\0' * 2 ** 20 diff --git a/tests/test_deque.py b/tests/test_deque.py index 8113dfe..add7714 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -1,4 +1,4 @@ -"Test diskcache.persistent.Deque." +"""Test diskcache.persistent.Deque.""" import pickle import shutil diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index 2c216fe..cdaf101 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -100,7 +100,7 @@ class UnpicklableType(object): def custom_key_func(key, key_prefix, version): - "A customized cache key function" + """A customized cache key function""" return 'CUSTOM-' + '-'.join([key_prefix, str(version), key]) @@ -921,7 +921,7 @@ def __getstate__(self): ) ) class DiskCacheTests(BaseCacheTests, TestCase): - "Specific test cases for diskcache.DjangoCache." + """Specific test cases for diskcache.DjangoCache.""" def setUp(self): super().setUp() diff --git a/tests/test_fanout.py b/tests/test_fanout.py index 8918af3..f212fac 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -1,4 +1,4 @@ -"Test diskcache.fanout.FanoutCache." +"""Test diskcache.fanout.FanoutCache.""" import collections as co import hashlib diff --git a/tests/test_index.py b/tests/test_index.py index 27639f7..742daf3 100644 --- a/tests/test_index.py +++ b/tests/test_index.py @@ -1,4 +1,4 @@ -"Test diskcache.persistent.Index." +"""Test diskcache.persistent.Index.""" import pickle import shutil diff --git a/tests/test_recipes.py b/tests/test_recipes.py index 3f330a6..ae74459 100644 --- a/tests/test_recipes.py +++ b/tests/test_recipes.py @@ -1,4 +1,4 @@ -"Test diskcache.recipes." +"""Test diskcache.recipes.""" import shutil import threading diff --git a/tests/utils.py b/tests/utils.py index 5b41ce9..38e5d33 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -32,7 +32,7 @@ def secs(value): def run(*args): - "Run command, print output, and return output." + """Run command, print output, and return output.""" print('utils$', *args) result = sp.check_output(args) print(result) @@ -40,7 +40,7 @@ def run(*args): def mount_ramdisk(size, path): - "Mount RAM disk at `path` with `size` in bytes." + """Mount RAM disk at `path` with `size` in bytes.""" sectors = size / 512 os.makedirs(path) @@ -53,7 +53,7 @@ def mount_ramdisk(size, path): def unmount_ramdisk(dev_path, path): - "Unmount RAM disk with `dev_path` and `path`." + """Unmount RAM disk with `dev_path` and `path`.""" run('umount', path) run('diskutil', 'eject', dev_path) run('rm', '-r', path) From 72bbd73d29184e095df248efd48c31eeeb8e992c Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 20:58:43 -0700 Subject: [PATCH 160/211] Pylint fixes --- diskcache/core.py | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 4cbf67f..2e946ad 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -46,25 +46,25 @@ def __repr__(self): MODE_PICKLE = 4 DEFAULT_SETTINGS = { - u'statistics': 0, # False - u'tag_index': 0, # False - u'eviction_policy': u'least-recently-stored', - u'size_limit': 2 ** 30, # 1gb - u'cull_limit': 10, - u'sqlite_auto_vacuum': 1, # FULL - u'sqlite_cache_size': 2 ** 13, # 8,192 pages - u'sqlite_journal_mode': u'wal', - u'sqlite_mmap_size': 2 ** 26, # 64mb - u'sqlite_synchronous': 1, # NORMAL - u'disk_min_file_size': 2 ** 15, # 32kb - u'disk_pickle_protocol': pickle.HIGHEST_PROTOCOL, + 'statistics': 0, # False + 'tag_index': 0, # False + 'eviction_policy': 'least-recently-stored', + 'size_limit': 2 ** 30, # 1gb + 'cull_limit': 10, + 'sqlite_auto_vacuum': 1, # FULL + 'sqlite_cache_size': 2 ** 13, # 8,192 pages + 'sqlite_journal_mode': 'wal', + 'sqlite_mmap_size': 2 ** 26, # 64mb + 'sqlite_synchronous': 1, # NORMAL + 'disk_min_file_size': 2 ** 15, # 32kb + 'disk_pickle_protocol': pickle.HIGHEST_PROTOCOL, } METADATA = { - u'count': 0, - u'size': 0, - u'hits': 0, - u'misses': 0, + 'count': 0, + 'size': 0, + 'hits': 0, + 'misses': 0, } EVICTION_POLICY = { @@ -1194,7 +1194,7 @@ def get( try: value = self._disk.fetch(mode, filename, db_value, read) - except IOError as error: + except IOError: # Key was deleted before we could retrieve result. if self.statistics: sql(cache_miss) @@ -1314,7 +1314,7 @@ def pop( try: value = self._disk.fetch(mode, filename, db_value, False) - except IOError as error: + except IOError: # Key was deleted before we could retrieve result. return default finally: @@ -1582,7 +1582,7 @@ def pull( try: value = self._disk.fetch(mode, name, db_value, False) - except IOError as error: + except IOError: # Key was deleted before we could retrieve result. continue finally: @@ -1696,7 +1696,7 @@ def peek( try: value = self._disk.fetch(mode, name, db_value, False) - except IOError as error: + except IOError: # Key was deleted before we could retrieve result. continue finally: @@ -1777,7 +1777,7 @@ def peekitem(self, last=True, expire_time=False, tag=False, retry=False): try: value = self._disk.fetch(mode, name, db_value, False) - except IOError as error: + except IOError: # Key was deleted before we could retrieve result. continue break @@ -1911,7 +1911,7 @@ def check(self, fix=False, retry=False): rows = sql('PRAGMA integrity_check').fetchall() - if len(rows) != 1 or rows[0][0] != u'ok': + if len(rows) != 1 or rows[0][0] != 'ok': for (message,) in rows: warnings.warn(message) From 3e87128d4154acf90bd64c6630b1200108dc1ed2 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 21:04:34 -0700 Subject: [PATCH 161/211] Disable no-self-use in Disk._write --- diskcache/core.py | 1 + 1 file changed, 1 insertion(+) diff --git a/diskcache/core.py b/diskcache/core.py index 2e946ad..251c5a2 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -228,6 +228,7 @@ def store(self, value, read, key=UNKNOWN): return len(result), MODE_PICKLE, filename, None def _write(self, full_path, iterator, mode, encoding=None): + # pylint: disable=no-self-use full_dir, _ = op.split(full_path) for count in range(1, 11): From fbc537a7138330652d0d42aa09caa5531746b302 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 21:51:30 -0700 Subject: [PATCH 162/211] Add `ignore` to memoize() --- diskcache/core.py | 10 +++++++--- diskcache/djangocache.py | 4 +++- diskcache/persistent.py | 5 +++-- diskcache/recipes.py | 5 +++-- tests/test_core.py | 13 +++++++++++++ 5 files changed, 29 insertions(+), 8 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 251c5a2..4035293 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -385,19 +385,22 @@ class EmptyDirWarning(UserWarning): """Warning used by Cache.check for empty directories.""" -def args_to_key(base, args, kwargs, typed): +def args_to_key(base, args, kwargs, typed, ignore): """Create cache key out of function arguments. :param tuple base: base of key :param tuple args: function arguments :param dict kwargs: function keyword arguments :param bool typed: include types in cache key + :param set ignore: positional or keyword args to ignore :return: cache key tuple """ + args = tuple(arg for index, arg in enumerate(args) if index not in ignore) key = base + args if kwargs: + kwargs = {key: val for key, val in kwargs.items() if key not in ignore} key += (ENOVAL,) sorted_items = sorted(kwargs.items()) @@ -1792,7 +1795,7 @@ def peekitem(self, last=True, expire_time=False, tag=False, retry=False): else: return key, value - def memoize(self, name=None, typed=False, expire=None, tag=None): + def memoize(self, name=None, typed=False, expire=None, tag=None, ignore=()): """Memoizing cache decorator. Decorator to wrap callable with memoizing function using cache. @@ -1851,6 +1854,7 @@ def memoize(self, name=None, typed=False, expire=None, tag=None): :param float expire: seconds until arguments expire (default None, no expiry) :param str tag: text to associate with arguments (default None) + :param set ignore: positional or keyword args to ignore (default ()) :return: callable decorator """ @@ -1877,7 +1881,7 @@ def wrapper(*args, **kwargs): def __cache_key__(*args, **kwargs): """Make key for cache given function arguments.""" - return args_to_key(base, args, kwargs, typed) + return args_to_key(base, args, kwargs, typed, ignore) wrapper.__cache_key__ = __cache_key__ return wrapper diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index 449f3a0..347a613 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -373,6 +373,7 @@ def memoize( version=None, typed=False, tag=None, + ignore=(), ): """Memoizing cache decorator. @@ -407,6 +408,7 @@ def memoize( :param int version: key version number (default None, cache parameter) :param bool typed: cache different types separately (default False) :param str tag: text to associate with arguments (default None) + :param set ignore: positional or keyword args to ignore (default ()) :return: callable decorator """ @@ -445,7 +447,7 @@ def wrapper(*args, **kwargs): def __cache_key__(*args, **kwargs): """Make key for cache given function arguments.""" - return args_to_key(base, args, kwargs, typed) + return args_to_key(base, args, kwargs, typed, ignore) wrapper.__cache_key__ = __cache_key__ return wrapper diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 89c3899..9b5939b 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -1096,7 +1096,7 @@ def __ne__(self, other): """ return not self == other - def memoize(self, name=None, typed=False): + def memoize(self, name=None, typed=False, ignore=()): """Memoizing cache decorator. Decorator to wrap callable with memoizing function using cache. @@ -1147,10 +1147,11 @@ def memoize(self, name=None, typed=False): :param str name: name given for callable (default None, automatic) :param bool typed: cache different types separately (default False) + :param set ignore: positional or keyword args to ignore (default ()) :return: callable decorator """ - return self._cache.memoize(name, typed) + return self._cache.memoize(name, typed, ignore) @contextmanager def transact(self): diff --git a/diskcache/recipes.py b/diskcache/recipes.py index 0c02dd7..b345560 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -337,7 +337,7 @@ def wrapper(*args, **kwargs): return decorator -def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1): +def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1, ignore=()): """Memoizing cache decorator with cache stampede protection. Cache stampedes are a type of system overload that can occur when parallel @@ -390,6 +390,7 @@ def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1): :param str name: name given for callable (default None, automatic) :param bool typed: cache different types separately (default False) :param str tag: text to associate with arguments (default None) + :param set ignore: positional or keyword args to ignore (default ()) :return: callable decorator """ @@ -459,7 +460,7 @@ def recompute(): def __cache_key__(*args, **kwargs): """Make key for cache given function arguments.""" - return args_to_key(base, args, kwargs, typed) + return args_to_key(base, args, kwargs, typed, ignore) wrapper.__cache_key__ = __cache_key__ return wrapper diff --git a/tests/test_core.py b/tests/test_core.py index c1e7a4a..0a1fca6 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1369,3 +1369,16 @@ def test_disk_write_os_error(cache): with mock.patch('diskcache.core.open', func): with pytest.raises(OSError): cache[0] = '\0' * 2 ** 20 + + +def test_memoize_ignore(cache): + + @cache.memoize(ignore={1, 'arg1'}) + def test(*args, **kwargs): + return args, kwargs + + cache.stats(enable=True) + assert test('a', 'b', 'c', arg0='d', arg1='e', arg2='f') + assert test('a', 'w', 'c', arg0='d', arg1='x', arg2='f') + assert test('a', 'y', 'c', arg0='d', arg1='z', arg2='f') + assert cache.stats() == (2, 1) From bd800aa069ad6bc15aa3f6fec84ae92d2f644de5 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 21:52:05 -0700 Subject: [PATCH 163/211] Fixes for blue --- diskcache/core.py | 4 +++- diskcache/recipes.py | 4 +++- tests/test_core.py | 1 - 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index 4035293..d7f707b 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -1795,7 +1795,9 @@ def peekitem(self, last=True, expire_time=False, tag=False, retry=False): else: return key, value - def memoize(self, name=None, typed=False, expire=None, tag=None, ignore=()): + def memoize( + self, name=None, typed=False, expire=None, tag=None, ignore=() + ): """Memoizing cache decorator. Decorator to wrap callable with memoizing function using cache. diff --git a/diskcache/recipes.py b/diskcache/recipes.py index b345560..b5af6dd 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -337,7 +337,9 @@ def wrapper(*args, **kwargs): return decorator -def memoize_stampede(cache, expire, name=None, typed=False, tag=None, beta=1, ignore=()): +def memoize_stampede( + cache, expire, name=None, typed=False, tag=None, beta=1, ignore=() +): """Memoizing cache decorator with cache stampede protection. Cache stampedes are a type of system overload that can occur when parallel diff --git a/tests/test_core.py b/tests/test_core.py index 0a1fca6..b3d3d66 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -1372,7 +1372,6 @@ def test_disk_write_os_error(cache): def test_memoize_ignore(cache): - @cache.memoize(ignore={1, 'arg1'}) def test(*args, **kwargs): return args, kwargs From 606d8f2b12e8022e6d5b509d7c41439c4242043d Mon Sep 17 00:00:00 2001 From: Abhinav Omprakash <55880260+AbhinavOmprakash@users.noreply.github.com> Date: Fri, 11 Jun 2021 09:54:54 +0530 Subject: [PATCH 164/211] Fixes #201 added github repo to project_urls --- setup.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 7f0561c..b29a463 100644 --- a/setup.py +++ b/setup.py @@ -29,7 +29,12 @@ def run_tests(self): long_description=readme, author='Grant Jenks', author_email='contact@grantjenks.com', - url='http://www.grantjenks.com/docs/diskcache/', + url='http://www.grantjenks.com/docs/diskcache/', + project_urls = { + 'Documentation':'http://www.grantjenks.com/docs/diskcache/', + 'Source':'https://github.com/grantjenks/python-diskcache', + 'Tracker':'https://github.com/grantjenks/python-diskcache/issues', + 'Funding':'https://gumroad.com/l/diskcache',} license='Apache 2.0', packages=['diskcache'], tests_require=['tox'], From c22d3ee59ee28bd58aec59182d375b1eccd191f2 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 22:03:57 -0700 Subject: [PATCH 165/211] Fixup formatting for project urls --- setup.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index b29a463..b49d9c8 100644 --- a/setup.py +++ b/setup.py @@ -29,12 +29,13 @@ def run_tests(self): long_description=readme, author='Grant Jenks', author_email='contact@grantjenks.com', - url='http://www.grantjenks.com/docs/diskcache/', - project_urls = { - 'Documentation':'http://www.grantjenks.com/docs/diskcache/', - 'Source':'https://github.com/grantjenks/python-diskcache', - 'Tracker':'https://github.com/grantjenks/python-diskcache/issues', - 'Funding':'https://gumroad.com/l/diskcache',} + url='http://www.grantjenks.com/docs/diskcache/', + project_urls={ + 'Documentation': 'http://www.grantjenks.com/docs/diskcache/', + 'Funding': 'https://gum.co/diskcache', + 'Source': 'https://github.com/grantjenks/python-diskcache', + 'Tracker': 'https://github.com/grantjenks/python-diskcache/issues', + }, license='Apache 2.0', packages=['diskcache'], tests_require=['tox'], From d55a50ee083784afa9c85e14e41c4a2d132f3111 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 22:15:50 -0700 Subject: [PATCH 166/211] Stop using ENOVAL in args_to_key() --- diskcache/core.py | 3 +-- tests/test_core.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index d7f707b..fb343be 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -397,11 +397,10 @@ def args_to_key(base, args, kwargs, typed, ignore): """ args = tuple(arg for index, arg in enumerate(args) if index not in ignore) - key = base + args + key = base + args + (None,) if kwargs: kwargs = {key: val for key, val in kwargs.items() if key not in ignore} - key += (ENOVAL,) sorted_items = sorted(kwargs.items()) for item in sorted_items: diff --git a/tests/test_core.py b/tests/test_core.py index b3d3d66..55ca962 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -92,6 +92,8 @@ def test_custom_disk(): for key, value in zip(cache, values): assert key == value + test_memoize_iter(cache) + shutil.rmtree(cache.directory, ignore_errors=True) @@ -1381,3 +1383,17 @@ def test(*args, **kwargs): assert test('a', 'w', 'c', arg0='d', arg1='x', arg2='f') assert test('a', 'y', 'c', arg0='d', arg1='z', arg2='f') assert cache.stats() == (2, 1) + + +def test_memoize_iter(cache): + @cache.memoize() + def test(*args, **kwargs): + return sum(args) + sum(kwargs.values()) + + cache.clear() + assert test(1, 2, 3) + assert test(a=1, b=2, c=3) + assert test(-1, 0, 1, a=1, b=2, c=3) + assert len(cache) == 3 + for key in cache: + assert cache[key] == 6 From b10b3866f4d64ea197e3fb40c0ebe25aa32900d6 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 22:29:21 -0700 Subject: [PATCH 167/211] Add caveat about inconsistent pickles --- docs/tutorial.rst | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 58220b2..3191af6 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -863,8 +863,17 @@ protocol`_ is not used. Neither the `__hash__` nor `__eq__` methods are used for lookups. Instead lookups depend on the serialization method defined by :class:`Disk ` objects. For strings, bytes, integers, and floats, equality matches Python's definition. But large integers and all other -types will be converted to bytes using pickling and the bytes representation -will define equality. +types will be converted to bytes and the bytes representation will define +equality. + +The default :class:`diskcache.Disk` serialization uses pickling for both keys +and values. Unfortunately, pickling produces inconsistencies sometimes when +applied to container data types like tuples. Two equal tuples may serialize to +different bytes objects using pickle. The likelihood of differences is reduced +by using `pickletools.optimize` but still inconsistencies occur (`#54`_). The +inconsistent serialized pickle values is particularly problematic when applied +to the key in the cache. Consider using an alternative Disk type, like +:class:`JSONDisk `, for consistent serialization of keys. SQLite is used to synchronize database access between threads and processes and as such inherits all SQLite caveats. Most notably SQLite is `not recommended`_ @@ -898,6 +907,7 @@ does not account the size of directories themselves or other filesystem metadata. If directory count or size is a concern then consider implementing an alternative :class:`Disk `. +.. _`#54`: https://github.com/grantjenks/python-diskcache/issues/54 .. _`hash protocol`: https://docs.python.org/library/functions.html#hash .. _`not recommended`: https://www.sqlite.org/faq.html#q5 .. _`performs poorly`: https://www.pythonanywhere.com/forums/topic/1847/ From 3ad6c0e4365ef93e60a75aee75740e6551e279f9 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 13 Sep 2021 22:40:26 -0700 Subject: [PATCH 168/211] Bug Fix: Use "ignore" keyword argument with Index.memoize() --- diskcache/persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 9b5939b..c3d570b 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -1151,7 +1151,7 @@ def memoize(self, name=None, typed=False, ignore=()): :return: callable decorator """ - return self._cache.memoize(name, typed, ignore) + return self._cache.memoize(name, typed, ignore=ignore) @contextmanager def transact(self): From 4de3c0ed99dd2bcebffad8e6e0cb7a2885210a97 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Tue, 14 Sep 2021 14:07:00 -0700 Subject: [PATCH 169/211] Drop old Ubuntu from integration testing --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 0a44f89..b1143a4 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -31,7 +31,7 @@ jobs: strategy: max-parallel: 8 matrix: - os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-16.04] + os: [ubuntu-latest, macos-latest, windows-latest] python-version: [3.6, 3.7, 3.8, 3.9] steps: From 6ed012a655673c54468beb7ff00038443ed2595e Mon Sep 17 00:00:00 2001 From: artiom Date: Wed, 29 Sep 2021 10:57:36 +0100 Subject: [PATCH 170/211] docs: fix typo --- docs/tutorial.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 3191af6..1963635 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -570,7 +570,7 @@ access and editing at both front and back sides. :class:`Deque cross-thread and cross-process communication. :class:`Deque ` objects are also useful in scenarios where contents should remain persistent or limitations prohibit holding all items in memory at the same time. The deque -uses a fixed amout of memory regardless of the size or number of items stored +uses a fixed amount of memory regardless of the size or number of items stored inside it. Index @@ -603,7 +603,7 @@ interface. :class:`Index ` objects inherit all the benefits of cross-thread and cross-process communication. :class:`Index ` objects are also useful in scenarios where contents should remain persistent or limitations prohibit holding all items in memory at the same time. The index -uses a fixed amout of memory regardless of the size or number of items stored +uses a fixed amount of memory regardless of the size or number of items stored inside it. .. _tutorial-transactions: From 20f1d93a6e3852bac4289cb94e27585d9c23330c Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 29 Sep 2021 08:11:04 -0700 Subject: [PATCH 171/211] Disable consider-using-f-string --- .pylintrc | 1 + 1 file changed, 1 insertion(+) diff --git a/.pylintrc b/.pylintrc index 158fe34..6baa978 100644 --- a/.pylintrc +++ b/.pylintrc @@ -143,6 +143,7 @@ disable=print-statement, no-else-return, duplicate-code, inconsistent-return-statements, + consider-using-f-string, # Enable the message, report, category or checker with the given id(s). You can # either give multiple identifier separated by comma (,) or put this option From 7bfbce63ba127d601ddfbf2838539e34cad87616 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 29 Nov 2021 22:58:48 -0800 Subject: [PATCH 172/211] Support for Python 3.10 in testing (#238) * Add support for Python 3.10 * Update copyright to 2022 * Bump version to 5.3.0 * Add Python 3.10 to the README --- .github/workflows/integration.yml | 4 ++-- .github/workflows/release.yml | 2 +- LICENSE | 2 +- README.rst | 6 +++--- diskcache/__init__.py | 6 +++--- docs/conf.py | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index b1143a4..07a5650 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: '3.10' - name: Install dependencies run: | pip install --upgrade pip @@ -32,7 +32,7 @@ jobs: max-parallel: 8 matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9, '3.10'] steps: - name: Set up Python ${{ matrix.python-version }} x64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2a07787..1f89c14 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: '3.10' - name: Install dependencies run: | diff --git a/LICENSE b/LICENSE index ca80a22..bb4cfb7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright 2016-2021 Grant Jenks +Copyright 2016-2022 Grant Jenks Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the diff --git a/README.rst b/README.rst index c4aa8e9..eb06a6a 100644 --- a/README.rst +++ b/README.rst @@ -77,8 +77,8 @@ Features - Thread-safe and process-safe - Supports multiple eviction policies (LRU and LFU included) - Keys support "tag" metadata and eviction -- Developed on Python 3.9 -- Tested on CPython 3.6, 3.7, 3.8, 3.9 +- Developed on Python 3.10 +- Tested on CPython 3.6, 3.7, 3.8, 3.9, 3.10 - Tested on Linux, Mac OS X, and Windows - Tested using GitHub Actions @@ -387,7 +387,7 @@ Reference License ------- -Copyright 2016-2021 Grant Jenks +Copyright 2016-2022 Grant Jenks Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 934a3a7..c361ca9 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -61,8 +61,8 @@ pass __title__ = 'diskcache' -__version__ = '5.2.1' -__build__ = 0x050201 +__version__ = '5.3.0' +__build__ = 0x050300 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' -__copyright__ = 'Copyright 2016-2021 Grant Jenks' +__copyright__ = 'Copyright 2016-2022 Grant Jenks' diff --git a/docs/conf.py b/docs/conf.py index d725198..92ce1b9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,7 @@ # -- Project information ----------------------------------------------------- project = 'DiskCache' -copyright = '2021, Grant Jenks' +copyright = '2022, Grant Jenks' author = 'Grant Jenks' # The full version, including alpha/beta/rc tags From 78a5cc690e0aa53ad3537a72b2f52f3c161da1ae Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 30 Dec 2021 17:35:44 -0800 Subject: [PATCH 173/211] Update tests for Django 3.2 --- tests/test_djangocache.py | 266 +++++++++++++++++++++++++------------- 1 file changed, 176 insertions(+), 90 deletions(-) diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index cdaf101..36e1b01 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -1,5 +1,5 @@ # Most of this file was copied from: -# https://raw.githubusercontent.com/django/django/stable/2.2.x/tests/cache/tests.py +# https://raw.githubusercontent.com/django/django/stable/3.2.x/tests/cache/tests.py # Unit tests for cache framework # Uses whatever cache backend is set in the test settings file. @@ -9,12 +9,14 @@ import pickle import re import shutil +import sys import tempfile import threading import time import unittest import warnings -from unittest import mock +from pathlib import Path +from unittest import mock, skipIf from django.conf import settings from django.core import management, signals @@ -88,6 +90,10 @@ def __getstate__(self): raise pickle.PickleError() +def empty_response(request): + return HttpResponse() + + KEY_ERRORS_WITH_MEMCACHED_MSG = ( 'Cache key contains characters that will cause errors if used with ' 'memcached: %r' @@ -138,6 +144,14 @@ class BaseCacheTests: # A common set of tests to apply to all cache backends factory = RequestFactory() + # RemovedInDjango41Warning: python-memcached doesn't support .get() with + # default. + supports_get_with_default = True + + # Some clients raise custom exceptions when .incr() or .decr() are called + # with a non-integer value. + incr_decr_type_error = TypeError + def tearDown(self): cache.clear() @@ -146,11 +160,15 @@ def test_simple(self): cache.set('key', 'value') self.assertEqual(cache.get('key'), 'value') + def test_default_used_when_none_is_set(self): + """If None is cached, get() returns it instead of the default.""" + cache.set('key_default_none', None) + self.assertIsNone(cache.get('key_default_none', default='default')) + def test_add(self): # A key can be added to a cache - cache.add('addkey1', 'value') - result = cache.add('addkey1', 'newvalue') - self.assertFalse(result) + self.assertIs(cache.add('addkey1', 'value'), True) + self.assertIs(cache.add('addkey1', 'newvalue'), False) self.assertEqual(cache.get('addkey1'), 'value') def test_prefix(self): @@ -158,7 +176,7 @@ def test_prefix(self): cache.set('somekey', 'value') # should not be set in the prefixed cache - self.assertFalse(caches['prefix'].has_key('somekey')) + self.assertIs(caches['prefix'].has_key('somekey'), False) caches['prefix'].set('somekey', 'value2') @@ -180,28 +198,43 @@ def test_get_many(self): self.assertEqual( cache.get_many(iter(['a', 'b', 'e'])), {'a': 'a', 'b': 'b'} ) + cache.set_many({'x': None, 'y': 1}) + self.assertEqual(cache.get_many(['x', 'y']), {'x': None, 'y': 1}) def test_delete(self): # Cache keys can be deleted cache.set_many({'key1': 'spam', 'key2': 'eggs'}) self.assertEqual(cache.get('key1'), 'spam') - cache.delete('key1') + self.assertIs(cache.delete('key1'), True) self.assertIsNone(cache.get('key1')) self.assertEqual(cache.get('key2'), 'eggs') + def test_delete_nonexistent(self): + self.assertIs(cache.delete('nonexistent_key'), False) + def test_has_key(self): # The cache can be inspected for cache keys cache.set('hello1', 'goodbye1') - self.assertTrue(cache.has_key('hello1')) - self.assertFalse(cache.has_key('goodbye1')) + self.assertIs(cache.has_key('hello1'), True) + self.assertIs(cache.has_key('goodbye1'), False) cache.set('no_expiry', 'here', None) - self.assertTrue(cache.has_key('no_expiry')) + self.assertIs(cache.has_key('no_expiry'), True) + cache.set('null', None) + self.assertIs( + cache.has_key('null'), + True if self.supports_get_with_default else False, + ) def test_in(self): # The in operator can be used to inspect cache contents cache.set('hello2', 'goodbye2') self.assertIn('hello2', cache) self.assertNotIn('goodbye2', cache) + cache.set('null', None) + if self.supports_get_with_default: + self.assertIn('null', cache) + else: + self.assertNotIn('null', cache) def test_incr(self): # Cache values can be incremented @@ -213,6 +246,9 @@ def test_incr(self): self.assertEqual(cache.incr('answer', -10), 42) with self.assertRaises(ValueError): cache.incr('does_not_exist') + cache.set('null', None) + with self.assertRaises(self.incr_decr_type_error): + cache.incr('null') def test_decr(self): # Cache values can be decremented @@ -224,6 +260,9 @@ def test_decr(self): self.assertEqual(cache.decr('answer', -10), 42) with self.assertRaises(ValueError): cache.decr('does_not_exist') + cache.set('null', None) + with self.assertRaises(self.incr_decr_type_error): + cache.decr('null') def test_close(self): self.assertTrue(hasattr(cache, 'close')) @@ -295,24 +334,23 @@ def test_expiration(self): time.sleep(2) self.assertIsNone(cache.get('expire1')) - cache.add('expire2', 'newvalue') + self.assertIs(cache.add('expire2', 'newvalue'), True) self.assertEqual(cache.get('expire2'), 'newvalue') - self.assertFalse(cache.has_key('expire3')) + self.assertIs(cache.has_key('expire3'), False) def test_touch(self): # cache.touch() updates the timeout. cache.set('expire1', 'very quickly', timeout=1) self.assertIs(cache.touch('expire1', timeout=4), True) time.sleep(2) - self.assertTrue(cache.has_key('expire1')) + self.assertIs(cache.has_key('expire1'), True) time.sleep(3) - self.assertFalse(cache.has_key('expire1')) - + self.assertIs(cache.has_key('expire1'), False) # cache.touch() works without the timeout argument. cache.set('expire1', 'very quickly', timeout=1) self.assertIs(cache.touch('expire1'), True) time.sleep(2) - self.assertTrue(cache.has_key('expire1')) + self.assertIs(cache.has_key('expire1'), True) self.assertIs(cache.touch('nonexistent'), False) @@ -333,13 +371,13 @@ def test_unicode(self): # Test `add` for (key, value) in stuff.items(): with self.subTest(key=key): - cache.delete(key) - cache.add(key, value) + self.assertIs(cache.delete(key), True) + self.assertIs(cache.add(key, value), True) self.assertEqual(cache.get(key), value) # Test `set_many` for (key, value) in stuff.items(): - cache.delete(key) + self.assertIs(cache.delete(key), True) cache.set_many(stuff) for (key, value) in stuff.items(): with self.subTest(key=key): @@ -359,7 +397,7 @@ def test_binary_string(self): self.assertEqual(value, decompress(compressed_result).decode()) # Test add - cache.add('binary1-add', compressed_value) + self.assertIs(cache.add('binary1-add', compressed_value), True) compressed_result = cache.get('binary1-add') self.assertEqual(compressed_value, compressed_result) self.assertEqual(value, decompress(compressed_result).decode()) @@ -405,14 +443,14 @@ def test_clear(self): def test_long_timeout(self): """ - Followe memcached's convention where a timeout greater than 30 days is + Follow memcached's convention where a timeout greater than 30 days is treated as an absolute expiration timestamp instead of a relative offset (#12399). """ cache.set('key1', 'eggs', 60 * 60 * 24 * 30 + 1) # 30 days + 1 second self.assertEqual(cache.get('key1'), 'eggs') - cache.add('key2', 'ham', 60 * 60 * 24 * 30 + 1) + self.assertIs(cache.add('key2', 'ham', 60 * 60 * 24 * 30 + 1), True) self.assertEqual(cache.get('key2'), 'ham') cache.set_many( @@ -429,10 +467,9 @@ def test_forever_timeout(self): cache.set('key1', 'eggs', None) self.assertEqual(cache.get('key1'), 'eggs') - cache.add('key2', 'ham', None) + self.assertIs(cache.add('key2', 'ham', None), True) self.assertEqual(cache.get('key2'), 'ham') - added = cache.add('key1', 'new eggs', None) - self.assertIs(added, False) + self.assertIs(cache.add('key1', 'new eggs', None), False) self.assertEqual(cache.get('key1'), 'eggs') cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, None) @@ -440,7 +477,7 @@ def test_forever_timeout(self): self.assertEqual(cache.get('key4'), 'lobster bisque') cache.set('key5', 'belgian fries', timeout=1) - cache.touch('key5', timeout=None) + self.assertIs(cache.touch('key5', timeout=None), True) time.sleep(2) self.assertEqual(cache.get('key5'), 'belgian fries') @@ -451,7 +488,7 @@ def test_zero_timeout(self): cache.set('key1', 'eggs', 0) self.assertIsNone(cache.get('key1')) - cache.add('key2', 'ham', 0) + self.assertIs(cache.add('key2', 'ham', 0), True) self.assertIsNone(cache.get('key2')) cache.set_many({'key3': 'sausage', 'key4': 'lobster bisque'}, 0) @@ -459,7 +496,7 @@ def test_zero_timeout(self): self.assertIsNone(cache.get('key4')) cache.set('key5', 'belgian fries', timeout=5) - cache.touch('key5', timeout=0) + self.assertIs(cache.touch('key5', timeout=0), True) self.assertIsNone(cache.get('key5')) def test_float_timeout(self): @@ -467,7 +504,12 @@ def test_float_timeout(self): cache.set('key1', 'spam', 100.2) self.assertEqual(cache.get('key1'), 'spam') - def _perform_cull_test(self, cull_cache, initial_count, final_count): + def _perform_cull_test(self, cull_cache_name, initial_count, final_count): + try: + cull_cache = caches[cull_cache_name] + except InvalidCacheBackendError: + self.skipTest("Culling isn't implemented.") + # Create initial cache key entries. This will overflow the cache, # causing a cull. for i in range(1, initial_count): @@ -480,10 +522,24 @@ def _perform_cull_test(self, cull_cache, initial_count, final_count): self.assertEqual(count, final_count) def test_cull(self): - self._perform_cull_test(caches['cull'], 50, 29) + self._perform_cull_test('cull', 50, 29) def test_zero_cull(self): - self._perform_cull_test(caches['zero_cull'], 50, 19) + self._perform_cull_test('zero_cull', 50, 19) + + def test_cull_delete_when_store_empty(self): + try: + cull_cache = caches['cull'] + except InvalidCacheBackendError: + self.skipTest("Culling isn't implemented.") + old_max_entries = cull_cache._max_entries + # Force _cull to delete on first cached record. + cull_cache._max_entries = -1 + try: + cull_cache.set('force_cull_delete', 'value', 1000) + self.assertIs(cull_cache.has_key('force_cull_delete'), True) + finally: + cull_cache._max_entries = old_max_entries def _perform_invalid_key_test(self, key, expected_warning): """ @@ -500,10 +556,24 @@ def func(key, *args): old_func = cache.key_func cache.key_func = func + tests = [ + ('add', [key, 1]), + ('get', [key]), + ('set', [key, 1]), + ('incr', [key]), + ('decr', [key]), + ('touch', [key]), + ('delete', [key]), + ('get_many', [[key, 'b']]), + ('set_many', [{key: 1, 'b': 2}]), + ('delete_many', [{key: 1, 'b': 2}]), + ] try: - with self.assertWarns(CacheKeyWarning) as cm: - cache.set(key, 'value') - self.assertEqual(str(cm.warning), expected_warning) + for operation, args in tests: + with self.subTest(operation=operation): + with self.assertWarns(CacheKeyWarning) as cm: + getattr(cache, operation)(*args) + self.assertEqual(str(cm.warning), expected_warning) finally: cache.key_func = old_func @@ -567,41 +637,41 @@ def test_cache_versioning_get_set(self): def test_cache_versioning_add(self): # add, default version = 1, but manually override version = 2 - cache.add('answer1', 42, version=2) + self.assertIs(cache.add('answer1', 42, version=2), True) self.assertIsNone(cache.get('answer1', version=1)) self.assertEqual(cache.get('answer1', version=2), 42) - cache.add('answer1', 37, version=2) + self.assertIs(cache.add('answer1', 37, version=2), False) self.assertIsNone(cache.get('answer1', version=1)) self.assertEqual(cache.get('answer1', version=2), 42) - cache.add('answer1', 37, version=1) + self.assertIs(cache.add('answer1', 37, version=1), True) self.assertEqual(cache.get('answer1', version=1), 37) self.assertEqual(cache.get('answer1', version=2), 42) # v2 add, using default version = 2 - caches['v2'].add('answer2', 42) + self.assertIs(caches['v2'].add('answer2', 42), True) self.assertIsNone(cache.get('answer2', version=1)) self.assertEqual(cache.get('answer2', version=2), 42) - caches['v2'].add('answer2', 37) + self.assertIs(caches['v2'].add('answer2', 37), False) self.assertIsNone(cache.get('answer2', version=1)) self.assertEqual(cache.get('answer2', version=2), 42) - caches['v2'].add('answer2', 37, version=1) + self.assertIs(caches['v2'].add('answer2', 37, version=1), True) self.assertEqual(cache.get('answer2', version=1), 37) self.assertEqual(cache.get('answer2', version=2), 42) # v2 add, default version = 2, but manually override version = 1 - caches['v2'].add('answer3', 42, version=1) + self.assertIs(caches['v2'].add('answer3', 42, version=1), True) self.assertEqual(cache.get('answer3', version=1), 42) self.assertIsNone(cache.get('answer3', version=2)) - caches['v2'].add('answer3', 37, version=1) + self.assertIs(caches['v2'].add('answer3', 37, version=1), False) self.assertEqual(cache.get('answer3', version=1), 42) self.assertIsNone(cache.get('answer3', version=2)) - caches['v2'].add('answer3', 37) + self.assertIs(caches['v2'].add('answer3', 37), True) self.assertEqual(cache.get('answer3', version=1), 42) self.assertEqual(cache.get('answer3', version=2), 37) @@ -609,73 +679,73 @@ def test_cache_versioning_has_key(self): cache.set('answer1', 42) # has_key - self.assertTrue(cache.has_key('answer1')) - self.assertTrue(cache.has_key('answer1', version=1)) - self.assertFalse(cache.has_key('answer1', version=2)) + self.assertIs(cache.has_key('answer1'), True) + self.assertIs(cache.has_key('answer1', version=1), True) + self.assertIs(cache.has_key('answer1', version=2), False) - self.assertFalse(caches['v2'].has_key('answer1')) - self.assertTrue(caches['v2'].has_key('answer1', version=1)) - self.assertFalse(caches['v2'].has_key('answer1', version=2)) + self.assertIs(caches['v2'].has_key('answer1'), False) + self.assertIs(caches['v2'].has_key('answer1', version=1), True) + self.assertIs(caches['v2'].has_key('answer1', version=2), False) def test_cache_versioning_delete(self): cache.set('answer1', 37, version=1) cache.set('answer1', 42, version=2) - cache.delete('answer1') + self.assertIs(cache.delete('answer1'), True) self.assertIsNone(cache.get('answer1', version=1)) self.assertEqual(cache.get('answer1', version=2), 42) cache.set('answer2', 37, version=1) cache.set('answer2', 42, version=2) - cache.delete('answer2', version=2) + self.assertIs(cache.delete('answer2', version=2), True) self.assertEqual(cache.get('answer2', version=1), 37) self.assertIsNone(cache.get('answer2', version=2)) cache.set('answer3', 37, version=1) cache.set('answer3', 42, version=2) - caches['v2'].delete('answer3') + self.assertIs(caches['v2'].delete('answer3'), True) self.assertEqual(cache.get('answer3', version=1), 37) self.assertIsNone(cache.get('answer3', version=2)) cache.set('answer4', 37, version=1) cache.set('answer4', 42, version=2) - caches['v2'].delete('answer4', version=1) + self.assertIs(caches['v2'].delete('answer4', version=1), True) self.assertIsNone(cache.get('answer4', version=1)) self.assertEqual(cache.get('answer4', version=2), 42) def test_cache_versioning_incr_decr(self): cache.set('answer1', 37, version=1) cache.set('answer1', 42, version=2) - cache.incr('answer1') + self.assertEqual(cache.incr('answer1'), 38) self.assertEqual(cache.get('answer1', version=1), 38) self.assertEqual(cache.get('answer1', version=2), 42) - cache.decr('answer1') + self.assertEqual(cache.decr('answer1'), 37) self.assertEqual(cache.get('answer1', version=1), 37) self.assertEqual(cache.get('answer1', version=2), 42) cache.set('answer2', 37, version=1) cache.set('answer2', 42, version=2) - cache.incr('answer2', version=2) + self.assertEqual(cache.incr('answer2', version=2), 43) self.assertEqual(cache.get('answer2', version=1), 37) self.assertEqual(cache.get('answer2', version=2), 43) - cache.decr('answer2', version=2) + self.assertEqual(cache.decr('answer2', version=2), 42) self.assertEqual(cache.get('answer2', version=1), 37) self.assertEqual(cache.get('answer2', version=2), 42) cache.set('answer3', 37, version=1) cache.set('answer3', 42, version=2) - caches['v2'].incr('answer3') + self.assertEqual(caches['v2'].incr('answer3'), 43) self.assertEqual(cache.get('answer3', version=1), 37) self.assertEqual(cache.get('answer3', version=2), 43) - caches['v2'].decr('answer3') + self.assertEqual(caches['v2'].decr('answer3'), 42) self.assertEqual(cache.get('answer3', version=1), 37) self.assertEqual(cache.get('answer3', version=2), 42) cache.set('answer4', 37, version=1) cache.set('answer4', 42, version=2) - caches['v2'].incr('answer4', version=1) + self.assertEqual(caches['v2'].incr('answer4', version=1), 38) self.assertEqual(cache.get('answer4', version=1), 38) self.assertEqual(cache.get('answer4', version=2), 42) - caches['v2'].decr('answer4', version=1) + self.assertEqual(caches['v2'].decr('answer4', version=1), 37) self.assertEqual(cache.get('answer4', version=1), 37) self.assertEqual(cache.get('answer4', version=2), 42) @@ -790,6 +860,13 @@ def test_incr_version(self): with self.assertRaises(ValueError): cache.incr_version('does_not_exist') + cache.set('null', None) + if self.supports_get_with_default: + self.assertEqual(cache.incr_version('null'), 2) + else: + with self.assertRaises(self.incr_decr_type_error): + cache.incr_version('null') + def test_decr_version(self): cache.set('answer', 42, version=2) self.assertIsNone(cache.get('answer')) @@ -814,6 +891,13 @@ def test_decr_version(self): with self.assertRaises(ValueError): cache.decr_version('does_not_exist', version=2) + cache.set('null', None, version=2) + if self.supports_get_with_default: + self.assertEqual(cache.decr_version('null', version=2), 1) + else: + with self.assertRaises(self.incr_decr_type_error): + cache.decr_version('null', version=2) + def test_custom_key_func(self): # Two caches with different key functions aren't visible to each other cache.set('answer1', 42) @@ -827,30 +911,33 @@ def test_custom_key_func(self): self.assertEqual(caches['custom_key2'].get('answer2'), 42) def test_cache_write_unpicklable_object(self): - update_middleware = UpdateCacheMiddleware() - update_middleware.cache = cache - - fetch_middleware = FetchFromCacheMiddleware() + fetch_middleware = FetchFromCacheMiddleware(empty_response) fetch_middleware.cache = cache request = self.factory.get('/cache/test') request._cache_update_cache = True - get_cache_data = FetchFromCacheMiddleware().process_request(request) + get_cache_data = FetchFromCacheMiddleware( + empty_response + ).process_request(request) self.assertIsNone(get_cache_data) - response = HttpResponse() content = 'Testing cookie serialization.' - response.content = content - response.set_cookie('foo', 'bar') - update_middleware.process_response(request, response) + def get_response(req): + response = HttpResponse(content) + response.set_cookie('foo', 'bar') + return response + + update_middleware = UpdateCacheMiddleware(get_response) + update_middleware.cache = cache + response = update_middleware(request) get_cache_data = fetch_middleware.process_request(request) self.assertIsNotNone(get_cache_data) self.assertEqual(get_cache_data.content, content.encode()) self.assertEqual(get_cache_data.cookies, response.cookies) - update_middleware.process_response(request, get_cache_data) + UpdateCacheMiddleware(lambda req: get_cache_data)(request) get_cache_data = fetch_middleware.process_request(request) self.assertIsNotNone(get_cache_data) self.assertEqual(get_cache_data.content, content.encode()) @@ -869,7 +956,12 @@ def test_get_or_set(self): self.assertIsNone(cache.get('projector')) self.assertEqual(cache.get_or_set('projector', 42), 42) self.assertEqual(cache.get('projector'), 42) - self.assertEqual(cache.get_or_set('null', None), None) + self.assertIsNone(cache.get_or_set('null', None)) + if self.supports_get_with_default: + # Previous get_or_set() stores None in the cache. + self.assertIsNone(cache.get('null', 'default')) + else: + self.assertEqual(cache.get('null', 'default'), 'default') def test_get_or_set_callable(self): def my_callable(): @@ -878,14 +970,16 @@ def my_callable(): self.assertEqual(cache.get_or_set('mykey', my_callable), 'value') self.assertEqual(cache.get_or_set('mykey', my_callable()), 'value') - def test_get_or_set_callable_returning_none(self): - self.assertIsNone(cache.get_or_set('mykey', lambda: None)) - # Previous get_or_set() doesn't store None in the cache. - self.assertEqual(cache.get('mykey', 'default'), 'default') + self.assertIsNone(cache.get_or_set('null', lambda: None)) + if self.supports_get_with_default: + # Previous get_or_set() stores None in the cache. + self.assertIsNone(cache.get('null', 'default')) + else: + self.assertEqual(cache.get('null', 'default'), 'default') def test_get_or_set_version(self): msg = "get_or_set() missing 1 required positional argument: 'default'" - cache.get_or_set('brian', 1979, version=2) + self.assertEqual(cache.get_or_set('brian', 1979, version=2), 1979) with self.assertRaisesMessage(TypeError, msg): cache.get_or_set('brian') with self.assertRaisesMessage(TypeError, msg): @@ -949,6 +1043,11 @@ def test_ignores_non_cache_files(self): ) os.remove(fname) + def test_creates_cache_dir_if_nonexistent(self): + os.rmdir(self.dirname) + cache.set('foo', 'bar') + self.assertTrue(os.path.exists(self.dirname)) + def test_clear_does_not_remove_cache_dir(self): cache.clear() self.assertTrue( @@ -1026,19 +1125,6 @@ def test_pop(self): ) self.assertEqual(cache.pop(4, retry=False), 4) - def test_pickle(self): - letters = 'abcde' - cache.clear() - - for num, val in enumerate(letters): - cache.set(val, num) - - data = pickle.dumps(cache) - other = pickle.loads(data) - - for key in letters: - self.assertEqual(other.get(key), cache.get(key)) - def test_cache(self): subcache = cache.cache('test') directory = os.path.join(cache.directory, 'cache', 'test') From f3836f9f1938ede1eaf32d474be0311d0c7a3183 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 30 Dec 2021 17:35:55 -0800 Subject: [PATCH 174/211] Fix DjangoCache.delete to return True/False --- diskcache/djangocache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index 347a613..8bf85ce 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -220,7 +220,7 @@ def delete(self, key, version=None, retry=True): """ # pylint: disable=arguments-differ key = self.make_key(key, version=version) - self._cache.delete(key, retry) + return self._cache.delete(key, retry) def incr(self, key, delta=1, version=None, default=None, retry=True): """Increment value by delta for item with key. From 8da6634877178ff3704d8468aa5a95b9baf4f9ac Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 30 Dec 2021 17:37:38 -0800 Subject: [PATCH 175/211] Bump Django testing to 3.2 --- requirements.txt | 2 +- tox.ini | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 2ab91c7..efb2160 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ -e . blue coverage -django==2.2.* +django==3.2.* django_redis doc8 flake8 diff --git a/tox.ini b/tox.ini index 104a084..650c8f0 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ skip_missing_interpreters=True [testenv] commands=pytest deps= - django==2.2.* + django==3.2.* pytest pytest-cov pytest-django @@ -31,7 +31,7 @@ allowlist_externals=make changedir=docs commands=make html deps= - django==2.2.* + django==3.2.* sphinx [testenv:flake8] @@ -53,7 +53,7 @@ deps=mypy [testenv:pylint] commands=pylint {toxinidir}/diskcache deps= - django==2.2.* + django==3.2.* pylint [testenv:rstcheck] From 221f3d38cea69e33ba9a83cde0e2e202f3d3d70e Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 30 Dec 2021 17:45:26 -0800 Subject: [PATCH 176/211] Remove unused imports --- tests/test_djangocache.py | 38 ++------------------------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index 36e1b01..6fae5be 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -3,64 +3,30 @@ # Unit tests for cache framework # Uses whatever cache backend is set in the test settings file. -import copy -import io import os import pickle -import re import shutil -import sys import tempfile -import threading import time -import unittest -import warnings -from pathlib import Path -from unittest import mock, skipIf +from unittest import mock from django.conf import settings -from django.core import management, signals from django.core.cache import ( - DEFAULT_CACHE_ALIAS, CacheKeyWarning, - InvalidCacheKey, cache, caches, ) -from django.core.cache.utils import make_template_fragment_key -from django.db import close_old_connections, connection, connections -from django.http import ( - HttpRequest, - HttpResponse, - HttpResponseNotModified, - StreamingHttpResponse, -) +from django.http import HttpResponse from django.middleware.cache import ( - CacheMiddleware, FetchFromCacheMiddleware, UpdateCacheMiddleware, ) -from django.middleware.csrf import CsrfViewMiddleware -from django.template import engines -from django.template.context_processors import csrf -from django.template.response import TemplateResponse from django.test import ( RequestFactory, - SimpleTestCase, TestCase, - TransactionTestCase, override_settings, ) from django.test.signals import setting_changed -from django.utils import timezone, translation -from django.utils.cache import ( - get_cache_key, - learn_cache_key, - patch_cache_control, - patch_vary_headers, -) -from django.utils.encoding import force_text -from django.views.decorators.cache import cache_page ################################################################################ # Setup Django for models import. From 5ac77969c88df42eece83be2e4ce85d2e277109d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 30 Dec 2021 17:48:08 -0800 Subject: [PATCH 177/211] Run isort --- tests/test_djangocache.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index 6fae5be..5f83b81 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -11,21 +11,13 @@ from unittest import mock from django.conf import settings -from django.core.cache import ( - CacheKeyWarning, - cache, - caches, -) +from django.core.cache import CacheKeyWarning, cache, caches from django.http import HttpResponse from django.middleware.cache import ( FetchFromCacheMiddleware, UpdateCacheMiddleware, ) -from django.test import ( - RequestFactory, - TestCase, - override_settings, -) +from django.test import RequestFactory, TestCase, override_settings from django.test.signals import setting_changed ################################################################################ From 1cb1425b1ba24f26fb1e37349c4c2658c2a46d8f Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Thu, 30 Dec 2021 17:58:24 -0800 Subject: [PATCH 178/211] Bump version to 5.4.0 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index c361ca9..2355128 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -61,8 +61,8 @@ pass __title__ = 'diskcache' -__version__ = '5.3.0' -__build__ = 0x050300 +__version__ = '5.4.0' +__build__ = 0x050400 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2022 Grant Jenks' From c9844bba9de039ab64858073d8864f7e40172c9e Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 20 Feb 2022 12:47:41 -0800 Subject: [PATCH 179/211] Put commands above deps for doc8 testenv --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 650c8f0..65da17c 100644 --- a/tox.ini +++ b/tox.ini @@ -23,8 +23,8 @@ commands=blue --check {toxinidir}/setup.py {toxinidir}/diskcache {toxinidir}/tes deps=blue [testenv:doc8] -deps=doc8 commands=doc8 docs --ignore-path docs/_build +deps=doc8 [testenv:docs] allowlist_externals=make From 2d2cc8b39db3e0c0785dd304bde7902219d85ffe Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 20 Feb 2022 13:42:14 -0800 Subject: [PATCH 180/211] Update rsync command for uploading docs --- tox.ini | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tox.ini b/tox.ini index 65da17c..36ed2b8 100644 --- a/tox.ini +++ b/tox.ini @@ -64,8 +64,9 @@ deps=rstcheck allowlist_externals=rsync changedir=docs commands= - rsync -azP --stats --delete _build/html/ \ - grantjenks.com:/srv/www/www.grantjenks.com/public/docs/diskcache/ + rsync --rsync-path 'sudo -u herokuish rsync' -azP --stats --delete \ + _build/html/ \ + grantjenks:/srv/www/grantjenks.com/public/docs/diskcache/ [isort] multi_line_output = 3 From c1774469b8d4c4906fe24f7b5afd637795af48d9 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 22:12:28 -0700 Subject: [PATCH 181/211] Remove unused import --- setup.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/setup.py b/setup.py index b49d9c8..841dfb9 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,3 @@ -from io import open - from setuptools import setup from setuptools.command.test import test as TestCommand From f3fcdffee88af7923740b9864d533524a79d1222 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 22:27:48 -0700 Subject: [PATCH 182/211] Update Cache(...) params when allocating --- diskcache/fanout.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index dc5240c..8fe51d9 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -573,9 +573,11 @@ def reset(self, key, value=ENOVAL): break return result - def cache(self, name): + def cache(self, name, timeout=60, disk=None, **settings): """Return Cache with given `name` in subdirectory. + If disk is none (default), uses the fanout cache disk. + >>> fanout_cache = FanoutCache() >>> cache = fanout_cache.cache('test') >>> cache.set('abc', 123) @@ -588,6 +590,9 @@ def cache(self, name): True :param str name: subdirectory name for Cache + :param float timeout: SQLite connection timeout + :param disk: Disk type or subclass for serialization + :param settings: any of DEFAULT_SETTINGS :return: Cache with given name """ @@ -598,7 +603,12 @@ def cache(self, name): except KeyError: parts = name.split('/') directory = op.join(self._directory, 'cache', *parts) - temp = Cache(directory=directory, disk=self._disk) + temp = Cache( + directory=directory, + timeout=timeout, + disk=self._disk if disk is None else Disk, + **settings, + ) _caches[name] = temp return temp @@ -626,7 +636,11 @@ def deque(self, name): except KeyError: parts = name.split('/') directory = op.join(self._directory, 'deque', *parts) - cache = Cache(directory=directory, disk=self._disk) + cache = Cache( + directory=directory, + disk=self._disk, + eviction_policy='none', + ) deque = Deque.fromcache(cache) _deques[name] = deque return deque @@ -658,7 +672,11 @@ def index(self, name): except KeyError: parts = name.split('/') directory = op.join(self._directory, 'index', *parts) - cache = Cache(directory=directory, disk=self._disk) + cache = Cache( + directory=directory, + disk=self._disk, + eviction_policy='none', + ) index = Index.fromcache(cache) _indexes[name] = index return index From ee7a248e5c09e6fb9145b2e4a1777a345114b71d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 22:37:39 -0700 Subject: [PATCH 183/211] Add docs about the eviction policy to recipes --- diskcache/recipes.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/diskcache/recipes.py b/diskcache/recipes.py index b5af6dd..babb68f 100644 --- a/diskcache/recipes.py +++ b/diskcache/recipes.py @@ -17,6 +17,9 @@ class Averager: Sometimes known as "online statistics," the running average maintains the total and count. The average can then be calculated at any time. + Assumes the key will not be evicted. Set the eviction policy to 'none' on + the cache to guarantee the key is not evicted. + >>> import diskcache >>> cache = diskcache.FanoutCache() >>> ave = Averager(cache, 'latency') @@ -65,6 +68,9 @@ def pop(self): class Lock: """Recipe for cross-process and cross-thread lock. + Assumes the key will not be evicted. Set the eviction policy to 'none' on + the cache to guarantee the key is not evicted. + >>> import diskcache >>> cache = diskcache.Cache() >>> lock = Lock(cache, 'report-123') @@ -113,6 +119,9 @@ def __exit__(self, *exc_info): class RLock: """Recipe for cross-process and cross-thread re-entrant lock. + Assumes the key will not be evicted. Set the eviction policy to 'none' on + the cache to guarantee the key is not evicted. + >>> import diskcache >>> cache = diskcache.Cache() >>> rlock = RLock(cache, 'user-123') @@ -181,6 +190,9 @@ def __exit__(self, *exc_info): class BoundedSemaphore: """Recipe for cross-process and cross-thread bounded semaphore. + Assumes the key will not be evicted. Set the eviction policy to 'none' on + the cache to guarantee the key is not evicted. + >>> import diskcache >>> cache = diskcache.Cache() >>> semaphore = BoundedSemaphore(cache, 'max-cons', value=2) @@ -251,6 +263,9 @@ def throttle( ): """Decorator to throttle calls to function. + Assumes keys will not be evicted. Set the eviction policy to 'none' on the + cache to guarantee the keys are not evicted. + >>> import diskcache, time >>> cache = diskcache.Cache() >>> count = 0 @@ -305,6 +320,9 @@ def barrier(cache, lock_factory, name=None, expire=None, tag=None): Supports different kinds of locks: Lock, RLock, BoundedSemaphore. + Assumes keys will not be evicted. Set the eviction policy to 'none' on the + cache to guarantee the keys are not evicted. + >>> import diskcache, time >>> cache = diskcache.Cache() >>> @barrier(cache, Lock) From fb2fa2c401bb88ecbbc58009d02cbe65f2fc594a Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 22:52:52 -0700 Subject: [PATCH 184/211] Test on Django 4.2 LTS --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index 36ed2b8..0ddcc8f 100644 --- a/tox.ini +++ b/tox.ini @@ -5,7 +5,7 @@ skip_missing_interpreters=True [testenv] commands=pytest deps= - django==3.2.* + django==4.2.* pytest pytest-cov pytest-django @@ -31,7 +31,7 @@ allowlist_externals=make changedir=docs commands=make html deps= - django==3.2.* + django==4.2.* sphinx [testenv:flake8] @@ -53,7 +53,7 @@ deps=mypy [testenv:pylint] commands=pylint {toxinidir}/diskcache deps= - django==3.2.* + django==4.2.* pylint [testenv:rstcheck] From 0a9783353ff7dc9f874154ef98b23de27e4aba8d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 22:53:17 -0700 Subject: [PATCH 185/211] Update year to 2023 --- README.rst | 4 ++-- diskcache/__init__.py | 2 +- docs/conf.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index eb06a6a..04abdc0 100644 --- a/README.rst +++ b/README.rst @@ -4,7 +4,7 @@ DiskCache: Disk Backed Cache `DiskCache`_ is an Apache2 licensed disk and file backed cache library, written in pure-Python, and compatible with Django. -The cloud-based computing of 2021 puts a premium on memory. Gigabytes of empty +The cloud-based computing of 2023 puts a premium on memory. Gigabytes of empty space is left on disks as processes vie for memory. Among these processes is Memcached (and sometimes Redis) which is used as a cache. Wouldn't it be nice to leverage empty disk space for caching? @@ -387,7 +387,7 @@ Reference License ------- -Copyright 2016-2022 Grant Jenks +Copyright 2016-2023 Grant Jenks Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 2355128..f7aa771 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -65,4 +65,4 @@ __build__ = 0x050400 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' -__copyright__ = 'Copyright 2016-2022 Grant Jenks' +__copyright__ = 'Copyright 2016-2023 Grant Jenks' diff --git a/docs/conf.py b/docs/conf.py index 92ce1b9..92bf3ec 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,7 @@ # -- Project information ----------------------------------------------------- project = 'DiskCache' -copyright = '2022, Grant Jenks' +copyright = '2023, Grant Jenks' author = 'Grant Jenks' # The full version, including alpha/beta/rc tags From 712cc1827b29fb8f6a76803d32f87a5fb57f3c6e Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 22:56:16 -0700 Subject: [PATCH 186/211] Bump python testing to 3.11 --- .github/workflows/integration.yml | 4 ++-- .github/workflows/release.yml | 2 +- tox.ini | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 07a5650..2d83ad3 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | pip install --upgrade pip @@ -32,7 +32,7 @@ jobs: max-parallel: 8 matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.6, 3.7, 3.8, 3.9, '3.10'] + python-version: [3.7, 3.8, 3.9, '3.10', 3.11] steps: - name: Set up Python ${{ matrix.python-version }} x64 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1f89c14..676593c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v2 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | diff --git a/tox.ini b/tox.ini index 0ddcc8f..3735ebc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=bluecheck,doc8,docs,isortcheck,flake8,mypy,pylint,rstcheck,py36,py37,py38,py39 +envlist=bluecheck,doc8,docs,isortcheck,flake8,mypy,pylint,rstcheck,py37,py38,py39,py310,py311 skip_missing_interpreters=True [testenv] From d7ae0990b240e8ea22c1a4060f58900edd339a18 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 23:07:29 -0700 Subject: [PATCH 187/211] i blue it --- diskcache/core.py | 10 ++--- diskcache/fanout.py | 2 +- tests/benchmark_glob.py | 2 +- tests/settings.py | 2 +- tests/stress_test_core.py | 30 ++++++------- tests/stress_test_fanout.py | 30 ++++++------- tests/test_core.py | 84 ++++++++++++++++++------------------- tests/test_djangocache.py | 2 +- tests/test_fanout.py | 16 +++---- 9 files changed, 85 insertions(+), 93 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index fb343be..05a0854 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -49,14 +49,14 @@ def __repr__(self): 'statistics': 0, # False 'tag_index': 0, # False 'eviction_policy': 'least-recently-stored', - 'size_limit': 2 ** 30, # 1gb + 'size_limit': 2**30, # 1gb 'cull_limit': 10, 'sqlite_auto_vacuum': 1, # FULL - 'sqlite_cache_size': 2 ** 13, # 8,192 pages + 'sqlite_cache_size': 2**13, # 8,192 pages 'sqlite_journal_mode': 'wal', - 'sqlite_mmap_size': 2 ** 26, # 64mb + 'sqlite_mmap_size': 2**26, # 64mb 'sqlite_synchronous': 1, # NORMAL - 'disk_min_file_size': 2 ** 15, # 32kb + 'disk_min_file_size': 2**15, # 32kb 'disk_pickle_protocol': pickle.HIGHEST_PROTOCOL, } @@ -212,7 +212,7 @@ def store(self, value, read, key=UNKNOWN): size = op.getsize(full_path) return size, MODE_TEXT, filename, None elif read: - reader = ft.partial(value.read, 2 ** 22) + reader = ft.partial(value.read, 2**22) filename, full_path = self.filename(key, value) iterator = iter(reader, b'') size = self._write(full_path, iterator, 'xb') diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 8fe51d9..5283490 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -45,7 +45,7 @@ def __init__( timeout=timeout, disk=disk, size_limit=size_limit, - **settings + **settings, ) for num in range(shards) ) diff --git a/tests/benchmark_glob.py b/tests/benchmark_glob.py index 7f0bf7c..7da5fd3 100644 --- a/tests/benchmark_glob.py +++ b/tests/benchmark_glob.py @@ -22,7 +22,7 @@ print(template % ('Count', 'Time')) print(' '.join(['=' * size] * len(cols))) -for count in [10 ** exp for exp in range(6)]: +for count in [10**exp for exp in range(6)]: for value in range(count): with open(op.join('tmp', '%s.tmp' % value), 'wb') as writer: pass diff --git a/tests/settings.py b/tests/settings.py index 1a2f569..04aee85 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -25,7 +25,7 @@ # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -ALLOWED_HOSTS = [u'testserver'] +ALLOWED_HOSTS = ['testserver'] # Application definition diff --git a/tests/stress_test_core.py b/tests/stress_test_core.py index c30fa3f..2b2578b 100644 --- a/tests/stress_test_core.py +++ b/tests/stress_test_core.py @@ -33,16 +33,14 @@ def make_long(): def make_unicode(): word_size = random.randint(1, 26) - word = u''.join( - random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) - ) + word = ''.join(random.sample('abcdefghijklmnopqrstuvwxyz', word_size)) size = random.randint(1, int(200 / 13)) return word * size def make_bytes(): word_size = random.randint(1, 26) - word = u''.join( - random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + word = ''.join( + random.sample('abcdefghijklmnopqrstuvwxyz', word_size) ).encode('utf-8') size = random.randint(1, int(200 / 13)) return word * size @@ -77,18 +75,16 @@ def make_long(): def make_unicode(): word_size = random.randint(1, 26) - word = u''.join( - random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) - ) - size = random.randint(1, int(2 ** 16 / 13)) + word = ''.join(random.sample('abcdefghijklmnopqrstuvwxyz', word_size)) + size = random.randint(1, int(2**16 / 13)) return word * size def make_bytes(): word_size = random.randint(1, 26) - word = u''.join( - random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + word = ''.join( + random.sample('abcdefghijklmnopqrstuvwxyz', word_size) ).encode('utf-8') - size = random.randint(1, int(2 ** 16 / 13)) + size = random.randint(1, int(2**16 / 13)) return word * size def make_float(): @@ -233,7 +229,7 @@ def percentile(sequence, percent): def stress_test( create=True, delete=True, - eviction_policy=u'least-recently-stored', + eviction_policy='least-recently-stored', processes=1, threads=1, ): @@ -293,17 +289,17 @@ def stress_test( def stress_test_lru(): """Stress test least-recently-used eviction policy.""" - stress_test(eviction_policy=u'least-recently-used') + stress_test(eviction_policy='least-recently-used') def stress_test_lfu(): """Stress test least-frequently-used eviction policy.""" - stress_test(eviction_policy=u'least-frequently-used') + stress_test(eviction_policy='least-frequently-used') def stress_test_none(): """Stress test 'none' eviction policy.""" - stress_test(eviction_policy=u'none') + stress_test(eviction_policy='none') def stress_test_mp(): @@ -396,7 +392,7 @@ def stress_test_mp(): '-v', '--eviction-policy', type=str, - default=u'least-recently-stored', + default='least-recently-stored', ) args = parser.parse_args() diff --git a/tests/stress_test_fanout.py b/tests/stress_test_fanout.py index d3b67e3..e78dda5 100644 --- a/tests/stress_test_fanout.py +++ b/tests/stress_test_fanout.py @@ -32,16 +32,14 @@ def make_long(): def make_unicode(): word_size = random.randint(1, 26) - word = u''.join( - random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) - ) + word = ''.join(random.sample('abcdefghijklmnopqrstuvwxyz', word_size)) size = random.randint(1, int(200 / 13)) return word * size def make_bytes(): word_size = random.randint(1, 26) - word = u''.join( - random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + word = ''.join( + random.sample('abcdefghijklmnopqrstuvwxyz', word_size) ).encode('utf-8') size = random.randint(1, int(200 / 13)) return word * size @@ -76,18 +74,16 @@ def make_long(): def make_unicode(): word_size = random.randint(1, 26) - word = u''.join( - random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) - ) - size = random.randint(1, int(2 ** 16 / 13)) + word = ''.join(random.sample('abcdefghijklmnopqrstuvwxyz', word_size)) + size = random.randint(1, int(2**16 / 13)) return word * size def make_bytes(): word_size = random.randint(1, 26) - word = u''.join( - random.sample(u'abcdefghijklmnopqrstuvwxyz', word_size) + word = ''.join( + random.sample('abcdefghijklmnopqrstuvwxyz', word_size) ).encode('utf-8') - size = random.randint(1, int(2 ** 16 / 13)) + size = random.randint(1, int(2**16 / 13)) return word * size def make_float(): @@ -224,7 +220,7 @@ def percentile(sequence, percent): def stress_test( create=True, delete=True, - eviction_policy=u'least-recently-stored', + eviction_policy='least-recently-stored', processes=1, threads=1, ): @@ -284,17 +280,17 @@ def stress_test( def stress_test_lru(): """Stress test least-recently-used eviction policy.""" - stress_test(eviction_policy=u'least-recently-used') + stress_test(eviction_policy='least-recently-used') def stress_test_lfu(): """Stress test least-frequently-used eviction policy.""" - stress_test(eviction_policy=u'least-frequently-used') + stress_test(eviction_policy='least-frequently-used') def stress_test_none(): """Stress test 'none' eviction policy.""" - stress_test(eviction_policy=u'none') + stress_test(eviction_policy='none') def stress_test_mp(): @@ -387,7 +383,7 @@ def stress_test_mp(): '-v', '--eviction-policy', type=str, - default=u'least-recently-stored', + default='least-recently-stored', ) args = parser.parse_args() diff --git a/tests/test_core.py b/tests/test_core.py index 55ca962..356d104 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -38,11 +38,11 @@ def test_init(cache): def test_init_disk(): - with dc.Cache(disk_pickle_protocol=1, disk_min_file_size=2 ** 20) as cache: + with dc.Cache(disk_pickle_protocol=1, disk_min_file_size=2**20) as cache: key = (None, 0, 'abc') cache[key] = 0 cache.check() - assert cache.disk_min_file_size == 2 ** 20 + assert cache.disk_min_file_size == 2**20 assert cache.disk_pickle_protocol == 1 shutil.rmtree(cache.directory, ignore_errors=True) @@ -59,15 +59,15 @@ def test_disk_reset(): assert cache._disk.min_file_size == 0 assert cache._disk.pickle_protocol == 0 - cache.reset('disk_min_file_size', 2 ** 10) + cache.reset('disk_min_file_size', 2**10) cache.reset('disk_pickle_protocol', 2) cache[1] = value cache.check() - assert cache.disk_min_file_size == 2 ** 10 + assert cache.disk_min_file_size == 2**10 assert cache.disk_pickle_protocol == 2 - assert cache._disk.min_file_size == 2 ** 10 + assert cache._disk.min_file_size == 2**10 assert cache._disk.pickle_protocol == 2 shutil.rmtree(cache.directory, ignore_errors=True) @@ -150,7 +150,7 @@ def test_pragma_error(cache): cursor.fetchall = fetchall fetchall.side_effect = [sqlite3.OperationalError] * 60000 - size = 2 ** 28 + size = 2**28 with mock.patch('time.sleep', lambda num: 0): with mock.patch.object(cache, '_local', local): @@ -177,15 +177,15 @@ def __getattr__(self, name): def test_getsetdel(cache): values = [ (None, False), - ((None,) * 2 ** 20, False), + ((None,) * 2**20, False), (1234, False), - (2 ** 512, False), + (2**512, False), (56.78, False), - (u'hello', False), - (u'hello' * 2 ** 20, False), + ('hello', False), + ('hello' * 2**20, False), (b'world', False), - (b'world' * 2 ** 20, False), - (io.BytesIO(b'world' * 2 ** 20), True), + (b'world' * 2**20, False), + (io.BytesIO(b'world' * 2**20), True), ] for key, (value, file_like) in enumerate(values): @@ -229,7 +229,7 @@ def test_get_keyerror4(cache): func = mock.Mock(side_effect=IOError(errno.ENOENT, '')) cache.reset('statistics', True) - cache[0] = b'abcd' * 2 ** 20 + cache[0] = b'abcd' * 2**20 with mock.patch('diskcache.core.open', func): with pytest.raises((IOError, KeyError, OSError)): @@ -237,7 +237,7 @@ def test_get_keyerror4(cache): def test_read(cache): - cache.set(0, b'abcd' * 2 ** 20) + cache.set(0, b'abcd' * 2**20) with cache.read(0) as reader: assert reader is not None @@ -249,7 +249,7 @@ def test_read_keyerror(cache): def test_set_twice(cache): - large_value = b'abcd' * 2 ** 20 + large_value = b'abcd' * 2**20 cache[0] = 0 cache[0] = 1 @@ -283,7 +283,7 @@ def test_set_timeout(cache): with pytest.raises(dc.Timeout): try: with mock.patch.object(cache, '_local', local): - cache.set('a', 'b' * 2 ** 20) + cache.set('a', 'b' * 2**20) finally: cache.check() @@ -299,11 +299,11 @@ def test_get(cache): assert cache.get(2, {}) == {} assert cache.get(0, expire_time=True, tag=True) == (None, None, None) - assert cache.set(0, 0, expire=None, tag=u'number') + assert cache.set(0, 0, expire=None, tag='number') assert cache.get(0, expire_time=True) == (0, None) - assert cache.get(0, tag=True) == (0, u'number') - assert cache.get(0, expire_time=True, tag=True) == (0, None, u'number') + assert cache.get(0, tag=True) == (0, 'number') + assert cache.get(0, expire_time=True, tag=True) == (0, None, 'number') def test_get_expired_fast_path(cache): @@ -359,8 +359,8 @@ def test_pop(cache): assert cache.set('delta', 210) assert cache.pop('delta', expire_time=True) == (210, None) - assert cache.set('epsilon', '0' * 2 ** 20) - assert cache.pop('epsilon') == '0' * 2 ** 20 + assert cache.set('epsilon', '0' * 2**20) + assert cache.pop('epsilon') == '0' * 2**20 def test_pop_ioerror(cache): @@ -426,11 +426,11 @@ def test_stats(cache): def test_path(cache): - cache[0] = u'abc' - large_value = b'abc' * 2 ** 20 + cache[0] = 'abc' + large_value = b'abc' * 2**20 cache[1] = large_value - assert cache.get(0, read=True) == u'abc' + assert cache.get(0, read=True) == 'abc' with cache.get(1, read=True) as reader: assert reader.name is not None @@ -465,7 +465,7 @@ def test_expire_rows(cache): def test_least_recently_stored(cache): - cache.reset('eviction_policy', u'least-recently-stored') + cache.reset('eviction_policy', 'least-recently-stored') cache.reset('size_limit', int(10.1e6)) cache.reset('cull_limit', 2) @@ -500,7 +500,7 @@ def test_least_recently_stored(cache): def test_least_recently_used(cache): - cache.reset('eviction_policy', u'least-recently-used') + cache.reset('eviction_policy', 'least-recently-used') cache.reset('size_limit', int(10.1e6)) cache.reset('cull_limit', 5) @@ -530,7 +530,7 @@ def test_least_recently_used(cache): def test_least_frequently_used(cache): - cache.reset('eviction_policy', u'least-frequently-used') + cache.reset('eviction_policy', 'least-frequently-used') cache.reset('size_limit', int(10.1e6)) cache.reset('cull_limit', 5) @@ -558,8 +558,8 @@ def test_least_frequently_used(cache): def test_check(cache): - blob = b'a' * 2 ** 20 - keys = (0, 1, 1234, 56.78, u'hello', b'world', None) + blob = b'a' * 2**20 + keys = (0, 1, 1234, 56.78, 'hello', b'world', None) for key in keys: cache[key] = blob @@ -662,12 +662,12 @@ def test_clear_timeout(cache): def test_tag(cache): - assert cache.set(0, None, tag=u'zero') + assert cache.set(0, None, tag='zero') assert cache.set(1, None, tag=1234) assert cache.set(2, None, tag=5.67) assert cache.set(3, None, tag=b'three') - assert cache.get(0, tag=True) == (None, u'zero') + assert cache.get(0, tag=True) == (None, 'zero') assert cache.get(1, tag=True) == (None, 1234) assert cache.get(2, tag=True) == (None, 5.67) assert cache.get(3, tag=True) == (None, b'three') @@ -675,11 +675,11 @@ def test_tag(cache): def test_with(cache): with dc.Cache(cache.directory) as tmp: - tmp[u'a'] = 0 - tmp[u'b'] = 1 + tmp['a'] = 0 + tmp['b'] = 1 - assert cache[u'a'] == 0 - assert cache[u'b'] == 1 + assert cache['a'] == 0 + assert cache['b'] == 1 def test_contains(cache): @@ -708,7 +708,7 @@ def test_add(cache): def test_add_large_value(cache): - value = b'abcd' * 2 ** 20 + value = b'abcd' * 2**20 assert cache.add(b'test-key', value) assert cache.get(b'test-key') == value assert not cache.add(b'test-key', value * 2) @@ -919,7 +919,7 @@ def test_push_peek_expire(cache): def test_push_pull_large_value(cache): - value = b'test' * (2 ** 20) + value = b'test' * (2**20) cache.push(value) assert cache.pull() == (500000000000000, value) assert len(cache) == 0 @@ -927,7 +927,7 @@ def test_push_pull_large_value(cache): def test_push_peek_large_value(cache): - value = b'test' * (2 ** 20) + value = b'test' * (2**20) cache.push(value) assert cache.peek() == (500000000000000, value) assert len(cache) == 1 @@ -1144,8 +1144,8 @@ def test_cull_timeout(cache): def test_key_roundtrip(cache): - key_part_0 = u'part0' - key_part_1 = u'part1' + key_part_0 = 'part0' + key_part_1 = 'part1' to_test = [ (key_part_0, key_part_1), [key_part_0, key_part_1], @@ -1354,7 +1354,7 @@ def foo(*args, **kwargs): def test_cleanup_dirs(cache): - value = b'\0' * 2 ** 20 + value = b'\0' * 2**20 start_count = len(os.listdir(cache.directory)) for i in range(10): cache[i] = value @@ -1370,7 +1370,7 @@ def test_disk_write_os_error(cache): func = mock.Mock(side_effect=[OSError] * 10) with mock.patch('diskcache.core.open', func): with pytest.raises(OSError): - cache[0] = '\0' * 2 ** 20 + cache[0] = '\0' * 2**20 def test_memoize_ignore(cache): diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index 5f83b81..b5cc2a8 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -1033,7 +1033,7 @@ def test_directory(self): self.assertTrue('tmp' in cache.directory) def test_read(self): - value = b'abcd' * 2 ** 20 + value = b'abcd' * 2**20 result = cache.set(b'test-key', value) self.assertTrue(result) diff --git a/tests/test_fanout.py b/tests/test_fanout.py index f212fac..deea03f 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -34,7 +34,7 @@ def test_init(cache): del default_settings['size_limit'] for key, value in default_settings.items(): assert getattr(cache, key) == value - assert cache.size_limit == 2 ** 27 + assert cache.size_limit == 2**27 cache.check() @@ -229,15 +229,15 @@ def test_incr_concurrent(): def test_getsetdel(cache): values = [ (None, False), - ((None,) * 2 ** 10, False), + ((None,) * 2**10, False), (1234, False), - (2 ** 512, False), + (2**512, False), (56.78, False), - (u'hello', False), - (u'hello' * 2 ** 10, False), + ('hello', False), + ('hello' * 2**10, False), (b'world', False), - (b'world' * 2 ** 10, False), - (io.BytesIO(b'world' * 2 ** 10), True), + (b'world' * 2**10, False), + (io.BytesIO(b'world' * 2**10), True), ] for key, (value, file_like) in enumerate(values): @@ -341,7 +341,7 @@ def test_tag_index(cache): def test_read(cache): - cache.set(0, b'abcd' * 2 ** 20) + cache.set(0, b'abcd' * 2**20) with cache.read(0) as reader: assert reader is not None From a53283d554fbe7bd678067f24d6f45d57de3f83b Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 23:25:16 -0700 Subject: [PATCH 188/211] Update requirements --- requirements-dev.txt | 23 +++++++++++++++++++++++ requirements.txt | 22 ---------------------- 2 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 requirements-dev.txt diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..6149361 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,23 @@ +-e . +blue +coverage +django==4.2.* +django_redis +doc8 +flake8 +ipython +jedi +pickleDB +pylibmc +pylint +pytest +pytest-cov +pytest-django +pytest-env +pytest-xdist +rstcheck +sphinx +sqlitedict +tox +twine +wheel diff --git a/requirements.txt b/requirements.txt index efb2160..d6e1198 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,23 +1 @@ -e . -blue -coverage -django==3.2.* -django_redis -doc8 -flake8 -ipython -jedi==0.17.* # Remove after IPython bug fixed. -pickleDB -pylibmc -pylint -pytest -pytest-cov -pytest-django -pytest-env -pytest-xdist -rstcheck -sphinx -sqlitedict -tox -twine -wheel From b22a7d58c3dbf3f71fa4f9156ccbd4892ebf3fe2 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 23:25:35 -0700 Subject: [PATCH 189/211] Update pylint --- .pylintrc | 829 ++++++++++++++++++++++++---------------------- diskcache/core.py | 6 +- 2 files changed, 439 insertions(+), 396 deletions(-) diff --git a/.pylintrc b/.pylintrc index 6baa978..dc1490a 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,27 +1,77 @@ -[MASTER] +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= # A comma-separated list of package or module names from where C extensions may # be loaded. Extensions are loading into the active Python interpreter and may # run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) extension-pkg-whitelist= -# Specify a score threshold to be exceeded before program exits with error. -fail-under=10.0 +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= -# Add files or directories to the blacklist. They should be base names, not -# paths. +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. ignore=CVS -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). #init-hook= # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. jobs=1 # Control the amount of potential inferred values when inferring a single @@ -36,6 +86,19 @@ load-plugins= # Pickle collected data for later comparisons. persistent=yes +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.11 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + # When enabled, pylint would attempt to guess common misconfiguration and emit # user-friendly hints instead of false-positive error messages. suggestion-mode=yes @@ -44,321 +107,8 @@ suggestion-mode=yes # active Python interpreter and may run arbitrary code. unsafe-load-any-extension=no - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - use-symbolic-message-instead, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - deprecated-operator-function, - deprecated-urllib-function, - xreadlines-attribute, - deprecated-sys-function, - exception-escape, - comprehension-escape, - no-member, - no-else-return, - duplicate-code, - inconsistent-return-statements, - consider-using-f-string, - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module. -max-module-lines=3000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=yes - -# Minimum lines number of a similarity. -min-similarity-lines=4 +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= [BASIC] @@ -367,13 +117,15 @@ min-similarity-lines=4 argument-naming-style=snake_case # Regular expression matching correct argument names. Overrides argument- -# naming-style. +# naming-style. If left empty, argument names will be checked with the set +# naming style. #argument-rgx= # Naming style matching correct attribute names. attr-naming-style=snake_case # Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming # style. #attr-rgx= @@ -393,20 +145,30 @@ bad-names-rgxs= class-attribute-naming-style=any # Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. #class-attribute-rgx= +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + # Naming style matching correct class names. class-naming-style=PascalCase # Regular expression matching correct class names. Overrides class-naming- -# style. +# style. If left empty, class names will be checked with the set naming style. #class-rgx= # Naming style matching correct constant names. const-naming-style=UPPER_CASE # Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming # style. #const-rgx= @@ -418,7 +180,8 @@ docstring-min-length=-1 function-naming-style=snake_case # Regular expression matching correct function names. Overrides function- -# naming-style. +# naming-style. If left empty, function names will be checked with the set +# naming style. #function-rgx= # Good variable names which should always be accepted, separated by a comma. @@ -440,21 +203,22 @@ include-naming-hint=no inlinevar-naming-style=any # Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. #inlinevar-rgx= # Naming style matching correct method names. method-naming-style=snake_case # Regular expression matching correct method names. Overrides method-naming- -# style. +# style. If left empty, method names will be checked with the set naming style. #method-rgx= # Naming style matching correct module names. module-naming-style=snake_case # Regular expression matching correct module names. Overrides module-naming- -# style. +# style. If left empty, module names will be checked with the set naming style. #module-rgx= # Colon-delimited sets of names that determine each other's naming style when @@ -470,90 +234,56 @@ no-docstring-rgx=^_ # These decorators are taken in consideration only for invalid-name. property-classes=abc.abstractproperty +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + # Naming style matching correct variable names. variable-naming-style=snake_case # Regular expression matching correct variable names. Overrides variable- -# naming-style. +# naming-style. If left empty, variable names will be checked with the set +# naming style. #variable-rgx= -[STRING] - -# This flag controls whether inconsistent-quotes generates a warning when the -# character used as a quote delimiter is used inconsistently within a module. -check-quote-consistency=no +[CLASSES] -# This flag controls whether the implicit-str-concat should generate a warning -# on implicit string concatenation in sequences defined over several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[CLASSES] +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no # List of method names used to declare (i.e. assign) instance attributes. defining-attr-methods=__init__, __new__, setUp, + asyncSetUp, __post_init__ # List of member names, which should be excluded from the protected access # warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit # List of valid names for the first argument in a class method. valid-classmethod-first-arg=cls # List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls +valid-metaclass-classmethod-first-arg=mcs [DESIGN] +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + # Maximum number of arguments for function / method. max-args=8 @@ -587,7 +317,320 @@ min-public-methods=2 [EXCEPTIONS] -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=BaseException, - Exception +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=2500 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + consider-using-f-string, + no-member, + no-else-return, + no-else-raise, + inconsistent-return-statements + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=20 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work.. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io diff --git a/diskcache/core.py b/diskcache/core.py index 05a0854..9f3a597 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -170,7 +170,7 @@ def get(self, key, raw): :return: corresponding Python key """ - # pylint: disable=no-self-use,unidiomatic-typecheck + # pylint: disable=unidiomatic-typecheck if raw: return bytes(key) if type(key) is sqlite3.Binary else key else: @@ -228,7 +228,6 @@ def store(self, value, read, key=UNKNOWN): return len(result), MODE_PICKLE, filename, None def _write(self, full_path, iterator, mode, encoding=None): - # pylint: disable=no-self-use full_dir, _ = op.split(full_path) for count in range(1, 11): @@ -264,7 +263,7 @@ def fetch(self, mode, filename, value, read): :raises: IOError if the value cannot be read """ - # pylint: disable=no-self-use,unidiomatic-typecheck,consider-using-with + # pylint: disable=unidiomatic-typecheck,consider-using-with if mode == MODE_RAW: return bytes(value) if type(value) is sqlite3.Binary else value elif mode == MODE_BINARY: @@ -1378,6 +1377,7 @@ def delete(self, key, retry=False): :raises Timeout: if database timeout occurs """ + # pylint: disable=unnecessary-dunder-call try: return self.__delitem__(key, retry=retry) except KeyError: From 471aa5e551aed186dd34d5bc7d345be835efd6f3 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 23:26:38 -0700 Subject: [PATCH 190/211] Drop Python 3.7 from testing --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 3735ebc..e7217a7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist=bluecheck,doc8,docs,isortcheck,flake8,mypy,pylint,rstcheck,py37,py38,py39,py310,py311 +envlist=bluecheck,doc8,docs,isortcheck,flake8,mypy,pylint,rstcheck,py38,py39,py310,py311 skip_missing_interpreters=True [testenv] From c14345f105f14eba45986b09ec96d87b8997c9cc Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 23:42:38 -0700 Subject: [PATCH 191/211] Update tests for Django 4.2 --- tests/test_djangocache.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/test_djangocache.py b/tests/test_djangocache.py index b5cc2a8..734ba1b 100644 --- a/tests/test_djangocache.py +++ b/tests/test_djangocache.py @@ -870,7 +870,6 @@ def test_custom_key_func(self): def test_cache_write_unpicklable_object(self): fetch_middleware = FetchFromCacheMiddleware(empty_response) - fetch_middleware.cache = cache request = self.factory.get('/cache/test') request._cache_update_cache = True @@ -887,7 +886,6 @@ def get_response(req): return response update_middleware = UpdateCacheMiddleware(get_response) - update_middleware.cache = cache response = update_middleware(request) get_cache_data = fetch_middleware.process_request(request) From 0294d58cd0cb72dd6affbbc46b3ff05d96d015cc Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 23:43:15 -0700 Subject: [PATCH 192/211] Bump version to v5.5.0 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index f7aa771..8d7e28c 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -61,8 +61,8 @@ pass __title__ = 'diskcache' -__version__ = '5.4.0' -__build__ = 0x050400 +__version__ = '5.5.0' +__build__ = 0x050500 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2023 Grant Jenks' From 6cd6888a16be1d531a19ca91d5e0daa6edac9718 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 23:45:14 -0700 Subject: [PATCH 193/211] Drop 3.7 from CI --- .github/workflows/integration.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 2d83ad3..07aceec 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -32,7 +32,7 @@ jobs: max-parallel: 8 matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: [3.7, 3.8, 3.9, '3.10', 3.11] + python-version: [3.8, 3.9, '3.10', 3.11] steps: - name: Set up Python ${{ matrix.python-version }} x64 From bbac13b54f9ac0a5c45b7cfbd242869b41042cac Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 23:52:57 -0700 Subject: [PATCH 194/211] Install dev requirements for wheel package --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 676593c..21b6e5b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | pip install --upgrade pip - pip install -r requirements.txt + pip install -r requirements-dev.txt - name: Create source dist run: python setup.py sdist From 0f5d8ed63406e26de96701f98aa585b4fb26f6dd Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 10 Apr 2023 23:53:20 -0700 Subject: [PATCH 195/211] Bump version to 5.5.1 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 8d7e28c..95dafb3 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -61,8 +61,8 @@ pass __title__ = 'diskcache' -__version__ = '5.5.0' -__build__ = 0x050500 +__version__ = '5.5.1' +__build__ = 0x050501 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2023 Grant Jenks' From fe5ee43ac5df1847556f280c696ab921f0910be2 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 16 Apr 2023 21:42:13 -0700 Subject: [PATCH 196/211] Close the cache explicitly before deleting the reference --- diskcache/persistent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/diskcache/persistent.py b/diskcache/persistent.py index c3d570b..cce3736 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -533,12 +533,13 @@ def reverse(self): # GrantJ 2019-03-22 Consider using an algorithm that swaps the values # at two keys. Like self._cache.swap(key1, key2, retry=True) The swap # method would exchange the values at two given keys. Then, using a - # forward iterator and a reverse iterator, the reversis method could + # forward iterator and a reverse iterator, the reverse method could # avoid making copies of the values. temp = Deque(iterable=reversed(self)) self.clear() self.extend(temp) directory = temp.directory + temp.close() del temp rmtree(directory) From 9380c784d9e2954611b2ea309f1c657602085f25 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Sun, 16 Apr 2023 23:02:24 -0700 Subject: [PATCH 197/211] Oops, close the cache, not the deque --- diskcache/persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diskcache/persistent.py b/diskcache/persistent.py index cce3736..01cf4e3 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -539,7 +539,7 @@ def reverse(self): self.clear() self.extend(temp) directory = temp.directory - temp.close() + temp._cache.close() del temp rmtree(directory) From f5a17ff0959a4cc7147d45a4cc8d8eef8d7416b0 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Apr 2023 19:00:34 -0700 Subject: [PATCH 198/211] Shutup pylint --- diskcache/persistent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/diskcache/persistent.py b/diskcache/persistent.py index 01cf4e3..c3f22b5 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -530,6 +530,7 @@ def reverse(self): ['c', 'b', 'a'] """ + # pylint: disable=protected-access # GrantJ 2019-03-22 Consider using an algorithm that swaps the values # at two keys. Like self._cache.swap(key1, key2, retry=True) The swap # method would exchange the values at two given keys. Then, using a From ef94856d2447fa9662bc62557f9b96ba6f131e15 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Apr 2023 19:01:25 -0700 Subject: [PATCH 199/211] Bump version to 5.5.2 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 95dafb3..134c88a 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -61,8 +61,8 @@ pass __title__ = 'diskcache' -__version__ = '5.5.1' -__build__ = 0x050501 +__version__ = '5.5.2' +__build__ = 0x050502 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2023 Grant Jenks' From 74e554c5d9340765f6fd6f7891da49a420e92b70 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Apr 2023 19:05:01 -0700 Subject: [PATCH 200/211] Bump versions of checkout and setup-python --- .github/workflows/integration.yml | 8 ++++---- .github/workflows/release.yml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/integration.yml b/.github/workflows/integration.yml index 07aceec..b596fc6 100644 --- a/.github/workflows/integration.yml +++ b/.github/workflows/integration.yml @@ -12,9 +12,9 @@ jobs: check: [bluecheck, doc8, docs, flake8, isortcheck, mypy, pylint, rstcheck] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.11' - name: Install dependencies @@ -36,12 +36,12 @@ jobs: steps: - name: Set up Python ${{ matrix.python-version }} x64 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} architecture: x64 - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install tox run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 21b6e5b..efe73c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Install libmemcached-dev run: | @@ -19,7 +19,7 @@ jobs: sudo apt-get install libmemcached-dev - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: '3.11' From 35dbeabd283b242e9afd33713a5cea5cd260f51d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Apr 2023 20:38:04 -0700 Subject: [PATCH 201/211] Add maxlen parameter to diskcache.Deque (#191) * Add maxlen parameter to diskcache.Deque --- diskcache/djangocache.py | 5 ++- diskcache/fanout.py | 5 ++- diskcache/persistent.py | 91 +++++++++++++++++++++++++++++++--------- docs/tutorial.rst | 3 ++ tests/test_deque.py | 27 +++++++++++- 5 files changed, 107 insertions(+), 24 deletions(-) diff --git a/diskcache/djangocache.py b/diskcache/djangocache.py index 8bf85ce..5dc8ce2 100644 --- a/diskcache/djangocache.py +++ b/diskcache/djangocache.py @@ -44,14 +44,15 @@ def cache(self, name): """ return self._cache.cache(name) - def deque(self, name): + def deque(self, name, maxlen=None): """Return Deque with given `name` in subdirectory. :param str name: subdirectory name for Deque + :param maxlen: max length (default None, no max) :return: Deque with given name """ - return self._cache.deque(name) + return self._cache.deque(name, maxlen=maxlen) def index(self, name): """Return Index with given `name` in subdirectory. diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 5283490..50005fc 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -612,7 +612,7 @@ def cache(self, name, timeout=60, disk=None, **settings): _caches[name] = temp return temp - def deque(self, name): + def deque(self, name, maxlen=None): """Return Deque with given `name` in subdirectory. >>> cache = FanoutCache() @@ -626,6 +626,7 @@ def deque(self, name): 1 :param str name: subdirectory name for Deque + :param maxlen: max length (default None, no max) :return: Deque with given name """ @@ -641,7 +642,7 @@ def deque(self, name): disk=self._disk, eviction_policy='none', ) - deque = Deque.fromcache(cache) + deque = Deque.fromcache(cache, maxlen=maxlen) _deques[name] = deque return deque diff --git a/diskcache/persistent.py b/diskcache/persistent.py index c3f22b5..522bb74 100644 --- a/diskcache/persistent.py +++ b/diskcache/persistent.py @@ -75,7 +75,7 @@ class Deque(Sequence): """ - def __init__(self, iterable=(), directory=None): + def __init__(self, iterable=(), directory=None, maxlen=None): """Initialize deque instance. If directory is None then temporary directory created. The directory @@ -86,10 +86,11 @@ def __init__(self, iterable=(), directory=None): """ self._cache = Cache(directory, eviction_policy='none') - self.extend(iterable) + self._maxlen = float('inf') if maxlen is None else maxlen + self._extend(iterable) @classmethod - def fromcache(cls, cache, iterable=()): + def fromcache(cls, cache, iterable=(), maxlen=None): """Initialize deque using `cache`. >>> cache = Cache() @@ -111,7 +112,8 @@ def fromcache(cls, cache, iterable=()): # pylint: disable=no-member,protected-access self = cls.__new__(cls) self._cache = cache - self.extend(iterable) + self._maxlen = float('inf') if maxlen is None else maxlen + self._extend(iterable) return self @property @@ -124,6 +126,31 @@ def directory(self): """Directory path where deque is stored.""" return self._cache.directory + @property + def maxlen(self): + """Max length of the deque.""" + return self._maxlen + + @maxlen.setter + def maxlen(self, value): + """Set max length of the deque. + + Pops items from left while length greater than max. + + >>> deque = Deque() + >>> deque.extendleft('abcde') + >>> deque.maxlen = 3 + >>> list(deque) + ['c', 'd', 'e'] + + :param value: max length + + """ + self._maxlen = value + with self._cache.transact(retry=True): + while len(self._cache) > self._maxlen: + self._popleft() + def _index(self, index, func): len_self = len(self) @@ -244,7 +271,7 @@ def __iadd__(self, iterable): :return: deque with added items """ - self.extend(iterable) + self._extend(iterable) return self def __iter__(self): @@ -292,10 +319,11 @@ def __reversed__(self): pass def __getstate__(self): - return self.directory + return self.directory, self.maxlen def __setstate__(self, state): - self.__init__(directory=state) + directory, maxlen = state + self.__init__(directory=directory, maxlen=maxlen) def append(self, value): """Add `value` to back of deque. @@ -310,7 +338,12 @@ def append(self, value): :param value: value to add to back of deque """ - self._cache.push(value, retry=True) + with self._cache.transact(retry=True): + self._cache.push(value, retry=True) + if len(self._cache) > self._maxlen: + self._popleft() + + _append = append def appendleft(self, value): """Add `value` to front of deque. @@ -325,7 +358,12 @@ def appendleft(self, value): :param value: value to add to front of deque """ - self._cache.push(value, side='front', retry=True) + with self._cache.transact(retry=True): + self._cache.push(value, side='front', retry=True) + if len(self._cache) > self._maxlen: + self._pop() + + _appendleft = appendleft def clear(self): """Remove all elements from deque. @@ -340,6 +378,13 @@ def clear(self): """ self._cache.clear(retry=True) + _clear = clear + + def copy(self): + """Copy deque with same directory and max length.""" + TypeSelf = type(self) + return TypeSelf(directory=self.directory, maxlen=self.maxlen) + def count(self, value): """Return number of occurrences of `value` in deque. @@ -365,7 +410,9 @@ def extend(self, iterable): """ for value in iterable: - self.append(value) + self._append(value) + + _extend = extend def extendleft(self, iterable): """Extend front side of deque with value from `iterable`. @@ -379,7 +426,7 @@ def extendleft(self, iterable): """ for value in iterable: - self.appendleft(value) + self._appendleft(value) def peek(self): """Peek at value at back of deque. @@ -459,6 +506,8 @@ def pop(self): raise IndexError('pop from an empty deque') return value + _pop = pop + def popleft(self): """Remove and return value at front of deque. @@ -483,6 +532,8 @@ def popleft(self): raise IndexError('pop from an empty deque') return value + _popleft = popleft + def remove(self, value): """Remove first occurrence of `value` in deque. @@ -537,8 +588,8 @@ def reverse(self): # forward iterator and a reverse iterator, the reverse method could # avoid making copies of the values. temp = Deque(iterable=reversed(self)) - self.clear() - self.extend(temp) + self._clear() + self._extend(temp) directory = temp.directory temp._cache.close() del temp @@ -575,22 +626,22 @@ def rotate(self, steps=1): for _ in range(steps): try: - value = self.pop() + value = self._pop() except IndexError: return else: - self.appendleft(value) + self._appendleft(value) else: steps *= -1 steps %= len_self for _ in range(steps): try: - value = self.popleft() + value = self._popleft() except IndexError: return else: - self.append(value) + self._append(value) __hash__ = None # type: ignore @@ -669,7 +720,9 @@ def __init__(self, *args, **kwargs): args = args[1:] directory = None self._cache = Cache(directory, eviction_policy='none') - self.update(*args, **kwargs) + self._update(*args, **kwargs) + + _update = MutableMapping.update @classmethod def fromcache(cls, cache, *args, **kwargs): @@ -695,7 +748,7 @@ def fromcache(cls, cache, *args, **kwargs): # pylint: disable=no-member,protected-access self = cls.__new__(cls) self._cache = cache - self.update(*args, **kwargs) + self._update(*args, **kwargs) return self @property diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 1963635..69277d3 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -565,6 +565,9 @@ access and editing at both front and back sides. :class:`Deque 4 >>> other.popleft() 'foo' + >>> thing = Deque('abcde', maxlen=3) + >>> list(thing) + ['c', 'd', 'e'] :class:`Deque ` objects provide an efficient and safe means of cross-thread and cross-process communication. :class:`Deque ` diff --git a/tests/test_deque.py b/tests/test_deque.py index add7714..71c69e2 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -77,6 +77,20 @@ def test_getsetdel(deque): assert len(deque) == 0 +def test_append(deque): + deque.maxlen = 3 + for item in 'abcde': + deque.append(item) + assert deque == 'cde' + + +def test_appendleft(deque): + deque.maxlen = 3 + for item in 'abcde': + deque.appendleft(item) + assert deque == 'edc' + + def test_index_positive(deque): cache = mock.MagicMock() cache.__len__.return_value = 3 @@ -131,9 +145,12 @@ def test_state(deque): sequence = list('abcde') deque.extend(sequence) assert deque == sequence + deque.maxlen = 3 + assert list(deque) == sequence[-3:] state = pickle.dumps(deque) values = pickle.loads(state) - assert values == sequence + assert values == sequence[-3:] + assert values.maxlen == 3 def test_compare(deque): @@ -161,6 +178,14 @@ def test_repr(): assert repr(deque) == 'Deque(directory=%r)' % directory +def test_copy(deque): + sequence = list('abcde') + deque.extend(sequence) + temp = deque.copy() + assert deque == sequence + assert temp == sequence + + def test_count(deque): deque += 'abbcccddddeeeee' From 4beffe892a6c4352098a79614de40649d7e9f88e Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Apr 2023 20:40:55 -0700 Subject: [PATCH 202/211] Bump version to 5.6.0 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 134c88a..8647b9a 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -61,8 +61,8 @@ pass __title__ = 'diskcache' -__version__ = '5.5.2' -__build__ = 0x050502 +__version__ = '5.6.0' +__build__ = 0x050600 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2023 Grant Jenks' From cffbcec2b198e3a296ec294bd43da37fc559645b Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Apr 2023 22:30:26 -0700 Subject: [PATCH 203/211] Fix docs re: JSONDisk --- docs/tutorial.rst | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/tutorial.rst b/docs/tutorial.rst index 69277d3..2eb454d 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -821,30 +821,28 @@ example below uses compressed JSON, available for convenience as .. code-block:: python - import json, zlib - class JSONDisk(diskcache.Disk): def __init__(self, directory, compress_level=1, **kwargs): self.compress_level = compress_level - super(JSONDisk, self).__init__(directory, **kwargs) + super().__init__(directory, **kwargs) def put(self, key): json_bytes = json.dumps(key).encode('utf-8') data = zlib.compress(json_bytes, self.compress_level) - return super(JSONDisk, self).put(data) + return super().put(data) def get(self, key, raw): - data = super(JSONDisk, self).get(key, raw) + data = super().get(key, raw) return json.loads(zlib.decompress(data).decode('utf-8')) - def store(self, value, read): + def store(self, value, read, key=UNKNOWN): if not read: json_bytes = json.dumps(value).encode('utf-8') value = zlib.compress(json_bytes, self.compress_level) - return super(JSONDisk, self).store(value, read) + return super().store(value, read, key=key) def fetch(self, mode, filename, value, read): - data = super(JSONDisk, self).fetch(mode, filename, value, read) + data = super().fetch(mode, filename, value, read) if not read: data = json.loads(zlib.decompress(data).decode('utf-8')) return data From f81160f22af9e8af0e07e179808280188146a020 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Apr 2023 22:46:44 -0700 Subject: [PATCH 204/211] Support pathlib.Path as directory argument --- diskcache/core.py | 1 + diskcache/fanout.py | 1 + tests/test_core.py | 8 ++++++++ tests/test_fanout.py | 8 ++++++++ 4 files changed, 18 insertions(+) diff --git a/diskcache/core.py b/diskcache/core.py index 9f3a597..af65454 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -433,6 +433,7 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): if directory is None: directory = tempfile.mkdtemp(prefix='diskcache-') + directory = str(directory) directory = op.expanduser(directory) directory = op.expandvars(directory) diff --git a/diskcache/fanout.py b/diskcache/fanout.py index 50005fc..9822ee4 100644 --- a/diskcache/fanout.py +++ b/diskcache/fanout.py @@ -30,6 +30,7 @@ def __init__( """ if directory is None: directory = tempfile.mkdtemp(prefix='diskcache-') + directory = str(directory) directory = op.expanduser(directory) directory = op.expandvars(directory) diff --git a/tests/test_core.py b/tests/test_core.py index 356d104..788afef 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -5,6 +5,7 @@ import io import os import os.path as op +import pathlib import pickle import shutil import sqlite3 @@ -37,6 +38,13 @@ def test_init(cache): cache.close() +def test_init_path(cache): + path = pathlib.Path(cache.directory) + other = dc.Cache(path) + other.close() + assert cache.directory == other.directory + + def test_init_disk(): with dc.Cache(disk_pickle_protocol=1, disk_min_file_size=2**20) as cache: key = (None, 0, 'abc') diff --git a/tests/test_fanout.py b/tests/test_fanout.py index deea03f..af221b6 100644 --- a/tests/test_fanout.py +++ b/tests/test_fanout.py @@ -5,6 +5,7 @@ import io import os import os.path as op +import pathlib import pickle import shutil import subprocess as sp @@ -44,6 +45,13 @@ def test_init(cache): cache.check() +def test_init_path(cache): + path = pathlib.Path(cache.directory) + other = dc.FanoutCache(path) + other.close() + assert cache.directory == other.directory + + def test_set_get_delete(cache): for value in range(100): cache.set(value, value) From 4d3068625a3edcd2f5a1f6f104ef621f1f7ea395 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Mon, 17 Apr 2023 22:47:22 -0700 Subject: [PATCH 205/211] Bump version to 5.6.1 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 8647b9a..1931a0d 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -61,8 +61,8 @@ pass __title__ = 'diskcache' -__version__ = '5.6.0' -__build__ = 0x050600 +__version__ = '5.6.1' +__build__ = 0x050601 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2023 Grant Jenks' From 17a5f42facc312dae6e98b7b53345e2ed02be21d Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 30 Aug 2023 22:56:38 -0700 Subject: [PATCH 206/211] Bug fix: Fix peek when value is so large that a file is used (#288) Error caused by copy/paste from pull(). --- diskcache/core.py | 3 --- tests/test_deque.py | 10 ++++++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/diskcache/core.py b/diskcache/core.py index af65454..c7c8486 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -1703,9 +1703,6 @@ def peek( except IOError: # Key was deleted before we could retrieve result. continue - finally: - if name is not None: - self._disk.remove(name) break if expire_time and tag: diff --git a/tests/test_deque.py b/tests/test_deque.py index 71c69e2..f997a86 100644 --- a/tests/test_deque.py +++ b/tests/test_deque.py @@ -302,3 +302,13 @@ def test_rotate_indexerror_negative(deque): with mock.patch.object(deque, '_cache', cache): deque.rotate(-1) + + +def test_peek(deque): + value = b'x' * 100_000 + deque.append(value) + assert len(deque) == 1 + assert deque.peek() == value + assert len(deque) == 1 + assert deque.peek() == value + assert len(deque) == 1 From 63a5f6068b77fe9c02c8f310758fa1f05ae1ae04 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 30 Aug 2023 22:58:11 -0700 Subject: [PATCH 207/211] Bump version to 5.6.2 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 1931a0d..719640f 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -61,8 +61,8 @@ pass __title__ = 'diskcache' -__version__ = '5.6.1' -__build__ = 0x050601 +__version__ = '5.6.2' +__build__ = 0x050602 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2023 Grant Jenks' From 23d10dce8f4be9c00df4786d508964b3b7d72b27 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 30 Aug 2023 23:09:54 -0700 Subject: [PATCH 208/211] Update release.yml to use pypa/gh-action-pypi-publish --- .github/workflows/release.yml | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index efe73c6..33b3a8f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,35 +9,22 @@ jobs: upload: runs-on: ubuntu-latest + permissions: + id-token: write steps: - uses: actions/checkout@v3 - - name: Install libmemcached-dev - run: | - sudo apt-get update - sudo apt-get install libmemcached-dev - - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.11' - - name: Install dependencies - run: | - pip install --upgrade pip - pip install -r requirements-dev.txt - - - name: Create source dist - run: python setup.py sdist + - name: Install build + run: pip install build - - name: Create wheel dist - run: python setup.py bdist_wheel + - name: Create build + run: python -m build - - name: Upload with twine - env: - TWINE_USERNAME: ${{ secrets.TWINE_USERNAME }} - TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} - run: | - ls -l dist/* - twine upload dist/* + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 From 323787f507a6456c56cce213156a78b17073fe00 Mon Sep 17 00:00:00 2001 From: Grant Jenks Date: Wed, 30 Aug 2023 23:10:27 -0700 Subject: [PATCH 209/211] Bump version to 5.6.3 --- diskcache/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/diskcache/__init__.py b/diskcache/__init__.py index 719640f..7757d66 100644 --- a/diskcache/__init__.py +++ b/diskcache/__init__.py @@ -61,8 +61,8 @@ pass __title__ = 'diskcache' -__version__ = '5.6.2' -__build__ = 0x050602 +__version__ = '5.6.3' +__build__ = 0x050603 __author__ = 'Grant Jenks' __license__ = 'Apache 2.0' __copyright__ = 'Copyright 2016-2023 Grant Jenks' From 9cd3816333fa34cb30d6cc2a7f227b6b1cdb793c Mon Sep 17 00:00:00 2001 From: ddorian Date: Tue, 27 Feb 2024 00:30:46 +0100 Subject: [PATCH 210/211] Change `Cache_expire_time` to a partial index because we don't need to query rows efficiently `where expire_time IS NULL` (#305) --- diskcache/core.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/diskcache/core.py b/diskcache/core.py index c7c8486..ad9ad4c 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -531,7 +531,7 @@ def __init__(self, directory=None, timeout=60, disk=Disk, **settings): sql( 'CREATE INDEX IF NOT EXISTS Cache_expire_time ON' - ' Cache (expire_time)' + ' Cache (expire_time) WHERE expire_time IS NOT NULL' ) query = EVICTION_POLICY[self.eviction_policy]['init'] From ebfa37cd99d7ef716ec452ad8af4b4276a8e2233 Mon Sep 17 00:00:00 2001 From: ddorian Date: Sun, 3 Mar 2024 02:19:29 +0100 Subject: [PATCH 211/211] Change `Cache_tag_rowid` to a partial index because we don't need to query rows efficiently `where tag IS NULL` (#307) * Change `Cache_tag_rowid` to a partial index because we don't need to query rows efficiently `where tag IS NULL` * Fix formatting --- diskcache/core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/diskcache/core.py b/diskcache/core.py index ad9ad4c..7a3d23b 100644 --- a/diskcache/core.py +++ b/diskcache/core.py @@ -2028,7 +2028,10 @@ def create_tag_index(self): """ sql = self._sql - sql('CREATE INDEX IF NOT EXISTS Cache_tag_rowid ON Cache(tag, rowid)') + sql( + 'CREATE INDEX IF NOT EXISTS Cache_tag_rowid ON Cache(tag, rowid) ' + 'WHERE tag IS NOT NULL' + ) self.reset('tag_index', 1) def drop_tag_index(self):