From 682981700b4b8f0d96438f172ee68c8b80c8b8ae Mon Sep 17 00:00:00 2001 From: Patrick Hayes Date: Wed, 20 Apr 2016 14:45:55 -0700 Subject: [PATCH 01/16] Allow custom serializer objects --- beaker/session.py | 36 +++++++++++++++++++++++++++--------- beaker/util.py | 28 +++++++++++++++++++++++----- tests/test_cookie_only.py | 19 +++++++++++++++++++ 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/beaker/session.py b/beaker/session.py index cbd76944..a9485dd9 100644 --- a/beaker/session.py +++ b/beaker/session.py @@ -105,14 +105,15 @@ class Session(dict): For security reason this is 128bits be default. If you want to keep backward compatibility with sessions generated before 1.8.0 set this to 48. + :param serializer: Custom serializer object, with ``"loads"`` and ``"dumps"`` methods. """ def __init__(self, request, id=None, invalidate_corrupt=False, use_cookies=True, type=None, data_dir=None, key='beaker.session.id', timeout=None, cookie_expires=True, - cookie_domain=None, cookie_path='/', data_serializer='pickle', secret=None, + cookie_domain=None, cookie_path='/', data_serializer=None, secret=None, secure=False, namespace_class=None, httponly=False, encrypt_key=None, validate_key=None, encrypt_nonce_bits=DEFAULT_NONCE_BITS, - **namespace_args): + serializer=None, **namespace_args): if not type: if data_dir: self.type = 'file' @@ -132,7 +133,8 @@ def __init__(self, request, id=None, invalidate_corrupt=False, self.timeout = timeout self.use_cookies = use_cookies self.cookie_expires = cookie_expires - self.data_serializer = data_serializer + + self._set_serializer(data_serializer, serializer) # Default cookie domain/path self._domain = cookie_domain @@ -178,6 +180,20 @@ def __init__(self, request, id=None, invalidate_corrupt=False, else: raise + def _set_serializer(self, data_serializer, serializer): + self.data_serializer = data_serializer + if data_serializer is None and serializer is None: + self.data_serializer = 'pickle' + + if self.data_serializer == 'json': + self.serializer = util.JsonSerializer() + elif self.data_serializer == 'pickle': + self.serializer = util.PickleSerializer() + elif serializer is not None: + self.serializer = serializer + else: + raise BeakerException('Invalid value for data_serializer: %s' % data_serializer) + def has_key(self, name): return name in self @@ -269,10 +285,10 @@ def _encrypt_data(self, session_data=None): nonce = b64encode(os.urandom(nonce_len))[:nonce_b64len] encrypt_key = crypto.generateCryptoKeys(self.encrypt_key, self.validate_key + nonce, 1) - data = util.serialize(session_data, self.data_serializer) + data = self.serializer.dumps(session_data) return nonce + b64encode(crypto.aesEncrypt(data, encrypt_key)) else: - data = util.serialize(session_data, self.data_serializer) + data = self.serializer.dumps(session_data) return b64encode(data) def _decrypt_data(self, session_data): @@ -298,7 +314,7 @@ def _decrypt_data(self, session_data): data = b64decode(session_data) try: - return util.deserialize(data, self.data_serializer) + return self.serializer.loads(data) except: if self.invalidate_corrupt: return None @@ -498,13 +514,14 @@ class CookieSession(Session): :param encrypt_key: The key to use for the local session encryption, if not provided the session will not be encrypted. :param validate_key: The key used to sign the local encrypted session + :param serializer: Custom serializer object, with ``"loads"`` and ``"dumps"`` methods. """ def __init__(self, request, key='beaker.session.id', timeout=None, cookie_expires=True, cookie_domain=None, cookie_path='/', encrypt_key=None, validate_key=None, secure=False, - httponly=False, data_serializer='pickle', - encrypt_nonce_bits=DEFAULT_NONCE_BITS, **kwargs): + httponly=False, data_serializer=None, + encrypt_nonce_bits=DEFAULT_NONCE_BITS, serializer=None, **kwargs): if not crypto.has_aes and encrypt_key: raise InvalidCryptoBackendError("No AES library is installed, can't generate " @@ -522,7 +539,8 @@ def __init__(self, request, key='beaker.session.id', timeout=None, self.httponly = httponly self._domain = cookie_domain self._path = cookie_path - self.data_serializer = data_serializer + + self._set_serializer(data_serializer, serializer) try: cookieheader = request['cookie'] diff --git a/beaker/util.py b/beaker/util.py index 35f0441f..fc74a262 100644 --- a/beaker/util.py +++ b/beaker/util.py @@ -442,15 +442,33 @@ def func_namespace(func): return '%s|%s' % (inspect.getsourcefile(func), func.__name__) -def serialize(data, method): - if method == 'json': +class PickleSerializer(object): + def loads(self, data_string): + return pickle.loads(data_string) + + def dumps(self, data): + return pickle.dumps(data, 2) + + +class JsonSerializer(object): + def loads(self, data_string): + return json.loads(zlib.decompress(data_string).decode('utf-8')) + + def dumps(self, data): return zlib.compress(json.dumps(data).encode('utf-8')) + + +def serialize(data, serializer): + if method == 'json': + serializer = JsonSerializer() else: - return pickle.dumps(data, 2) + serializer = PickleSerializer() + return serializer.dumps(data) def deserialize(data_string, method): if method == 'json': - return json.loads(zlib.decompress(data_string).decode('utf-8')) + serializer = JsonSerializer() else: - return pickle.loads(data_string) + serializer = PickleSerializer() + return serializer.loads(data_string) diff --git a/tests/test_cookie_only.py b/tests/test_cookie_only.py index 53a8b32a..4c254c2f 100644 --- a/tests/test_cookie_only.py +++ b/tests/test_cookie_only.py @@ -1,6 +1,7 @@ import datetime, time import re import os +import json import beaker.session import beaker.util @@ -105,6 +106,24 @@ def test_pickle_serializer(): res = app.get('/') assert 'current value is: 3' in res +def test_custom_serializer(): + serializer = json + options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'serializer': serializer} + app = TestApp(SessionMiddleware(simple_app, **options)) + + res = app.get('/') + assert 'current value is: 1' in res + + res = app.get('/') + cookie = SignedCookie('hoobermas') + session_data = cookie.value_decode(app.cookies['beaker.session.id'])[0] + session_data = b64decode(session_data) + data = serializer.loads(session_data) + assert data['value'] == 2 + + res = app.get('/') + assert 'current value is: 3' in res + def test_expires(): options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'session.cookie_expires': datetime.timedelta(days=1)} From 9994d9425e7e8d154cd59437d7241e644633171a Mon Sep 17 00:00:00 2001 From: Patrick Hayes Date: Wed, 20 Apr 2016 15:46:40 -0700 Subject: [PATCH 02/16] Fix bytes decoding in python 3 --- tests/test_cookie_only.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/test_cookie_only.py b/tests/test_cookie_only.py index 4c254c2f..f78b3df6 100644 --- a/tests/test_cookie_only.py +++ b/tests/test_cookie_only.py @@ -107,7 +107,14 @@ def test_pickle_serializer(): assert 'current value is: 3' in res def test_custom_serializer(): - serializer = json + class CustomSerializer(object): + def loads(self, data_string): + return json.loads(data_string).decode('utf-8') + + def dumps(self, data): + return json.dumps(data_string.encode('utf-8')) + + serializer = CustomSerializer() options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'serializer': serializer} app = TestApp(SessionMiddleware(simple_app, **options)) From edd2093cb33cebfb4c7699560626dbffaae81806 Mon Sep 17 00:00:00 2001 From: Patrick Hayes Date: Wed, 20 Apr 2016 15:51:30 -0700 Subject: [PATCH 03/16] Fix test --- tests/test_cookie_only.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_cookie_only.py b/tests/test_cookie_only.py index f78b3df6..d7b0fc83 100644 --- a/tests/test_cookie_only.py +++ b/tests/test_cookie_only.py @@ -112,7 +112,7 @@ def loads(self, data_string): return json.loads(data_string).decode('utf-8') def dumps(self, data): - return json.dumps(data_string.encode('utf-8')) + return json.dumps(data.encode('utf-8')) serializer = CustomSerializer() options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'serializer': serializer} From 619bdd98a0c79ff0b347b74e4b047fe6a3fcb9a5 Mon Sep 17 00:00:00 2001 From: Patrick Hayes Date: Thu, 21 Apr 2016 11:49:53 -0700 Subject: [PATCH 04/16] reuse data_serializer key --- beaker/session.py | 34 ++++++++++++++++------------------ tests/test_cookie_only.py | 2 +- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/beaker/session.py b/beaker/session.py index a9485dd9..49f2a5b9 100644 --- a/beaker/session.py +++ b/beaker/session.py @@ -94,7 +94,9 @@ class Session(dict): :param cookie_domain: Domain to use for the cookie. :param cookie_path: Path to use for the cookie. :param data_serializer: If ``"json"`` or ``"pickle"`` should be used - to serialize data. By default ``pickle`` is used. + to serialize data. Can also be an object with + ``loads` and ``dumps`` methods. By default + ``"pickle"`` is used. :param secure: Whether or not the cookie should only be sent over SSL. :param httponly: Whether or not the cookie should only be accessible by the browser not by JavaScript. @@ -105,15 +107,14 @@ class Session(dict): For security reason this is 128bits be default. If you want to keep backward compatibility with sessions generated before 1.8.0 set this to 48. - :param serializer: Custom serializer object, with ``"loads"`` and ``"dumps"`` methods. """ def __init__(self, request, id=None, invalidate_corrupt=False, use_cookies=True, type=None, data_dir=None, key='beaker.session.id', timeout=None, cookie_expires=True, - cookie_domain=None, cookie_path='/', data_serializer=None, secret=None, + cookie_domain=None, cookie_path='/', data_serializer='pickle', secret=None, secure=False, namespace_class=None, httponly=False, encrypt_key=None, validate_key=None, encrypt_nonce_bits=DEFAULT_NONCE_BITS, - serializer=None, **namespace_args): + **namespace_args): if not type: if data_dir: self.type = 'file' @@ -134,7 +135,7 @@ def __init__(self, request, id=None, invalidate_corrupt=False, self.use_cookies = use_cookies self.cookie_expires = cookie_expires - self._set_serializer(data_serializer, serializer) + self._set_serializer(data_serializer) # Default cookie domain/path self._domain = cookie_domain @@ -180,19 +181,16 @@ def __init__(self, request, id=None, invalidate_corrupt=False, else: raise - def _set_serializer(self, data_serializer, serializer): + def _set_serializer(self, data_serializer): self.data_serializer = data_serializer - if data_serializer is None and serializer is None: - self.data_serializer = 'pickle' - if self.data_serializer == 'json': self.serializer = util.JsonSerializer() elif self.data_serializer == 'pickle': self.serializer = util.PickleSerializer() - elif serializer is not None: - self.serializer = serializer - else: + elif isinstance(self.data_serializer, basestring): raise BeakerException('Invalid value for data_serializer: %s' % data_serializer) + else: + self.serializer = data_serializer def has_key(self, name): return name in self @@ -507,21 +505,21 @@ class CookieSession(Session): :param cookie_domain: Domain to use for the cookie. :param cookie_path: Path to use for the cookie. :param data_serializer: If ``"json"`` or ``"pickle"`` should be used - to serialize data. By default ``pickle`` is used. + to serialize data. Can also be an object with + ``loads` and ``dumps`` methods. By default + ``"pickle"`` is used. :param secure: Whether or not the cookie should only be sent over SSL. :param httponly: Whether or not the cookie should only be accessible by the browser not by JavaScript. :param encrypt_key: The key to use for the local session encryption, if not provided the session will not be encrypted. :param validate_key: The key used to sign the local encrypted session - :param serializer: Custom serializer object, with ``"loads"`` and ``"dumps"`` methods. - """ def __init__(self, request, key='beaker.session.id', timeout=None, cookie_expires=True, cookie_domain=None, cookie_path='/', encrypt_key=None, validate_key=None, secure=False, - httponly=False, data_serializer=None, - encrypt_nonce_bits=DEFAULT_NONCE_BITS, serializer=None, **kwargs): + httponly=False, data_serializer='pickle', + encrypt_nonce_bits=DEFAULT_NONCE_BITS, **kwargs): if not crypto.has_aes and encrypt_key: raise InvalidCryptoBackendError("No AES library is installed, can't generate " @@ -540,7 +538,7 @@ def __init__(self, request, key='beaker.session.id', timeout=None, self._domain = cookie_domain self._path = cookie_path - self._set_serializer(data_serializer, serializer) + self._set_serializer(data_serializer) try: cookieheader = request['cookie'] diff --git a/tests/test_cookie_only.py b/tests/test_cookie_only.py index d7b0fc83..c777bb71 100644 --- a/tests/test_cookie_only.py +++ b/tests/test_cookie_only.py @@ -115,7 +115,7 @@ def dumps(self, data): return json.dumps(data.encode('utf-8')) serializer = CustomSerializer() - options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'serializer': serializer} + options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'data_serializer': serializer} app = TestApp(SessionMiddleware(simple_app, **options)) res = app.get('/') From 9af3b569e47752bd440eb930b26293d641d5996a Mon Sep 17 00:00:00 2001 From: Patrick Hayes Date: Thu, 21 Apr 2016 12:21:46 -0700 Subject: [PATCH 05/16] Ensure custom serializer was used --- tests/test_cookie_only.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/test_cookie_only.py b/tests/test_cookie_only.py index c777bb71..c0c00993 100644 --- a/tests/test_cookie_only.py +++ b/tests/test_cookie_only.py @@ -107,12 +107,15 @@ def test_pickle_serializer(): assert 'current value is: 3' in res def test_custom_serializer(): + was_used = [False, False] class CustomSerializer(object): def loads(self, data_string): - return json.loads(data_string).decode('utf-8') + was_used[0] = True + return json.loads(data_string.decode('utf-8')) def dumps(self, data): - return json.dumps(data.encode('utf-8')) + was_used[1] = True + return json.dumps(data).encode('utf-8') serializer = CustomSerializer() options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'data_serializer': serializer} @@ -131,6 +134,8 @@ def dumps(self, data): res = app.get('/') assert 'current value is: 3' in res + assert all(was_used) + def test_expires(): options = {'session.validate_key':'hoobermas', 'session.type':'cookie', 'session.cookie_expires': datetime.timedelta(days=1)} From b86acc57bcbd03c7f8ca975e9fc5ebba9521bed3 Mon Sep 17 00:00:00 2001 From: Patrick Hayes Date: Thu, 21 Apr 2016 12:43:19 -0700 Subject: [PATCH 06/16] Fix for python 3 --- beaker/session.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beaker/session.py b/beaker/session.py index 49f2a5b9..9899723c 100644 --- a/beaker/session.py +++ b/beaker/session.py @@ -1,4 +1,4 @@ -from ._compat import PY2, pickle, http_cookies, unicode_text, b64encode, b64decode +from ._compat import PY2, pickle, http_cookies, unicode_text, b64encode, b64decode, string_type import os import time @@ -187,7 +187,7 @@ def _set_serializer(self, data_serializer): self.serializer = util.JsonSerializer() elif self.data_serializer == 'pickle': self.serializer = util.PickleSerializer() - elif isinstance(self.data_serializer, basestring): + elif isinstance(self.data_serializer, string_type): raise BeakerException('Invalid value for data_serializer: %s' % data_serializer) else: self.serializer = data_serializer From a956724df00173947f246fb3cdc5fb03782a3e46 Mon Sep 17 00:00:00 2001 From: Dan Benamy Date: Thu, 19 May 2016 02:08:00 -0400 Subject: [PATCH 07/16] Add save_accessed_time option --- CHANGELOG | 8 +++ beaker/docs/sessions.rst | 6 +- beaker/middleware.py | 5 +- beaker/session.py | 79 +++++++++++++++++--------- beaker/util.py | 4 ++ tests/test_session.py | 119 ++++++++++++++++++++++++++++++++++++++- 6 files changed, 186 insertions(+), 35 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 84c1310f..49901ce1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,11 @@ +Release $next +============= + +* Sessions have a new option save_accessed_time which defaults to true for + backwards compatibility. Set to false to tell beaker not to add _accessed_time + to the session and the last_accessed attribute. This lets you avoid session + writes for requests that don't otherwise update the session. + Release 1.8.0 (2016-01-26) ========================== diff --git a/beaker/docs/sessions.rst b/beaker/docs/sessions.rst index 3ec0fe97..db4d2d33 100644 --- a/beaker/docs/sessions.rst +++ b/beaker/docs/sessions.rst @@ -84,11 +84,13 @@ application. * id - Unique 40 char SHA-generated session ID * last_accessed - The last time the session was accessed before the current - access, will be None if the session was just made + access, will be None if the session was just made; only set if the + save_accessed_time setting is true (the default) There's several special session keys populated as well: -* _accessed_time - Current accessed time of the session, when it was loaded +* _accessed_time - Current accessed time of the session, when it was loaded; + only set if the save_accessed_time setting is true (the default) * _creation_time - When the session was created diff --git a/beaker/middleware.py b/beaker/middleware.py index ecdd567a..319f2b76 100644 --- a/beaker/middleware.py +++ b/beaker/middleware.py @@ -106,8 +106,9 @@ def __init__(self, wrap_app, config=None, environ_key='beaker.session', # Load up the default params self.options = dict(invalidate_corrupt=True, type=None, - data_dir=None, key='beaker.session.id', - timeout=None, secret=None, log_file=None) + data_dir=None, key='beaker.session.id', + timeout=None, save_accessed_time=True, secret=None, + log_file=None) # Pull out any config args meant for beaker session. if there are any for dct in [config, kwargs]: diff --git a/beaker/session.py b/beaker/session.py index cbd76944..eee2ef7f 100644 --- a/beaker/session.py +++ b/beaker/session.py @@ -88,8 +88,11 @@ class Session(dict): :param key: The name the cookie should be set to. :param timeout: How long session data is considered valid. This is used regardless of the cookie being present or not to determine - whether session data is still valid. - :type timeout: int + whether session data is still valid. Can be set to None to + disable session time out. + :type timeout: int or None + :param save_accessed_time: Whether beaker should save the session's access + time. Defaults to true. :param cookie_expires: Expiration date for cookie :param cookie_domain: Domain to use for the cookie. :param cookie_path: Path to use for the cookie. @@ -108,8 +111,9 @@ class Session(dict): """ def __init__(self, request, id=None, invalidate_corrupt=False, use_cookies=True, type=None, data_dir=None, - key='beaker.session.id', timeout=None, cookie_expires=True, - cookie_domain=None, cookie_path='/', data_serializer='pickle', secret=None, + key='beaker.session.id', timeout=None, save_accessed_time=True, + cookie_expires=True, cookie_domain=None, cookie_path='/', + data_serializer='pickle', secret=None, secure=False, namespace_class=None, httponly=False, encrypt_key=None, validate_key=None, encrypt_nonce_bits=DEFAULT_NONCE_BITS, **namespace_args): @@ -129,7 +133,10 @@ def __init__(self, request, id=None, invalidate_corrupt=False, self.data_dir = data_dir self.key = key + if timeout and not save_accessed_time: + raise BeakerException("timeout requires save_accessed_time") self.timeout = timeout + self.save_atime = save_accessed_time self.use_cookies = use_cookies self.cookie_expires = cookie_expires self.data_serializer = data_serializer @@ -164,7 +171,9 @@ def __init__(self, request, id=None, invalidate_corrupt=False, self.is_new = self.id is None if self.is_new: self._create_id() - self['_accessed_time'] = self['_creation_time'] = time.time() + self['_creation_time'] = time.time() + if self.save_atime: + self['_accessed_time'] = self['_creation_time'] else: try: self.load() @@ -350,37 +359,41 @@ def load(self): # present if session_data is None: session_data = { - '_creation_time': now, - '_accessed_time': now + '_creation_time': now } + if self.save_atime: + session_data['_accessed_time'] = now self.is_new = True except (KeyError, TypeError): session_data = { - '_creation_time': now, - '_accessed_time': now + '_creation_time': now } + if self.save_atime: + session_data['_accessed_time'] = now self.is_new = True if session_data is None or len(session_data) == 0: session_data = { - '_creation_time': now, - '_accessed_time': now + '_creation_time': now } + if self.save_atime: + session_data['_accessed_time'] = now self.is_new = True if self.timeout is not None and \ now - session_data['_accessed_time'] > self.timeout: timed_out = True else: - # Properly set the last_accessed time, which is different - # than the *currently* _accessed_time - if self.is_new or '_accessed_time' not in session_data: - self.last_accessed = None - else: - self.last_accessed = session_data['_accessed_time'] + if self.save_atime: + # Properly set the last_accessed time, which is different + # than the *currently* _accessed_time + if self.is_new or '_accessed_time' not in session_data: + self.last_accessed = None + else: + self.last_accessed = session_data['_accessed_time'] - # Update the current _accessed_time - session_data['_accessed_time'] = now + # Update the current _accessed_time + session_data['_accessed_time'] = now # Set the path if applicable if '_path' in session_data: @@ -501,8 +514,8 @@ class CookieSession(Session): """ def __init__(self, request, key='beaker.session.id', timeout=None, - cookie_expires=True, cookie_domain=None, cookie_path='/', - encrypt_key=None, validate_key=None, secure=False, + save_accessed_time=True, cookie_expires=True, cookie_domain=None, + cookie_path='/', encrypt_key=None, validate_key=None, secure=False, httponly=False, data_serializer='pickle', encrypt_nonce_bits=DEFAULT_NONCE_BITS, **kwargs): @@ -513,6 +526,7 @@ def __init__(self, request, key='beaker.session.id', timeout=None, self.request = request self.key = key self.timeout = timeout + self.save_atime = save_accessed_time self.cookie_expires = cookie_expires self.encrypt_key = encrypt_key self.validate_key = validate_key @@ -532,6 +546,8 @@ def __init__(self, request, key='beaker.session.id', timeout=None, if validate_key is None: raise BeakerException("No validate_key specified for Cookie only " "Session.") + if timeout and not save_accessed_time: + raise BeakerException("timeout requires save_accessed_time") try: self.cookie = SignedCookie(validate_key, input=cookieheader) @@ -604,7 +620,8 @@ def _create_cookie(self): self['_creation_time'] = time.time() if '_id' not in self: self['_id'] = _session_id() - self['_accessed_time'] = time.time() + if self.save_atime: + self['_accessed_time'] = time.time() val = self._encrypt_data() if len(val) > 4064: @@ -722,21 +739,27 @@ def delete(self): def persist(self): """Persist the session to the storage - If its set to autosave, then the entire session will be saved - regardless of if save() has been called. Otherwise, just the - accessed time will be updated if save() was not called, or - the session will be saved if save() was called. + Always saves the whole session if save() or delete() have been called. + If they haven't: + - If autosave is set to true, saves the the entire session regardless. + - If save_accessed_time is set to true or unset, only saves the updated + access time. + - If save_accessed_time is set to false, doesn't save anything. """ if self.__dict__['_params'].get('auto'): self._session().save() - else: - if self.__dict__.get('_dirty'): + elif self.__dict__['_params'].get('save_accessed_time', True): + if self.dirty(): self._session().save() else: self._session().save(accessed_only=True) + else: # save_accessed_time is false + if self.dirty(): + self._session().save() def dirty(self): + """Returns True if save() or delete() have been called""" return self.__dict__.get('_dirty', False) def accessed(self): diff --git a/beaker/util.py b/beaker/util.py index 35f0441f..cb94c0b7 100644 --- a/beaker/util.py +++ b/beaker/util.py @@ -304,6 +304,8 @@ def coerce_session_params(params): ('secure', (bool, NoneType), "Session secure must be a boolean."), ('httponly', (bool, NoneType), "Session httponly must be a boolean."), ('timeout', (int, NoneType), "Session timeout must be an integer."), + ('save_accessed_time', (bool, NoneType), + "Session save_accessed_time must be a boolean (defaults to true)."), ('auto', (bool, NoneType), "Session is created if accessed."), ('webtest_varname', (str, NoneType), "Session varname must be a string."), ('data_serializer', (str,), "data_serializer must be a string.") @@ -313,6 +315,8 @@ def coerce_session_params(params): if cookie_expires and isinstance(cookie_expires, int) and \ not isinstance(cookie_expires, bool): opts['cookie_expires'] = timedelta(seconds=cookie_expires) + if opts['timeout'] is not None and not ops['save_accessed_time']: + raise Exception("save_accessed_time must be true to use timeout") return opts diff --git a/tests/test_session.py b/tests/test_session.py index 926dabbe..16a12eba 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -3,13 +3,16 @@ import sys import time +import unittest import warnings from nose import SkipTest +from beaker.container import MemoryNamespaceManager from beaker.crypto import has_aes -from beaker.session import Session -from beaker import util +from beaker.exceptions import BeakerException +from beaker.session import Session, SessionObject +from beaker.util import assert_raises def get_session(**kwargs): @@ -189,6 +192,17 @@ def test_timeout(): assert u_('Deutchland') not in session +def test_timeout_requires_accessed_time(): + """Test that it doesn't allow setting save_accessed_time to True with + timeout enabled + """ + get_session(timeout=None, save_accessed_time=True) # is ok + assert_raises(BeakerException, + get_session, + timeout=2, + save_accessed_time=False) + + def test_cookies_enabled(): """ Test if cookies are sent out properly when ``use_cookies`` @@ -315,7 +329,7 @@ def test_invalidate_corrupt(): f.write("crap") f.close() - util.assert_raises( + assert_raises( pickle.UnpicklingError, get_session, use_cookies=False, type='file', @@ -326,3 +340,102 @@ def test_invalidate_corrupt(): invalidate_corrupt=True, data_dir='./cache', id=session.id) assert "foo" not in dict(session) + + +def test_saves_accessed_time(): + session = get_session(save_accessed_time=True) + session.save() + atime1 = session['_accessed_time'] + + session2 = get_session(id=session.id, save_accessed_time=True) + session2.save() + assert session2['_accessed_time'] > atime1 + assert session2.last_accessed == atime1 + + +def test_doesnt_save_accessed_time(): + session = get_session(save_accessed_time=False) + session.save() + assert '_accessed_time' not in session + session2 = get_session(id=session.id, save_accessed_time=False) + session2.save() + assert '_accessed_time' not in session2 + assert not hasattr(session2, 'last_accessed') + + +def test_saves_creation_time_even_witout_accessed_time(): + session = get_session(save_accessed_time=False) + session.save() + assert '_creation_time' in session + + +class TestSessionObject(unittest.TestCase): + def setUp(self): + # San check that we are in fact using the memory backend... + assert get_session().namespace_class == MemoryNamespaceManager + # so we can be sure we're clearing the right state. + MemoryNamespaceManager.namespaces.clear() + + def test_no_autosave_saves_atime_without_save(self): + so = SessionObject({}, auto=False) + so['foo'] = 'bar' + so.persist() + session = get_session(id=so.id) + assert '_accessed_time' in session + assert 'foo' not in session # because we didn't save() + + def test_no_autosave_saves_with_save(self): + so = SessionObject({}, auto=False) + so['foo'] = 'bar' + so.save() + so.persist() + session = get_session(id=so.id) + assert '_accessed_time' in session + assert 'foo' in session + + def test_no_autosave_saves_with_delete(self): + req = {'cookie': {'beaker.session.id': 123}} + + so = SessionObject(req, auto=False) + so['foo'] = 'bar' + so.save() + so.persist() + session = get_session(id=so.id) + assert 'foo' in session + + so2 = SessionObject(req, auto=False) + so2.delete() + so2.persist() + session = get_session(id=so2.id) + assert 'foo' not in session + + def test_auto_save_saves_without_save(self): + so = SessionObject({}, auto=True) + so['foo'] = 'bar' + # look ma, no save()! + so.persist() + session = get_session(id=so.id) + assert 'foo' in session + + def test_accessed_time_off_doesnt_save_atime_when_saving(self): + so = SessionObject({}, save_accessed_time=False) + so['foo'] = 'bar' + so.save() + so.persist() + session = get_session(id=so.id, save_accessed_time=False) + assert 'foo' in session + assert '_accessed_time' not in session + + def test_accessed_time_off_doesnt_save_without_save(self): + req = {'cookie': {'beaker.session.id': 123}} + so = SessionObject(req, save_accessed_time=False) + so.persist() # so we can do a set on a non-new session + + so2 = SessionObject(req, save_accessed_time=False) + so2['foo'] = 'bar' + # no save() + so2.persist() + + session = get_session(id=so.id, save_accessed_time=False) + assert '_accessed_time' not in session + assert 'foo' not in session From aaaa7b53385535444bce872f2b11df70b5bfbf44 Mon Sep 17 00:00:00 2001 From: Dan Benamy Date: Fri, 20 May 2016 11:30:15 -0400 Subject: [PATCH 08/16] Fix typo --- beaker/util.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/beaker/util.py b/beaker/util.py index cb94c0b7..c4b61145 100644 --- a/beaker/util.py +++ b/beaker/util.py @@ -315,7 +315,7 @@ def coerce_session_params(params): if cookie_expires and isinstance(cookie_expires, int) and \ not isinstance(cookie_expires, bool): opts['cookie_expires'] = timedelta(seconds=cookie_expires) - if opts['timeout'] is not None and not ops['save_accessed_time']: + if opts['timeout'] is not None and not opts['save_accessed_time']: raise Exception("save_accessed_time must be true to use timeout") return opts From 2d9d8a59291995c23518df624fff3ca84ff9a141 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Thu, 2 Jun 2016 09:01:38 -0400 Subject: [PATCH 09/16] This allows to configure the session subclass that will be proxied by the SessionObject. This change is backwards-compatible. --- beaker/session.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/beaker/session.py b/beaker/session.py index cbd76944..89c97380 100644 --- a/beaker/session.py +++ b/beaker/session.py @@ -667,10 +667,16 @@ def _session(self): environ = self.__dict__['_environ'] self.__dict__['_headers'] = req = {'cookie_out': None} req['cookie'] = environ.get('HTTP_COOKIE') - if params.get('type') == 'cookie': - self.__dict__['_sess'] = CookieSession(req, **params) + session_cls = params.get('session_class', None) + if session_cls is None: + if params.get('type') == 'cookie': + session_cls = CookieSession + else: + session_cls = Session else: - self.__dict__['_sess'] = Session(req, **params) + assert issubclass(session_cls, Session),\ + "Not a Session: " + session_cls + self.__dict__['_sess'] = session_cls(req, **params) return self.__dict__['_sess'] def __getattr__(self, attr): From 5aa47e56ab6e9e58908fd60cd8b0b19057373ebe Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Sun, 5 Jun 2016 20:28:39 +0100 Subject: [PATCH 10/16] Convert readthedocs links for their .org -> .io migration for hosted projects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As per [their blog post of the 27th April](https://blog.readthedocs.com/securing-subdomains/) ‘Securing subdomains’: > Starting today, Read the Docs will start hosting projects from subdomains on the domain readthedocs.io, instead of on readthedocs.org. This change addresses some security concerns around site cookies while hosting user generated data on the same domain as our dashboard. Test Plan: Manually visited all the links I’ve modified. --- beaker/docs/conf.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/beaker/docs/conf.py b/beaker/docs/conf.py index 2293f3ef..7bc08415 100644 --- a/beaker/docs/conf.py +++ b/beaker/docs/conf.py @@ -162,7 +162,7 @@ # 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 = 'http://beaker.rtfd.org/' +html_use_opensearch = 'https://beaker.readthedocs.io/' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' diff --git a/setup.py b/setup.py index af01ea70..f5d7080e 100644 --- a/setup.py +++ b/setup.py @@ -61,7 +61,7 @@ keywords='wsgi myghty session web cache middleware', author='Ben Bangert, Mike Bayer, Philip Jenvey, Alessandro Molina', author_email='ben@groovie.org, pjenvey@groovie.org, amol@turbogears.org', - url='http://beaker.rtfd.org/', + url='https://beaker.readthedocs.io/', license='BSD', packages=find_packages(exclude=['ez_setup', 'examples', 'tests', 'tests.*']), zip_safe=False, From 3ef68999a45b170952e68f364a8d0347527ae7e1 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Sun, 5 Jun 2016 18:51:51 -0400 Subject: [PATCH 11/16] Allow to reset the cookie expire to no expiry --- beaker/session.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/beaker/session.py b/beaker/session.py index 89c97380..44196970 100644 --- a/beaker/session.py +++ b/beaker/session.py @@ -206,11 +206,13 @@ def _set_cookie_expires(self, expires): % repr(self.cookie_expires)) else: expires = None + if not self.cookie or self.key not in self.cookie: + self.cookie[self.key] = self.id if expires is not None: - if not self.cookie or self.key not in self.cookie: - self.cookie[self.key] = self.id self.cookie[self.key]['expires'] = \ expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT") + else: + self.cookie[self.key]['expires'] = '' return expires def _update_cookie_out(self, set_cookie=True): From cfc5dbf185bc475a4ae90a0778b1bf11491975a0 Mon Sep 17 00:00:00 2001 From: Marc-Antoine Parent Date: Fri, 10 Jun 2016 08:01:46 -0400 Subject: [PATCH 12/16] rewrote _set_cookie_expires, and added a test. --- beaker/session.py | 33 ++++++++++++++++----------------- tests/test_cookie_expires.py | 34 ++++++++++++++++++++++++++-------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/beaker/session.py b/beaker/session.py index 44196970..58ee365a 100644 --- a/beaker/session.py +++ b/beaker/session.py @@ -194,26 +194,25 @@ def _set_cookie_values(self, expires=None): def _set_cookie_expires(self, expires): if expires is None: - if self.cookie_expires is not True: - if self.cookie_expires is False: - expires = datetime.fromtimestamp(0x7FFFFFFF) - elif isinstance(self.cookie_expires, timedelta): - expires = datetime.utcnow() + self.cookie_expires - elif isinstance(self.cookie_expires, datetime): - expires = self.cookie_expires - else: - raise ValueError("Invalid argument for cookie_expires: %s" - % repr(self.cookie_expires)) - else: - expires = None + expires = self.cookie_expires + if expires is False: + expires_date = datetime.fromtimestamp(0x7FFFFFFF) + elif isinstance(expires, timedelta): + expires_date = datetime.utcnow() + expires + elif isinstance(expires, datetime): + expires_date = expires + elif expires is not True: + raise ValueError("Invalid argument for cookie_expires: %s" + % repr(self.cookie_expires)) + self.cookie_expires = expires if not self.cookie or self.key not in self.cookie: self.cookie[self.key] = self.id - if expires is not None: - self.cookie[self.key]['expires'] = \ - expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT") - else: + if expires is True: self.cookie[self.key]['expires'] = '' - return expires + return True + self.cookie[self.key]['expires'] = \ + expires_date.strftime("%a, %d-%b-%Y %H:%M:%S GMT") + return expires_date def _update_cookie_out(self, set_cookie=True): self.request['cookie_out'] = self.cookie[self.key].output(header='') diff --git a/tests/test_cookie_expires.py b/tests/test_cookie_expires.py index 568b4a40..7b95ac80 100644 --- a/tests/test_cookie_expires.py +++ b/tests/test_cookie_expires.py @@ -34,16 +34,34 @@ def app(*args, **kw): assert_equal(val, expected[pos]) +def cookie_expiration(session): + cookie = session.cookie.output() + expiry_m = re.match('Set-Cookie: beaker.session.id=[0-9a-f]{32}(; expires=[^;]+)?; Path=/', cookie) + assert expiry_m + expiry = expiry_m.group(1) + if expiry is None: + return True + if re.match('; expires=(Mon|Tue), 1[89]-Jan-2038 [0-9:]{8} GMT', expiry): + return False + else: + return expiry[10:] + + def test_cookie_exprires_2(): """Exhibit Set-Cookie: values.""" - expires = Session( - {}, cookie_expires=True - ).cookie.output() + expires = cookie_expiration(Session({}, cookie_expires=True)) - assert re.match('Set-Cookie: beaker.session.id=[0-9a-f]{32}; Path=/', expires), expires - no_expires = Session( - {}, cookie_expires=False - ).cookie.output() + assert expires is True, expires + no_expires = cookie_expiration(Session({}, cookie_expires=False)) - assert re.match('Set-Cookie: beaker.session.id=[0-9a-f]{32}; expires=(Mon|Tue), 1[89]-Jan-2038 [0-9:]{8} GMT; Path=/', no_expires), no_expires + assert no_expires is False, no_expires + +def test_set_cookie_expires(): + """Exhibit Set-Cookie: values.""" + session = Session({}, cookie_expires=True) + assert cookie_expiration(session) is True + session._set_cookie_expires(False) + assert cookie_expiration(session) is False + session._set_cookie_expires(True) + assert cookie_expiration(session) is True From 1443f1e46bac4d46211a30e97af16901f26e02dc Mon Sep 17 00:00:00 2001 From: Dan Benamy Date: Fri, 17 Jun 2016 12:57:24 -0400 Subject: [PATCH 13/16] Make _accessed_time an mtime when save_accessed_time is false Based on feedback in https://github.com/bbangert/beaker/pull/102. This simplifies the implementation. --- CHANGELOG | 7 +-- beaker/docs/configuration.rst | 8 ++++ beaker/docs/sessions.rst | 8 ++-- beaker/session.py | 49 ++++++++++---------- tests/test_session.py | 86 +++++++++++++++++++++++------------ 5 files changed, 97 insertions(+), 61 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 49901ce1..5aa8e07b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,9 +2,10 @@ Release $next ============= * Sessions have a new option save_accessed_time which defaults to true for - backwards compatibility. Set to false to tell beaker not to add _accessed_time - to the session and the last_accessed attribute. This lets you avoid session - writes for requests that don't otherwise update the session. + backwards compatibility. Set to false to tell beaker not to update + _accessed_time if the session hasn't been changed, for non-cookie sessions + stores. This lets you avoid needless datastore writes. _accessed_time will + always be updated when the session is intentionally saved. Release 1.8.0 (2016-01-26) ========================== diff --git a/beaker/docs/configuration.rst b/beaker/docs/configuration.rst index 866bff49..2c5b9011 100644 --- a/beaker/docs/configuration.rst +++ b/beaker/docs/configuration.rst @@ -170,6 +170,12 @@ cookie_domain (**optional**, string) key (**required**, string) Name of the cookie key used to save the session under. +save_accessed_time (**optional**, bool) + Whether beaker should save the session's access time (true) or only + modification time (false). + + Defaults to true. + secret (**required**, string) Used with the HMAC to ensure session integrity. This value should ideally be a randomly generated string. @@ -189,6 +195,8 @@ timeout (**optional**, integer) Defaults to never expiring. + Requires that save_accessed_time be true. + Encryption Options ------------------ diff --git a/beaker/docs/sessions.rst b/beaker/docs/sessions.rst index db4d2d33..300d0676 100644 --- a/beaker/docs/sessions.rst +++ b/beaker/docs/sessions.rst @@ -84,13 +84,13 @@ application. * id - Unique 40 char SHA-generated session ID * last_accessed - The last time the session was accessed before the current - access, will be None if the session was just made; only set if the - save_accessed_time setting is true (the default) + access, if save_accessed_time is true; the last time it was modified if false; + will be None if the session was just made There's several special session keys populated as well: -* _accessed_time - Current accessed time of the session, when it was loaded; - only set if the save_accessed_time setting is true (the default) +* _accessed_time - When the session was loaded if save_accessed_time is true; + when it was last written if false * _creation_time - When the session was created diff --git a/beaker/session.py b/beaker/session.py index eee2ef7f..6e9a56bf 100644 --- a/beaker/session.py +++ b/beaker/session.py @@ -92,7 +92,8 @@ class Session(dict): disable session time out. :type timeout: int or None :param save_accessed_time: Whether beaker should save the session's access - time. Defaults to true. + time (True) or only modification time (False). + Defaults to True. :param cookie_expires: Expiration date for cookie :param cookie_domain: Domain to use for the cookie. :param cookie_path: Path to use for the cookie. @@ -171,9 +172,7 @@ def __init__(self, request, id=None, invalidate_corrupt=False, self.is_new = self.id is None if self.is_new: self._create_id() - self['_creation_time'] = time.time() - if self.save_atime: - self['_accessed_time'] = self['_creation_time'] + self['_accessed_time'] = self['_creation_time'] = time.time() else: try: self.load() @@ -359,41 +358,37 @@ def load(self): # present if session_data is None: session_data = { - '_creation_time': now + '_creation_time': now, + '_accessed_time': now } - if self.save_atime: - session_data['_accessed_time'] = now self.is_new = True except (KeyError, TypeError): session_data = { - '_creation_time': now + '_creation_time': now, + '_accessed_time': now } - if self.save_atime: - session_data['_accessed_time'] = now self.is_new = True if session_data is None or len(session_data) == 0: session_data = { - '_creation_time': now + '_creation_time': now, + '_accessed_time': now } - if self.save_atime: - session_data['_accessed_time'] = now self.is_new = True if self.timeout is not None and \ now - session_data['_accessed_time'] > self.timeout: timed_out = True else: - if self.save_atime: - # Properly set the last_accessed time, which is different - # than the *currently* _accessed_time - if self.is_new or '_accessed_time' not in session_data: - self.last_accessed = None - else: - self.last_accessed = session_data['_accessed_time'] + # Properly set the last_accessed time, which is different + # than the *currently* _accessed_time + if self.is_new or '_accessed_time' not in session_data: + self.last_accessed = None + else: + self.last_accessed = session_data['_accessed_time'] - # Update the current _accessed_time - session_data['_accessed_time'] = now + # Update the current _accessed_time + session_data['_accessed_time'] = now # Set the path if applicable if '_path' in session_data: @@ -415,7 +410,7 @@ def save(self, accessed_only=False): """ # Look to see if its a new session that was only accessed # Don't save it under that case - if accessed_only and self.is_new: + if accessed_only and (self.is_new or not self.save_atime): return None # this session might not have a namespace yet or the session id @@ -500,6 +495,9 @@ class CookieSession(Session): regardless of the cookie being present or not to determine whether session data is still valid. :type timeout: int + :param save_accessed_time: Whether beaker should save the session's access + time (True) or only modification time (False). + Defaults to True. :param cookie_expires: Expiration date for cookie :param cookie_domain: Domain to use for the cookie. :param cookie_path: Path to use for the cookie. @@ -603,7 +601,7 @@ def _get_path(self): def save(self, accessed_only=False): """Saves the data for this session to persistent storage""" - if accessed_only and self.is_new: + if accessed_only and (self.is_new or not self.save_atime): return if accessed_only: self.clear() @@ -620,8 +618,7 @@ def _create_cookie(self): self['_creation_time'] = time.time() if '_id' not in self: self['_id'] = _session_id() - if self.save_atime: - self['_accessed_time'] = time.time() + self['_accessed_time'] = time.time() val = self._encrypt_data() if len(val) > 4064: diff --git a/tests/test_session.py b/tests/test_session.py index 16a12eba..5cfc89e9 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from beaker._compat import u_, pickle +import shutil import sys import time import unittest @@ -342,31 +343,59 @@ def test_invalidate_corrupt(): assert "foo" not in dict(session) -def test_saves_accessed_time(): - session = get_session(save_accessed_time=True) - session.save() - atime1 = session['_accessed_time'] - - session2 = get_session(id=session.id, save_accessed_time=True) - session2.save() - assert session2['_accessed_time'] > atime1 - assert session2.last_accessed == atime1 - - -def test_doesnt_save_accessed_time(): - session = get_session(save_accessed_time=False) - session.save() - assert '_accessed_time' not in session - session2 = get_session(id=session.id, save_accessed_time=False) - session2.save() - assert '_accessed_time' not in session2 - assert not hasattr(session2, 'last_accessed') - - -def test_saves_creation_time_even_witout_accessed_time(): - session = get_session(save_accessed_time=False) - session.save() - assert '_creation_time' in session +class TestSaveAccessedTime(unittest.TestCase): + # These tests can't use the memory session type since it seems that loading + # winds up with references to the underlying storage and makes changes to + # sessions even though they aren't save()ed. + def setUp(self): + # Ignore errors because in most cases the dir won't exist. + shutil.rmtree('./cache', ignore_errors=True) + + def tearDown(self): + shutil.rmtree('./cache') + + def test_saves_if_session_written_and_accessed_time_false(self): + session = get_session(data_dir='./cache', save_accessed_time=False) + # New sessions are treated a little differently so save the session + # before getting into the meat of the test. + session.save() + session = get_session(data_dir='./cache', save_accessed_time=False, + id=session.id) + last_accessed = session.last_accessed + session.save(accessed_only=False) + session = get_session(data_dir='./cache', save_accessed_time=False, + id=session.id) + # If the second save saved, we'll have a new last_accessed time. + self.assertGreater(session.last_accessed, last_accessed) + + + def test_saves_if_session_not_written_and_accessed_time_true(self): + session = get_session(data_dir='./cache', save_accessed_time=True) + # New sessions are treated a little differently so save the session + # before getting into the meat of the test. + session.save() + session = get_session(data_dir='./cache', save_accessed_time=True, + id=session.id) + last_accessed = session.last_accessed + session.save(accessed_only=True) # this is the save we're really testing + session = get_session(data_dir='./cache', save_accessed_time=True, + id=session.id) + # If the second save saved, we'll have a new last_accessed time. + self.assertGreater(session.last_accessed, last_accessed) + + + def test_doesnt_save_if_session_not_written_and_accessed_time_false(self): + session = get_session(data_dir='./cache', save_accessed_time=False) + # New sessions are treated a little differently so save the session + # before getting into the meat of the test. + session.save() + session = get_session(data_dir='./cache', save_accessed_time=False, + id=session.id) + last_accessed = session.last_accessed + session.save(accessed_only=True) # this shouldn't actually save + session = get_session(data_dir='./cache', save_accessed_time=False, + id=session.id) + self.assertEqual(session.last_accessed, last_accessed) class TestSessionObject(unittest.TestCase): @@ -417,14 +446,16 @@ def test_auto_save_saves_without_save(self): session = get_session(id=so.id) assert 'foo' in session - def test_accessed_time_off_doesnt_save_atime_when_saving(self): + def test_accessed_time_off_saves_atime_when_saving(self): so = SessionObject({}, save_accessed_time=False) + atime = so['_accessed_time'] so['foo'] = 'bar' so.save() so.persist() session = get_session(id=so.id, save_accessed_time=False) assert 'foo' in session - assert '_accessed_time' not in session + assert '_accessed_time' in session + self.assertEqual(session.last_accessed, atime) def test_accessed_time_off_doesnt_save_without_save(self): req = {'cookie': {'beaker.session.id': 123}} @@ -437,5 +468,4 @@ def test_accessed_time_off_doesnt_save_without_save(self): so2.persist() session = get_session(id=so.id, save_accessed_time=False) - assert '_accessed_time' not in session assert 'foo' not in session From 18128d6b92b71318d390d472061f42ce39024d11 Mon Sep 17 00:00:00 2001 From: Dan Benamy Date: Fri, 17 Jun 2016 12:58:39 -0400 Subject: [PATCH 14/16] Fix test_timeout_requires_accessed_time --- tests/test_session.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_session.py b/tests/test_session.py index 5cfc89e9..542d5f9f 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -194,10 +194,11 @@ def test_timeout(): def test_timeout_requires_accessed_time(): - """Test that it doesn't allow setting save_accessed_time to True with + """Test that it doesn't allow setting save_accessed_time to False with timeout enabled """ get_session(timeout=None, save_accessed_time=True) # is ok + get_session(timeout=None, save_accessed_time=False) # is ok assert_raises(BeakerException, get_session, timeout=2, From 0ab706e23dcc90ae48a065767c6afaf3e7bb5900 Mon Sep 17 00:00:00 2001 From: Dan Benamy Date: Fri, 17 Jun 2016 13:32:35 -0400 Subject: [PATCH 15/16] Fix tests on python 2.6 --- tests/test_session.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/test_session.py b/tests/test_session.py index 542d5f9f..a0193036 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -367,7 +367,10 @@ def test_saves_if_session_written_and_accessed_time_false(self): session = get_session(data_dir='./cache', save_accessed_time=False, id=session.id) # If the second save saved, we'll have a new last_accessed time. - self.assertGreater(session.last_accessed, last_accessed) + # Python 2.6 doesn't have assertGreater :-( + assert session.last_accessed > last_accessed, ( + '%r is not greater than %r' % + (session.last_accessed, last_accessed)) def test_saves_if_session_not_written_and_accessed_time_true(self): @@ -382,7 +385,10 @@ def test_saves_if_session_not_written_and_accessed_time_true(self): session = get_session(data_dir='./cache', save_accessed_time=True, id=session.id) # If the second save saved, we'll have a new last_accessed time. - self.assertGreater(session.last_accessed, last_accessed) + # Python 2.6 doesn't have assertGreater :-( + assert session.last_accessed > last_accessed, ( + '%r is not greater than %r' % + (session.last_accessed, last_accessed)) def test_doesnt_save_if_session_not_written_and_accessed_time_false(self): From 41f549646f2dadcb50f3154051b78c40c79c67fd Mon Sep 17 00:00:00 2001 From: makoto kuwata Date: Wed, 14 Sep 2016 16:48:34 +0900 Subject: [PATCH 16/16] fix to import StringIO.StringIO first In Python 2.6 and 2.7, `io.StringIO` class is available but it is not compatible with `String.StringIO` class. * `io.StringIO` class accepts unicode string. * `StringIO.StringIO` class accepts bytes string. Therefore `StringIO.StringIO` class should be imported prior to `io.StringIO`. Otherwise, `traceback.print_exc(file=tb)` (line 107) will raise TypeError. For example: ``` File "/opt/python/blue_env/local/lib/python2.7/site-packages/Beaker-1.8.0-py2.7.egg/beaker/cache.py", line 107, in _init traceback.print_exc(file=tb) File "/usr/lib/python2.7/traceback.py", line 241, in print_exc print_exception(etype, value, tb, limit, file) File "/usr/lib/python2.7/traceback.py", line 132, in print_exception _print(file, 'Traceback (most recent call last):') File "/usr/lib/python2.7/traceback.py", line 15, in _print file.write(str+terminator) TypeError: unicode argument expected, got 'str' ``` --- beaker/cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/beaker/cache.py b/beaker/cache.py index cbb071d2..dd8645fe 100644 --- a/beaker/cache.py +++ b/beaker/cache.py @@ -99,9 +99,9 @@ def _init(self): if not isinstance(sys.exc_info()[1], DistributionNotFound): import traceback try: - from io import StringIO + from StringIO import StringIO # Python2 except ImportError: - from StringIO import StringIO + from io import StringIO # Python3 tb = StringIO() traceback.print_exc(file=tb)