Skip to content

Commit c121b98

Browse files
committed
wrapped proxy_bypass() with cache lookup
Used to alleviate long gethostbyaddr calls Made new TimedCache and decorator to wrap a function with a cache * Entries looked up older than a minute (default amount) are evicted. * When full, evicts the oldest entry
1 parent d6f4818 commit c121b98

File tree

4 files changed

+175
-3
lines changed

4 files changed

+175
-3
lines changed

AUTHORS.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,4 +178,5 @@ Patches and Suggestions
178178
- Moinuddin Quadri <[email protected]> (`@moin18 <https://github.com/moin18>`_)
179179
- Matt Kohl (`@mattkohl <https://github.com/mattkohl>`_)
180180
- Jonathan Vanasco (`@jvanasco <https://github.com/jvanasco>`_)
181+
- David Fontenot (`@davidfontenot <https://github.com/davidfontenot>`_)
181182

requests/structures.py

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,12 @@
88
"""
99

1010
import collections
11+
import time
1112

1213
from .compat import OrderedDict
1314

15+
current_time = getattr(time, 'monotonic', time.time)
16+
1417

1518
class CaseInsensitiveDict(collections.MutableMapping):
1619
"""A case-insensitive ``dict``-like object.
@@ -103,3 +106,89 @@ def __getitem__(self, key):
103106

104107
def get(self, key, default=None):
105108
return self.__dict__.get(key, default)
109+
110+
111+
class TimedCacheManaged(object):
112+
"""
113+
Wrap a function call in a timed cache
114+
"""
115+
def __init__(self, fnc):
116+
self.fnc = fnc
117+
self.cache = TimedCache()
118+
119+
def __call__(self, *args, **kwargs):
120+
key = args[0]
121+
found = None
122+
try:
123+
found = self.cache[key]
124+
except KeyError:
125+
found = self.fnc(key, **kwargs)
126+
self.cache[key] = found
127+
128+
return found
129+
130+
131+
class TimedCache(collections.MutableMapping):
132+
"""
133+
Evicts entries after expiration_secs. If none are expired and maxlen is hit,
134+
will evict the oldest cached entry
135+
"""
136+
def __init__(self, maxlen=32, expiration_secs=60):
137+
"""
138+
:param maxlen: most number of entries to hold on to
139+
:param expiration_secs: the number of seconds to hold on
140+
to entries
141+
"""
142+
self.maxlen = maxlen
143+
self.expiration_secs = expiration_secs
144+
self._dict = OrderedDict()
145+
146+
def __repr__(self):
147+
return '<TimedCache maxlen:%d len:%d expiration_secs:%d>' % \
148+
(self.maxlen, len(self._dict), self.expiration_secs)
149+
150+
def __iter__(self):
151+
return map(lambda kv: (kv[0], kv[1][1]), self._dict.items()).__iter__()
152+
153+
def __delitem__(self, item):
154+
return self._dict.__delitem__(item)
155+
156+
def __getitem__(self, key):
157+
"""
158+
Look up an item in the cache. If the item
159+
has already expired, it will be invalidated and not returned
160+
161+
:param key: which entry to look up
162+
:return: the value in the cache, or None
163+
"""
164+
occurred, value = self._dict[key]
165+
now = int(current_time())
166+
167+
if now - occurred > self.expiration_secs:
168+
del self._dict[key]
169+
raise KeyError
170+
else:
171+
return value
172+
173+
def __setitem__(self, key, value):
174+
"""
175+
Locates the value at lookup key, if cache is full, will evict the
176+
oldest entry
177+
178+
:param key: the key to search the cache for
179+
:param value: the value to be added to the cache
180+
"""
181+
now = int(current_time())
182+
183+
while len(self._dict) >= self.maxlen:
184+
self._dict.popitem(last=False)
185+
186+
return self._dict.__setitem__(key, (now, value))
187+
188+
def __len__(self):
189+
""":return: the length of the cache"""
190+
return len(self._dict)
191+
192+
def clear(self):
193+
"""Clears the cache"""
194+
return self._dict.clear()

requests/utils.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
quote, urlparse, bytes, str, OrderedDict, unquote, getproxies,
2929
proxy_bypass, urlunparse, basestring, integer_types)
3030
from .cookies import RequestsCookieJar, cookiejar_from_dict
31-
from .structures import CaseInsensitiveDict
31+
from .structures import CaseInsensitiveDict, TimedCache, TimedCacheManaged
3232
from .exceptions import (
3333
InvalidURL, InvalidHeader, FileModeWarning, UnrewindableBodyError)
3434

