Files
requests3/requests/structures.py
T
Ian Cordasco 8e6e47af43 Update implementation of TimedCache object
There were some odd decisions made about the implementation of some of
the required methods for MuttableMapping in the TimedCache object. This
cleans those up and makes the implementation, ever so slightly, easier
to read.

See also #3885
2017-03-04 09:03:03 -06:00

195 lines
5.3 KiB
Python

# -*- coding: utf-8 -*-
"""
requests.structures
~~~~~~~~~~~~~~~~~~~
Data structures that power Requests.
"""
import collections
import time
from .compat import OrderedDict
current_time = getattr(time, 'monotonic', time.time)
class CaseInsensitiveDict(collections.MutableMapping):
"""A case-insensitive ``dict``-like object.
Implements all methods and operations of
``collections.MutableMapping`` as well as dict's ``copy``. Also
provides ``lower_items``.
All keys are expected to be strings. The structure remembers the
case of the last key to be set, and ``iter(instance)``,
``keys()``, ``items()``, ``iterkeys()``, and ``iteritems()``
will contain case-sensitive keys. However, querying and contains
testing is case insensitive::
cid = CaseInsensitiveDict()
cid['Accept'] = 'application/json'
cid['aCCEPT'] == 'application/json' # True
list(cid) == ['Accept'] # True
For example, ``headers['content-encoding']`` will return the
value of a ``'Content-Encoding'`` response header, regardless
of how the header name was originally stored.
If the constructor, ``.update``, or equality comparison
operations are given keys that have equal ``.lower()``s, the
behavior is undefined.
"""
def __init__(self, data=None, **kwargs):
self._store = OrderedDict()
if data is None:
data = {}
self.update(data, **kwargs)
def __setitem__(self, key, value):
# Use the lowercased key for lookups, but store the actual
# key alongside the value.
self._store[key.lower()] = (key, value)
def __getitem__(self, key):
return self._store[key.lower()][1]
def __delitem__(self, key):
del self._store[key.lower()]
def __iter__(self):
return (casedkey for casedkey, mappedvalue in self._store.values())
def __len__(self):
return len(self._store)
def lower_items(self):
"""Like iteritems(), but with all lowercase keys."""
return (
(lowerkey, keyval[1])
for (lowerkey, keyval)
in self._store.items()
)
def __eq__(self, other):
if isinstance(other, collections.Mapping):
other = CaseInsensitiveDict(other)
else:
return NotImplemented
# Compare insensitively
return dict(self.lower_items()) == dict(other.lower_items())
# Copy is required
def copy(self):
return CaseInsensitiveDict(self._store.values())
def __repr__(self):
return str(dict(self.items()))
class LookupDict(dict):
"""Dictionary lookup object."""
def __init__(self, name=None):
self.name = name
super(LookupDict, self).__init__()
def __repr__(self):
return '<lookup \'%s\'>' % (self.name)
def __getitem__(self, key):
# We allow fall-through here, so values default to None
return self.__dict__.get(key, None)
def get(self, key, default=None):
return self.__dict__.get(key, default)
class TimedCacheManaged(object):
"""
Wrap a function call in a timed cache
"""
def __init__(self, fnc):
self.fnc = fnc
self.cache = TimedCache()
def __call__(self, *args, **kwargs):
key = args[0]
found = None
try:
found = self.cache[key]
except KeyError:
found = self.fnc(key, **kwargs)
self.cache[key] = found
return found
class TimedCache(collections.MutableMapping):
"""
Evicts entries after expiration_secs. If none are expired and maxlen is hit,
will evict the oldest cached entry
"""
def __init__(self, maxlen=32, expiration_secs=60):
"""
:param maxlen: most number of entries to hold on to
:param expiration_secs: the number of seconds to hold on
to entries
"""
self.maxlen = maxlen
self.expiration_secs = expiration_secs
self._dict = OrderedDict()
def __repr__(self):
return '<TimedCache maxlen:%d len:%d expiration_secs:%d>' % \
(self.maxlen, len(self._dict), self.expiration_secs)
def __iter__(self):
return ((key, value[1]) for key, value in self._dict.items())
def __delitem__(self, item):
del self._dict[item]
def __getitem__(self, key):
"""
Look up an item in the cache. If the item
has already expired, it will be invalidated and not returned
:param key: which entry to look up
:return: the value in the cache, or None
"""
occurred, value = self._dict[key]
now = int(current_time())
if now - occurred > self.expiration_secs:
del self._dict[key]
raise KeyError(key)
else:
return value
def __setitem__(self, key, value):
"""
Locates the value at lookup key, if cache is full, will evict the
oldest entry
:param key: the key to search the cache for
:param value: the value to be added to the cache
"""
now = int(current_time())
while len(self._dict) >= self.maxlen:
self._dict.popitem(last=False)
self._dict[key] = (now, value)
def __len__(self):
""":return: the length of the cache"""
return len(self._dict)
def clear(self):
"""Clears the cache"""
return self._dict.clear()