@@ -579,6 +579,16 @@ def set_environ(env_name, value):
579579
os.environ[env_name] = old_value
580580

581581

582+
@TimedCacheManaged
583+
def _proxy_bypass_cached(netloc):
584+
"""
585+
Looks for netloc in the cache, if not found, will call proxy_bypass
586+
for the netloc and store its result in the cache
587+
588+
:rtype: bool
589+
"""
590+
return proxy_bypass(netloc)
591+
582592
def should_bypass_proxies(url, no_proxy):
583593
"""
584594
Returns whether we should bypass proxies or not.
@@ -626,7 +636,7 @@ def should_bypass_proxies(url, no_proxy):
626636
# legitimate problems.
627637
with set_environ('no_proxy', no_proxy_arg):
628638
try:
629-
bypass = proxy_bypass(netloc)
639+
bypass = _proxy_bypass_cached(netloc)
630640
except (TypeError, socket.gaierror):
631641
bypass = False
632642

tests/test_structures.py

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import pytest
44

5-
from requests.structures import CaseInsensitiveDict, LookupDict
5+
from requests.structures import CaseInsensitiveDict, LookupDict, TimedCache, TimedCacheManaged
66

77

88
class TestCaseInsensitiveDict:
@@ -74,3 +74,75 @@ def test_getitem(self, key, value):
7474
@get_item_parameters
7575
def test_get(self, key, value):
7676
assert self.lookup_dict.get(key) == value
77+
78+
79+
class TestTimedCache(object):
80+
@pytest.fixture(autouse=True)
81+
def setup(self):
82+
self.any_value = 'some value'
83+
self.expiration_secs = 60
84+
self.cache = TimedCache(expiration_secs=self.expiration_secs)
85+
yield
86+
self.cache.clear()
87+
88+
def test_get(self):
89+
self.cache['a'] = self.any_value
90+
assert self.cache['a'] is self.any_value
91+
92+
def test_repr(self):
93+
repr = str(self.cache)
94+
assert repr == '<TimedCache maxlen:32 len:0 expiration_secs:60>'
95+
96+
def test_get_expired_item(self, mocker):
97+
self.cache = TimedCache(maxlen=1, expiration_secs=self.expiration_secs)
98+
99+
mocker.patch('requests.structures.current_time', lambda: 0)
100+
self.cache['a'] = self.any_value
101+
mocker.patch('requests.structures.current_time', lambda: self.expiration_secs + 1)
102+
assert self.cache.get('a') is None
103+
104+
def test_evict_first_entry_when_full(self, mocker):
105+
self.cache = TimedCache(maxlen=2, expiration_secs=2)
106+
mocker.patch('requests.structures.current_time', lambda: 0)
107+
self.cache['a'] = self.any_value
108+
mocker.patch('requests.structures.current_time', lambda: 1)
109+
self.cache['b'] = self.any_value
110+
mocker.patch('requests.structures.current_time', lambda: 3)
111+
self.cache['c'] = self.any_value
112+
assert len(self.cache) is 2
113+
with pytest.raises(KeyError, message='Expected key not found'):
114+
self.cache['a']
115+
assert self.cache['b'] is self.any_value
116+
assert self.cache['c'] is self.any_value
117+
118+
def test_delete_item_removes_item(self):
119+
self.cache['a'] = self.any_value
120+
del self.cache['a']
121+
with pytest.raises(KeyError, message='Expected key not found'):
122+
self.cache['a']
123+
124+
def test_iterating_hides_timestamps(self):
125+
self.cache['a'] = 1
126+
self.cache['b'] = 2
127+
expected = [('a', 1), ('b', 2)]
128+
actual = [(key, val) for key, val in self.cache]
129+
assert expected == actual
130+
131+
132+
class TestTimedCacheManagedDecorator(object):
133+
def test_caches_repeated_calls(self, mocker):
134+
mocker.patch('requests.structures.current_time', lambda: 0)
135+
136+
nonlocals = {'value': 0}
137+
138+
@TimedCacheManaged
139+
def some_method(x):
140+
nonlocals['value'] = nonlocals['value'] + x
141+
return nonlocals['value']
142+
143+
first_result = some_method(1)
144+
assert first_result is 1
145+
second_result = some_method(1)
146+
assert second_result is 1
147+
third_result = some_method(2)
148+
assert third_result is 3

0 commit comments

Comments
 (0)