Revision: 8c529d1a0371
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Wed Apr 6 07:47:31 2011
Log: A few dockblock fixes.
http://code.google.com/p/tipfy/source/detail?r=8c529d1a0371
Revision: af5cb2fc89cf
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Wed Apr 6 07:54:03 2011
Log: Better variable interpolation in manage/config.py.
http://code.google.com/p/tipfy/source/detail?r=af5cb2fc89cf
Revision: 2bb928027963
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Wed Apr 6 09:34:32 2011
Log: Some docblocks added to manage/config.py.
http://code.google.com/p/tipfy/source/detail?r=2bb928027963
Revision: fde31b24e31c
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Fri Apr 8 03:12:34 2011
Log: Removed MakeFile and duplicated manage/utils.py.
http://code.google.com/p/tipfy/source/detail?r=fde31b24e31c
Revision: 896b1c4c4201
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sun Apr 10 16:21:06 2011
Log: Added builder.close() to get_test_context().
http://code.google.com/p/tipfy/source/detail?r=896b1c4c4201
Revision: 5b45741f4d04
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Fri Apr 15 06:52:18 2011
Log: Starting sessions review.
http://code.google.com/p/tipfy/source/detail?r=5b45741f4d04
Revision: 3f96e9cc186c
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sun Apr 17 06:19:50 2011
Log: Reverted sessions.
http://code.google.com/p/tipfy/source/detail?r=3f96e9cc186c
Revision: a07fb37a97b0
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sun Apr 17 06:49:01 2011
Log: Organized imports.
http://code.google.com/p/tipfy/source/detail?r=a07fb37a97b0
Revision: 00e03f9a9e4f
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sat Apr 23 15:22:32 2011
Log: Set correct version number.
http://code.google.com/p/tipfy/source/detail?r=00e03f9a9e4f
Revision: 29099f92601e
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sat Apr 23 15:36:10 2011
Log: Fixed weird set_sys_path side effect. Thanks for the the report
and so...
http://code.google.com/p/tipfy/source/detail?r=29099f92601e
==============================================================================
Revision: 8c529d1a0371
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Wed Apr 6 07:47:31 2011
Log: A few dockblock fixes.
http://code.google.com/p/tipfy/source/detail?r=8c529d1a0371
Modified:
/.hgignore
/tipfy/app.py
/tipfy/handler.py
/tipfyext/jinja2/__init__.py
=======================================
--- /.hgignore Sat Apr 2 18:15:11 2011
+++ /.hgignore Wed Apr 6 07:47:31 2011
@@ -4,24 +4,25 @@
\.egg-info
\.bak
\.swp
-dist
-docs/build
-docs/project
-tests/env
-htmlcov
+dist/
+docs/build/
+docs/project/
+tests/env/
+htmlcov/
+temp/
# ZC buildout; see http://www.buildout.org/docs/dirstruct.html
project/\.installed.cfg
-project/var/parts
-project/var/develop-eggs
-project/bin
-project/eggs
-project/var/downloads
+project/var/parts/
+roject/var/develop-eggs/
+project/bin/
+project/eggs/
+project/var/downloads/
# Examples
examples/auth/\.installed.cfg
-examples/auth/var/parts
-examples/auth/var/develop-eggs
-examples/auth/bin
-examples/auth/eggs
-examples/auth/var/downloads
+examples/auth/var/parts/
+examples/auth/var/develop-eggs/
+examples/auth/bin/
+examples/auth/eggs/
+examples/auth/var/downloads/
=======================================
--- /tipfy/app.py Sun Apr 3 07:55:49 2011
+++ /tipfy/app.py Wed Apr 6 07:47:31 2011
@@ -3,7 +3,7 @@
tipfy.app
~~~~~~~~~
- WSGI Application and RequestHandler.
+ WSGI Application.
:copyright: 2011 by tipfy.org.
:license: BSD, see LICENSE.txt for more details.
@@ -26,7 +26,7 @@
from . import default_config
from .config import Config, REQUIRED_VALUE
from .local import current_app, current_handler, get_request, local
-from .routing import Router, Rule
+from .routing import Router
#: Public interface.
HTTPException = werkzeug.exceptions.HTTPException
@@ -279,11 +279,10 @@
exception is reraised.
.. note::
- Although being a :class:`RequestHandler`, the error handler will
- execute the ``handle_exception`` method after instantiation,
instead
- of the method corresponding to the current request.
-
- Also, the error handler is responsible for setting the response
+ The exception is stored in the request object and accessible
+ through `request.exception`.
+
+ The error handler is responsible for setting the response
status code and logging the exception, as shown in the example
above.
=======================================
--- /tipfy/handler.py Sun Apr 3 07:14:04 2011
+++ /tipfy/handler.py Wed Apr 6 07:47:31 2011
@@ -22,12 +22,11 @@
Additionally it provides lazy access to auth, i18n and session stores,
and several utilities to handle a request.
- Although it is convenient to extend this class
(or :class:`RequestHandler`)
- and some extended functionality like sessions is implemented on top of
it,
- the only required interface by the WSGI app is the following:
+ Although it is convenient to extend this class
or :class:`RequestHandler`,
+ the only interface required by the WSGI app is the following:
class RequestHandler(object):
- def __init__(self, app, request):
+ def __init__(self, request):
pass
def __call__(self):
=======================================
--- /tipfyext/jinja2/__init__.py Sun Apr 3 07:14:04 2011
+++ /tipfyext/jinja2/__init__.py Wed Apr 6 07:47:31 2011
@@ -207,6 +207,21 @@
return self.jinja2.render_response(self, _filename, **context)
+"""
+# Example of using signals.
+
+from tipfyext.jinja2 import environment_created
+
+def setup_environment(jinja2, environment):
+ environment.globals.update({
+ # ... custom globals ...
+ })
+ environment.filters.update({
+ # ... custom filters ...
+ })
+
+environment_created.connect(setup_environment)
+"""
_signals = blinker.Namespace()
environment_created = _signals.signal('environment-created')
template_rendered = _signals.signal('template-rendered')
==============================================================================
Revision: af5cb2fc89cf
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Wed Apr 6 07:54:03 2011
Log: Better variable interpolation in manage/config.py.
http://code.google.com/p/tipfy/source/detail?r=af5cb2fc89cf
Modified:
/manage/config.py
/manage/manage.py
=======================================
--- /manage/config.py Sun Mar 20 05:39:46 2011
+++ /manage/config.py Wed Apr 6 07:54:03 2011
@@ -113,46 +113,74 @@
return converter(value)
except (ConfigParser.NoSectionError,
ConfigParser.NoOptionError):
pass
- except ValueError:
- # Failed conversion?
- pass
return default
- def _interpolate(self, section, option, value, tried=None):
- """Replaces variables avoiding infinite recursion."""
- if not '%(' in value:
- return value
-
- matches = set(self._interpolate_re.findall(value))
- if not matches:
- return value
+ def _find_variables(self, section, option, raw_value):
+ """Adapted from SafeConfigParser._interpolate_some()."""
+ result = set()
+ while raw_value:
+ pos = raw_value.find('%')
+ if pos < 0:
+ return result
+ if pos > 0:
+ raw_value = raw_value[pos:]
+
+ char = raw_value[1:2]
+ if char == '%':
+ raw_value = raw_value[2:]
+ elif char == '(':
+ match = self._interpolate_re.match(raw_value)
+ if match is None:
+ raise ConfigParser.InterpolationSyntaxError(option,
+ section, 'Bad interpolation variable
reference: %r.' %
+ raw_value)
+
+ result.add(match.group(1))
+ raw_value = raw_value[match.end():]
+ else:
+ raise ConfigParser.InterpolationSyntaxError(
+ option, section,
+ "'%%' must be followed by '%%' or '(', "
+ "found: %r." % raw_value)
+
+ return result
+
+ def _interpolate(self, section, option, raw_value, tried=None):
+ variables = self._find_variables(section, option, raw_value)
+ if not variables:
+ return raw_value
if tried is None:
tried = [(section, option)]
- variables = {}
- for match in matches:
- parts = tuple(match.split('|', 1))
+ values = {}
+ for var in variables:
+ parts = var.split('|', 1)
if len(parts) == 1:
- new_section, new_option = section, match
+ new_section, new_option = section, var
else:
new_section, new_option = parts
if parts in tried:
continue
- tried.append(parts)
try:
found = self._get(new_section, new_option)
+ except (ConfigParser.NoSectionError,
ConfigParser.NoOptionError):
+ raise ConfigParser.InterpolationError(section, option,
+ 'Could not find section %r and option %r.' %
+ (new_section, new_option))
+
+ tried.append((new_section, new_option))
+ if not self.has_option(new_section, new_option):
tried.append(('DEFAULT', new_option))
- variables[match] = self._interpolate(new_section,
new_option,
- found, tried)
- except Exception:
- pass
-
- if len(matches) == len(variables):
- return value % variables
-
- raise ConfigParser.InterpolationError(section, option,
- 'Cound not replace %r.' % value)
+ values[var] = self._interpolate(new_section, new_option,
+ found, tried)
+
+ try:
+ return raw_value % values
+ except KeyError, e:
+ raise ConfigParser.InterpolationError(section, option,
+ 'Cound not replace %r: variable %r is missing.' %
+ (raw_value, e.args[0]))
=======================================
--- /manage/manage.py Sun Mar 20 05:39:46 2011
+++ /manage/manage.py Wed Apr 6 07:54:03 2011
@@ -1,14 +1,14 @@
#!/usr/bin/env python
-import ConfigParser
import os
import runpy
import shutil
-import re
import sys
import textwrap
import argparse
+from config import Config
+
# Be a good neighbour.
if sys.platform == 'win32':
@@ -53,126 +53,6 @@
raise
-class Config(ConfigParser.RawConfigParser):
- """Wraps RawConfigParser `get*()` functions to allow a default to be
- returned instead of throwing errors. Also adds `getlist()` to split
- multi-line values into a list.
- """
- _interpolate_re = re.compile(r"%\(([^)]*)\)s")
-
- _boolean_states = {
- '1': True,
- 'yes': True,
- 'true': True,
- 'on': True,
- '0': False,
- 'no': False,
- 'false': False,
- 'off': False,
- }
-
- def get(self, section, option, default=None, raw=False):
- return self._get_wrapper(section, option, unicode, default, raw)
-
- def getboolean(self, section, option, default=None, raw=False):
- return self._get_wrapper(section, option, self._to_boolean,
default,
- raw)
-
- def getfloat(self, section, option, default=None, raw=False):
- return self._get_wrapper(section, option, self._to_float, default,
raw)
-
- def getint(self, section, option, default=None, raw=False):
- return self._get_wrapper(section, option, self._to_int, default,
raw)
-
- def getlist(self, section, option, default=None, raw=False,
unique=True):
- res = self._get_wrapper(section, option, self._to_list, default,
raw)
- if unique:
- return get_unique_sequence(res)
-
- return res
-
- def _get(self, section, option):
- return ConfigParser.RawConfigParser.get(self, section, option)
-
- def _to_boolean(self, value):
- key = value.lower()
- if key not in self._boolean_states:
- raise ValueError('Not a boolean: %r. Booleans must be '
- 'one of %s.' %
(value, ', '.join(self._boolean_states.keys())))
-
- return self._boolean_states[key]
-
- def _to_float(self, value):
- return float(value)
-
- def _to_int(self, value):
- return int(value)
-
- def _to_list(self, value):
- value = [line.strip() for line in value.splitlines()]
- return [v for v in value if v]
-
- def _get_wrapper(self, sections, option, converter, default, raw):
- """Wraps get functions allowing default values and a list of
sections
- looked up in order until a value is found.
- """
- if isinstance(sections, basestring):
- sections = [sections]
-
- for section in sections:
- try:
- value = self._get(section, option)
- if not raw:
- value = self._interpolate(section, option, value)
-
- return converter(value)
- except (ConfigParser.NoSectionError,
ConfigParser.NoOptionError):
- pass
- except ValueError:
- # Failed conversion?
- pass
-
- return default
-
- def _interpolate(self, section, option, value, tried=None):
- if not '%(' in value:
- return value
-
- matches = self._interpolate_re.findall(value)
- if not matches:
- return value
-
- if tried is None:
- tried = [(section, option)]
-
- variables = {}
- matches = get_unique_sequence(matches)
- for match in matches:
- parts = tuple(match.split('|', 1))
- if len(parts) == 1:
- new_section, new_option = section, match
- else:
- new_section, new_option = parts
-
- if parts in tried:
- continue
-
- tried.append(parts)
- try:
- found = self._get(new_section, new_option)
- tried.append(('DEFAULT', new_option))
- variables[match] = self._interpolate(new_section,
new_option,
- found, tried)
- except Exception:
- pass
-
- if len(matches) == len(variables):
- return value % variables
-
- raise ConfigParser.InterpolationError(section, option,
- 'Cound not replace %r.' % value)
-
-
class Action(object):
"""Base interface for custom actions."""
#: Action name.
==============================================================================
Revision: 2bb928027963
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Wed Apr 6 09:34:32 2011
Log: Some docblocks added to manage/config.py.
http://code.google.com/p/tipfy/source/detail?r=2bb928027963
Modified:
/manage/config.py
=======================================
--- /manage/config.py Wed Apr 6 07:54:03 2011
+++ /manage/config.py Wed Apr 6 09:34:32 2011
@@ -3,6 +3,10 @@
class Converter(object):
+ """Converts config values to several types.
+
+ Supported types are boolean, float, int, list and unicode.
+ """
_boolean_states = {
'1': True,
'yes': True,
@@ -37,15 +41,23 @@
class Config(ConfigParser.RawConfigParser):
- """Wraps RawConfigParser `get*()` functions to allow a default to be
- returned instead of throwing errors. Also adds `getlist()` to split
- multi-line values into a list.
-
- It also implements the magical interpolation behavior similar to the
one
- from `SafeConfigParser`, but also supports references to sections.
- This means that values can contain format strings which refer to other
- values in the config file. These variables are replaced on the fly.
- The most basic example is::
+ """Extended RawConfigParser with the following extra features:
+
+ - All `get*()` functions allow a default to be returned. Instead
+ of throwing errors when no section or option is found, it returns the
+ default value or None.
+
+ - The `get*()` functions can receive a list of sections to be searched
in
+ order.
+
+ - A `getlist()` method splits multi-line values into a list.
+
+ - It also implements the magical interpolation behavior similar to the
one
+ from `SafeConfigParser`, but also supports references to sections.
+ This means that values can contain format strings which refer to
other
+ values in the config file. These variables are replaced on the fly.
+
+ An example of variable substituition is::
[my_section]
app_name = my_app
@@ -74,27 +86,60 @@
_interpolate_re = re.compile(r"%\(([^)]*)\)s")
- def get(self, section, option, default=None, raw=False):
+ def get(self, sections, option, default=None, raw=False):
+ """Returns a config value from a given section, converted to
unicode.
+
+ :param sections:
+ The config section name, or a list of config section names to
be
+ searched in order.
+ :param option:
+ The config option name.
+ :param default:
+ A default value to return in case the section or option are not
+ found. Default is None.
+ :param raw:
+ If True, doesn't perform variable substitution if the value
+ has placeholders. Default is False.
+ :returns:
+ A config value.
+ """
converter = self.converter.to_unicode
- return self._get_wrapper(section, option, converter, default, raw)
-
- def getboolean(self, section, option, default=None, raw=False):
+ return self._get_wrapper(sections, option, converter, default, raw)
+
+ def getboolean(self, sections, option, default=None, raw=False):
+ """Returns a config value from a given section, converted to
boolean.
+
+ See :methd:`get` for a description of the parameters.
+ """
converter = self.converter.to_boolean
- return self._get_wrapper(section, option, converter, default, raw)
-
- def getfloat(self, section, option, default=None, raw=False):
+ return self._get_wrapper(sections, option, converter, default, raw)
+
+ def getfloat(self, sections, option, default=None, raw=False):
+ """Returns a config value from a given section, converted to float.
+
+ See :methd:`get` for a description of the parameters.
+ """
converter = self.converter.to_float
- return self._get_wrapper(section, option, converter, default, raw)
-
- def getint(self, section, option, default=None, raw=False):
+ return self._get_wrapper(sections, option, converter, default, raw)
+
+ def getint(self, sections, option, default=None, raw=False):
+ """Returns a config value from a given section, converted to int.
+
+ See :methd:`get` for a description of the parameters.
+ """
converter = self.converter.to_int
- return self._get_wrapper(section, option, converter, default, raw)
-
- def getlist(self, section, option, default=None, raw=False):
+ return self._get_wrapper(sections, option, converter, default, raw)
+
+ def getlist(self, sections, option, default=None, raw=False):
+ """Returns a config value from a given section, converted to
boolean.
+
+ See :methd:`get` for a description of the parameters.
+ """
converter = self.converter.to_list
- return self._get_wrapper(section, option, converter, default, raw)
+ return self._get_wrapper(sections, option, converter, default, raw)
def _get(self, section, option):
+ """Wrapper for `RawConfigParser.get`."""
return ConfigParser.RawConfigParser.get(self, section, option)
def _get_wrapper(self, sections, option, converter, default, raw):
@@ -116,38 +161,9 @@
return default
- def _find_variables(self, section, option, raw_value):
- """Adapted from SafeConfigParser._interpolate_some()."""
- result = set()
- while raw_value:
- pos = raw_value.find('%')
- if pos < 0:
- return result
- if pos > 0:
- raw_value = raw_value[pos:]
-
- char = raw_value[1:2]
- if char == '%':
- raw_value = raw_value[2:]
- elif char == '(':
- match = self._interpolate_re.match(raw_value)
- if match is None:
- raise ConfigParser.InterpolationSyntaxError(option,
- section, 'Bad interpolation variable
reference: %r.' %
- raw_value)
-
- result.add(match.group(1))
- raw_value = raw_value[match.end():]
- else:
- raise ConfigParser.InterpolationSyntaxError(
- option, section,
- "'%%' must be followed by '%%' or '(', "
- "found: %r." % raw_value)
-
- return result
-
def _interpolate(self, section, option, raw_value, tried=None):
- variables = self._find_variables(section, option, raw_value)
+ """Performs variable substituition in a config value."""
+ variables = self._get_variable_names(section, option, raw_value)
if not variables:
return raw_value
@@ -184,3 +200,36 @@
raise ConfigParser.InterpolationError(section, option,
'Cound not replace %r: variable %r is missing.' %
(raw_value, e.args[0]))
+
+ def _get_variable_names(self, section, option, raw_value):
+ """Returns a list of placeholder names in a config value, if any.
+
+ Adapted from SafeConfigParser._interpolate_some().
+ """
+ result = set()
+ while raw_value:
+ pos = raw_value.find('%')
+ if pos < 0:
+ return result
+ if pos > 0:
+ raw_value = raw_value[pos:]
+
+ char = raw_value[1:2]
+ if char == '%':
+ raw_value = raw_value[2:]
+ elif char == '(':
+ match = self._interpolate_re.match(raw_value)
+ if match is None:
+ raise ConfigParser.InterpolationSyntaxError(option,
+ section, 'Bad interpolation variable
reference: %r.' %
+ raw_value)
+
+ result.add(match.group(1))
+ raw_value = raw_value[match.end():]
+ else:
+ raise ConfigParser.InterpolationSyntaxError(
+ option, section,
+ "'%%' must be followed by '%%' or '(', "
+ "found: %r." % raw_value)
+
+ return result
==============================================================================
Revision: fde31b24e31c
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Fri Apr 8 03:12:34 2011
Log: Removed MakeFile and duplicated manage/utils.py.
http://code.google.com/p/tipfy/source/detail?r=fde31b24e31c
Deleted:
/Makefile
/manage/utils.py
=======================================
--- /Makefile Sun Apr 3 07:14:04 2011
+++ /dev/null
@@ -1,125 +0,0 @@
-# Convenience to run tests and coverage.
-# You must have installed the App Engine SDK toolkit, version 1.4.0 or
-# later, and it must be installed in /usr/local/google_appengine.
-# This probably won't work on Windows.
-# Borrowed from http://code.google.com/p/appengine-ndb-experiment/
-
-FLAGS=
-GAE= /usr/local/google_appengine
-GAEPATH=$(GAE):$(GAE)/lib/django_0_96:$(GAE)/lib/webob:$(GAE)/lib/yaml/lib:tests
-TESTS= `find tests -name [a-z]\*_test.py`
-NONTESTS=`find tipfy -name [a-z]\*.py`
-PORT= 8080
-ADDRESS=localhost
-PYTHON= python -Wignore
-failed=0
-
-define run_test
- PYTHONPATH=$(GAEPATH):. $(PYTHON) -m tests.$1_test $(FLAGS)
-endef
-
-test:
- for i in $(TESTS); \
- do \
- echo $$i; \
- PYTHONPATH=$(GAEPATH):. $(PYTHON) -m tests.`basename $$i .py`
$(FLAGS); \
- done
-
-# 'app', 'auth', 'config', 'dev', 'ext_jinja2', 'ext_mako', 'gae_acl',
-# 'gae_blobstore', 'gae_db', 'gae_mail', 'gae_sharded_counter',
-# 'gae_taskqueue', 'gae_xmpp', 'i18n', 'manage', 'routing', 'secure_cookie',
-# 'sessions', 'template', 'utils'
-
-app_test:
- $(call run_test,app)
-
-auth_test:
- $(call run_test,auth)
-
-config_test:
- $(call run_test,config)
-
-dev_test:
- $(call run_test,dev)
-
-ext_jinja2_test:
- $(call run_test,ext_jinja2)
-
-ext_mako_test:
- $(call run_test,ext_mako)
-
-gae_acl_test:
- $(call run_test,gae_acl)
-
-gae_blobstore_test:
- $(call run_test,gae_blobstore)
-
-gae_db_test:
- $(call run_test,gae_db)
-
-gae_mail_test:
- $(call run_test,gae_mail)
-
-gae_sharded_counter_test:
- $(call run_test,gae_sharded_counter)
-
-gae_taskqueue_test:
- $(call run_test,gae_taskqueue)
-
-gae_xmpp_test:
- $(call run_test,gae_xmpp)
-
-i18n_test:
- $(call run_test,i18n)
-
-manage_test:
- $(call run_test,manage)
-
-routing_test:
- $(call run_test,routing)
-
-secure_cookie_test:
- $(call run_test,secure_cookie)
-
-sessions_test:
- $(call run_test,sessions)
-
-template_test:
- $(call run_test,template)
-
-utils_test:
- $(call run_test,utils)
-
-c cov cove cover coverage:
- coverage erase
- for i in $(TESTS); \
- do \
- echo $$i; \
- PYTHONPATH=$(GAEPATH):. coverage run -p $$i; \
- done
- coverage combine
- coverage html $(NONTESTS)
- coverage report -m $(NONTESTS)
- echo "open file://`pwd`/htmlcov/index.html"
-
-serve:
- $(GAE)/dev_appserver.py . --port $(PORT) --address $(ADDRESS)
-
-debug:
- $(GAE)/dev_appserver.py . --port $(PORT) --address $(ADDRESS) --debug
-
-deploy:
- appcfg.py update .
-
-python:
- PYTHONPATH=$(GAEPATH):. $(PYTHON) -i startup.py
-
-python_raw:
- PYTHONPATH=$(GAEPATH):. $(PYTHON)
-
-zip:
- D=`pwd`; D=`basename $$D`; cd ..; rm $$D.zip; zip $$D.zip `hg st -c -m -a
-n -X $$D/.idea $$D`
-
-clean:
- rm -rf htmlcov
- rm -f `find . -name \*.pyc -o -name \*~ -o -name @* -o -name \*.orig`
=======================================
--- /manage/utils.py Sun Mar 20 05:39:46 2011
+++ /dev/null
@@ -1,31 +0,0 @@
-def get_unique_sequence(seq):
- seen = set()
- return [x for x in seq if x not in seen and not seen.add(x)]
-
-
-def import_string(import_name, silent=False):
- """Imports an object based on a string. If *silent* is True the return
- value will be None if the import fails.
-
- Simplified version of the function with same name from `Werkzeug`_. We
- duplicate it here because this file should not depend on external
packages.
-
- :param import_name:
- The dotted name for the object to import.
- :param silent:
- If True, import errors are ignored and None is returned instead.
- :returns:
- The imported object.
- """
- if isinstance(import_name, unicode):
- return import_name.encode('utf-8')
-
- try:
- if '.' in import_name:
- module, obj = import_name.rsplit('.', 1)
- return getattr(__import__(module, None, None, [obj]), obj)
- else:
- return __import__(import_name)
- except (ImportError, AttributeError):
- if not silent:
- raise
==============================================================================
Revision: 896b1c4c4201
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sun Apr 10 16:21:06 2011
Log: Added builder.close() to get_test_context().
http://code.google.com/p/tipfy/source/detail?r=896b1c4c4201
Modified:
/tipfy/app.py
=======================================
--- /tipfy/app.py Wed Apr 6 07:47:31 2011
+++ /tipfy/app.py Sun Apr 10 16:21:06 2011
@@ -384,7 +384,10 @@
"""
from werkzeug.test import EnvironBuilder
builder = EnvironBuilder(*args, **kwargs)
- return self.request_context_class(self, builder.get_environ())
+ try:
+ return self.request_context_class(self, builder.get_environ())
+ finally:
+ builder.close()
def get_test_handler(self, *args, **kwargs):
"""Returns a handler set as a current handler for testing purposes.
==============================================================================
Revision: 5b45741f4d04
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Fri Apr 15 06:52:18 2011
Log: Starting sessions review.
http://code.google.com/p/tipfy/source/detail?r=5b45741f4d04
Modified:
/tests/sessions_test.py
/tipfy/appengine/sessions.py
/tipfy/sessions.py
=======================================
--- /tests/sessions_test.py Sun Apr 3 07:14:04 2011
+++ /tests/sessions_test.py Fri Apr 15 06:52:18 2011
@@ -1,449 +1,56 @@
-from __future__ import with_statement
-
-import os
-import unittest
-
-from werkzeug import cached_property
-
-from tipfy.app import App, Request, Response
-from tipfy.handler import RequestHandler
-from tipfy.json import json_b64decode
-from tipfy.local import local
-from tipfy.routing import Rule
-from tipfy.sessions import (SecureCookieSession, SecureCookieStore,
- SessionMiddleware, SessionStore)
-from tipfy.appengine.sessions import (DatastoreSession, MemcacheSession,
- SessionModel)
+import datetime
+import functools
+import time
+
+from tipfy.sessions import SecureCookieSerializer
import test_utils
-class BaseHandler(RequestHandler):
- middleware = [SessionMiddleware()]
-
-
-class TestSessionStoreBase(test_utils.BaseTestCase):
- def _get_app(self):
- return App(config={
- 'tipfy.sessions': {
- 'secret_key': 'secret',
- }
- })
-
- def test_secure_cookie_store(self):
- with self._get_app().get_test_context() as request:
- store = request.session_store
- self.assertEqual(isinstance(store.secure_cookie_store,
SecureCookieStore), True)
-
- def test_secure_cookie_store_no_secret_key(self):
- with App().get_test_context() as request:
- store = request.session_store
- self.assertRaises(KeyError, getattr,
store, 'secure_cookie_store')
-
- def test_get_cookie_args(self):
- with self._get_app().get_test_context() as request:
- store = request.session_store
-
- self.assertEqual(store.get_cookie_args(), {
- 'max_age': None,
- 'domain': None,
- 'path': '/',
- 'secure': None,
- 'httponly': False,
- })
-
- self.assertEqual(store.get_cookie_args(max_age=86400,
domain='.foo.com'), {
- 'max_age': 86400,
- 'domain': '.foo.com',
- 'path': '/',
- 'secure': None,
- 'httponly': False,
- })
-
- def test_get_save_session(self):
- with self._get_app().get_test_context() as request:
- store = request.session_store
-
- session = store.get_session()
- self.assertEqual(isinstance(session, SecureCookieSession),
True)
- self.assertEqual(session, {})
-
- session['foo'] = 'bar'
-
- response = Response()
- store.save(response)
-
- with self._get_app().get_test_context('/',
headers={'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}) as
request:
- store = request.session_store
-
- session = store.get_session()
- self.assertEqual(isinstance(session, SecureCookieSession),
True)
- self.assertEqual(session, {'foo': 'bar'})
-
- def test_set_delete_cookie(self):
- with self._get_app().get_test_context() as request:
- store = request.session_store
-
- store.set_cookie('foo', 'bar')
- store.set_cookie('baz', 'ding')
-
- response = Response()
- store.save(response)
-
- headers =
{'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}
- with self._get_app().get_test_context('/', headers=headers) as
request:
- store = request.session_store
-
- self.assertEqual(request.cookies.get('foo'), 'bar')
- self.assertEqual(request.cookies.get('baz'), 'ding')
-
- store.delete_cookie('foo')
- store.save(response)
-
- headers =
{'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}
- with self._get_app().get_test_context('/', headers=headers) as
request:
- self.assertEqual(request.cookies.get('foo', None), '')
- self.assertEqual(request.cookies['baz'], 'ding')
-
- def test_set_cookie_encoded(self):
- with self._get_app().get_test_context() as request:
- store = request.session_store
-
- store.set_cookie('foo', 'bar', format='json')
- store.set_cookie('baz', 'ding', format='json')
-
- response = Response()
- store.save(response)
-
- headers =
{'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}
- with self._get_app().get_test_context('/', headers=headers) as
request:
- store = request.session_store
-
-
self.assertEqual(json_b64decode(request.cookies.get('foo')), 'bar')
-
self.assertEqual(json_b64decode(request.cookies.get('baz')), 'ding')
-
-
-class TestSessionStore(test_utils.BaseTestCase):
- def setUp(self):
- SessionStore.default_backends.update({
- 'datastore': DatastoreSession,
- 'memcache': MemcacheSession,
- 'securecookie': SecureCookieSession,
- })
- test_utils.BaseTestCase.setUp(self)
-
- def _get_app(self, *args, **kwargs):
- app = App(config={
- 'tipfy.sessions': {
- 'secret_key': 'secret',
- },
- })
- return app
-
- def test_set_session(self):
- class MyHandler(BaseHandler):
- def get(self):
- res = self.session.get('key')
- if not res:
- res = 'undefined'
- session = SecureCookieSession()
- session['key'] = 'a session value'
-
self.session_store.set_session(self.session_store.config['cookie_name'],
session)
-
- return Response(res)
-
- rules = [Rule('/', name='test', handler=MyHandler)]
-
- app = self._get_app('/')
- app.router.add(rules)
- client = app.get_test_client()
-
- response = client.get('/')
- self.assertEqual(response.data, 'undefined')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'a session value')
-
- def test_set_session_datastore(self):
- class MyHandler(BaseHandler):
- def get(self):
- session =
self.session_store.get_session(backend='datastore')
- res = session.get('key')
- if not res:
- res = 'undefined'
- session = DatastoreSession(None, 'a_random_session_id')
- session['key'] = 'a session value'
-
self.session_store.set_session(self.session_store.config['cookie_name'],
session, backend='datastore')
-
- return Response(res)
-
- rules = [Rule('/', name='test', handler=MyHandler)]
-
- app = self._get_app('/')
- app.router.add(rules)
- client = app.get_test_client()
-
- response = client.get('/')
- self.assertEqual(response.data, 'undefined')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'a session value')
-
- def test_get_memcache_session(self):
- class MyHandler(BaseHandler):
- def get(self):
- session =
self.session_store.get_session(backend='memcache')
- res = session.get('test')
- if not res:
- res = 'undefined'
- session['test'] = 'a memcache session value'
-
- return Response(res)
-
- rules = [Rule('/', name='test', handler=MyHandler)]
-
- app = self._get_app('/')
- app.router.add(rules)
- client = app.get_test_client()
-
- response = client.get('/')
- self.assertEqual(response.data, 'undefined')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'a memcache session value')
-
- def test_get_datastore_session(self):
- class MyHandler(BaseHandler):
- def get(self):
- session =
self.session_store.get_session(backend='datastore')
- res = session.get('test')
- if not res:
- res = 'undefined'
- session['test'] = 'a datastore session value'
-
- return Response(res)
-
- rules = [Rule('/', name='test', handler=MyHandler)]
-
- app = self._get_app('/')
- app.router.add(rules)
- client = app.get_test_client()
-
- response = client.get('/')
- self.assertEqual(response.data, 'undefined')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'a datastore session value')
-
- def test_set_delete_cookie(self):
- class MyHandler(BaseHandler):
- def get(self):
- res = self.request.cookies.get('test')
- if not res:
- res = 'undefined'
- self.session_store.set_cookie('test', 'a cookie value')
- else:
- self.session_store.delete_cookie('test')
-
- return Response(res)
-
- rules = [Rule('/', name='test', handler=MyHandler)]
-
- app = self._get_app('/')
- app.router.add(rules)
- client = app.get_test_client()
-
- response = client.get('/')
- self.assertEqual(response.data, 'undefined')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'a cookie value')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'undefined')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'a cookie value')
-
- def test_set_unset_cookie(self):
- class MyHandler(BaseHandler):
- def get(self):
- res = self.request.cookies.get('test')
- if not res:
- res = 'undefined'
- self.session_store.set_cookie('test', 'a cookie value')
-
- self.session_store.unset_cookie('test')
- return Response(res)
-
- rules = [Rule('/', name='test', handler=MyHandler)]
-
- app = self._get_app('/')
- app.router.add(rules)
- client = app.get_test_client()
-
- response = client.get('/')
- self.assertEqual(response.data, 'undefined')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'undefined')
-
- def test_set_get_secure_cookie(self):
- class MyHandler(BaseHandler):
- def get(self):
- response = Response()
-
- cookie = self.session_store.get_secure_cookie('test') or {}
- res = cookie.get('test')
- if not res:
- res = 'undefined'
- self.session_store.set_secure_cookie(response, 'test',
{'test': 'a secure cookie value'})
-
- response.data = res
- return response
-
- rules = [Rule('/', name='test', handler=MyHandler)]
-
- app = self._get_app('/')
- app.router.add(rules)
- client = app.get_test_client()
-
- response = client.get('/')
- self.assertEqual(response.data, 'undefined')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'a secure cookie value')
-
- def test_set_get_flashes(self):
- class MyHandler(BaseHandler):
- def get(self):
- res = [msg for msg, level in self.session.get_flashes()]
- if not res:
- res = [{'body': 'undefined'}]
- self.session.flash({'body': 'a flash value'})
-
- return Response(res[0]['body'])
-
- rules = [Rule('/', name='test', handler=MyHandler)]
-
- app = self._get_app('/')
- app.router.add(rules)
- client = app.get_test_client()
-
- response = client.get('/')
- self.assertEqual(response.data, 'undefined')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'a flash value')
-
- def test_set_get_messages(self):
- class MyHandler(BaseHandler):
- @cached_property
- def messages(self):
- """A list of status messages to be displayed to the
user."""
- messages = []
- flashes = self.session.get_flashes(key='_messages')
- for msg, level in flashes:
- msg['level'] = level
- messages.append(msg)
-
- return messages
-
- def set_message(self, level, body, title=None, life=None,
flash=False):
- """Adds a status message.
-
- :param level:
- Message level. Common values
are "success", "error", "info" or
- "alert".
- :param body:
- Message contents.
- :param title:
- Optional message title.
- :param life:
- Message life time in seconds. User interface can
implement
- a mechanism to make the message disappear after the
elapsed time.
- If not set, the message is permanent.
- :returns:
- None.
- """
- message = {'title': title, 'body': body, 'life': life}
- if flash is True:
- self.session.flash(message, level, '_messages')
- else:
- self.messages.append(message)
-
- def get(self):
- self.set_message('success', 'a normal message value')
- self.set_message('success', 'a flash message value',
flash=True)
- return Response('|'.join(msg['body'] for msg in
self.messages))
-
- rules = [Rule('/', name='test', handler=MyHandler)]
-
- app = self._get_app('/')
- app.router.add(rules)
- client = app.get_test_client()
-
- response = client.get('/')
- self.assertEqual(response.data, 'a normal message value')
-
- response = client.get('/', headers={
- 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
- })
- self.assertEqual(response.data, 'a flash message value|a normal
message value')
-
-
-class TestSessionModel(test_utils.BaseTestCase):
- def setUp(self):
- self.app = App()
- test_utils.BaseTestCase.setUp(self)
-
- def test_get_by_sid_without_cache(self):
- sid = 'test'
- entity = SessionModel.create(sid, {'foo': 'bar', 'baz': 'ding'})
- entity.put()
-
- cached_data = SessionModel.get_cache(sid)
- self.assertNotEqual(cached_data, None)
-
- entity.delete_cache()
- cached_data = SessionModel.get_cache(sid)
- self.assertEqual(cached_data, None)
-
- entity = SessionModel.get_by_sid(sid)
- self.assertNotEqual(entity, None)
-
- # Now will fetch cache.
- entity = SessionModel.get_by_sid(sid)
- self.assertNotEqual(entity, None)
-
- self.assertEqual('foo' in entity.data, True)
- self.assertEqual('baz' in entity.data, True)
- self.assertEqual(entity.data['foo'], 'bar')
- self.assertEqual(entity.data['baz'], 'ding')
-
- entity.delete()
- entity = SessionModel.get_by_sid(sid)
- self.assertEqual(entity, None)
+class SessionsTest(test_utils.BaseTestCase):
+ def test_secure_cookie_serializer(self):
+ def get_timestamp(*args):
+ d = datetime.datetime(*args)
+ return int(time.mktime(d.timetuple()))
+
+ value = ['a', 'b', 'c']
+ result = 'WyJhIiwiYiIsImMiXQ==|1293847200|
f7f95c30ba7cc2c1d49677e6b0eaf477504ad548'
+
+ serializer = SecureCookieSerializer('secret-key')
+ serializer.get_timestamp = functools.partial(get_timestamp, 2011,
1,
+ 1, 0, 0, 0)
+ # ok
+ rv = serializer.serialize('foo', value)
+ self.assertEqual(rv, result)
+
+ # ok
+ rv = serializer.deserialize('foo', result)
+ self.assertEqual(rv, value)
+
+ # no value
+ rv = serializer.deserialize('foo', None)
+ self.assertEqual(rv, None)
+
+ # not 3 parts
+ rv = serializer.deserialize('foo', 'a|b')
+ self.assertEqual(rv, None)
+
+ # bad signature
+ rv = serializer.deserialize('foo', result + 'foo')
+ self.assertEqual(rv, None)
+
+ # too old
+ serializer.get_timestamp = functools.partial(get_timestamp, 2011,
1,
+ 3, 0, 0, 0)
+ rv = serializer.deserialize('foo', result, max_age=86400)
+ self.assertEqual(rv, None)
+
+ # not correctly encoded
+ serializer2 = SecureCookieSerializer('foo')
+ serializer2.encode = lambda x: 'foo'
+ result2 = serializer2.serialize('foo', value)
+ rv2 = serializer2.deserialize('foo', result2)
+ self.assertEqual(rv2, None)
if __name__ == '__main__':
=======================================
--- /tipfy/appengine/sessions.py Wed Mar 30 05:57:11 2011
+++ /tipfy/appengine/sessions.py Fri Apr 15 06:52:18 2011
@@ -5,7 +5,7 @@
App Engine session backends.
- :copyright: 2011 by tipfy.org.
+ :copyright: 2010 by tipfy.org.
:license: BSD, see LICENSE.txt for more details.
"""
import re
@@ -14,8 +14,8 @@
from google.appengine.api import memcache
from google.appengine.ext import db
-from tipfy.sessions import BaseSession
-
+from tipfy.config import DEFAULT_VALUE
+from tipfy.sessions import BaseSessionFactory, SessionDict
from tipfy.appengine.db import (PickleProperty, get_protobuf_from_entity,
get_entity_from_protobuf)
@@ -101,76 +101,73 @@
db.delete(self)
-class AppEngineBaseSession(BaseSession):
- __slots__ = BaseSession.__slots__ + ('sid',)
-
- def __init__(self, data=None, sid=None, new=False):
- BaseSession.__init__(self, data, new)
- if new:
- self.sid = self.__class__._get_new_sid()
- elif sid is None:
- raise ValueError('A session id is required for existing
sessions.')
- else:
- self.sid = sid
-
- @classmethod
- def _get_new_sid(cls):
- # Force a namespace in the key, to not pollute the namespace in
case
- # global namespaces are in use.
- return cls.__module__ + '.' + cls.__name__ + '.' + uuid.uuid4().hex
-
- @classmethod
- def get_session(cls, store, name=None, **kwargs):
- if name:
- cookie = store.get_secure_cookie(name)
- if cookie is not None:
- sid = cookie.get('_sid')
- if sid and _is_valid_key(sid):
- return cls._get_by_sid(sid, **kwargs)
-
- return cls(new=True)
+class AppEngineSessionFactory(BaseSessionFactory):
+ session_class = SessionDict
+ sid = None
+
+ def get_session(self, max_age=DEFAULT_VALUE):
+ if self.session is None:
+ data = self.session_store.get_secure_cookie(self.name,
+ max_age=max_age)
+ if data is not None:
+ self.sid = data.get('_sid')
+ if _is_valid_key(self.sid):
+ self.session = self._get_by_sid(self.sid)
+
+ if self.session is None:
+ self.sid = self._get_new_sid()
+ self.session = self.session_class(self, new=True)
+
+ return self.session
+
+ def _get_new_sid(self):
+ return uuid.uuid4().hex
-class DatastoreSession(AppEngineBaseSession):
- """A session that stores data serialized in the datastore."""
+class DatastoreSessionFactory(AppEngineSessionFactory):
model_class = SessionModel
- @classmethod
- def _get_by_sid(cls, sid, **kwargs):
+ def _get_by_sid(self, sid):
"""Returns a session given a session id."""
- entity = cls.model_class.get_by_sid(sid)
+ entity = self.model_class.get_by_sid(sid)
if entity is not None:
- return cls(entity.data, sid)
-
- return cls(new=True)
-
- def save_session(self, response, store, name, **kwargs):
- if not self.modified:
+ return self.session_class(self, data=entity.data)
+
+ self.sid = self._get_new_sid()
+ return self.session_class(self, new=True)
+
+ def save_session(self, response):
+ if self.session is None or not self.session.modified:
return
- self.model_class.create(self.sid, dict(self)).put()
- store.set_secure_cookie(response, name, {'_sid': self.sid},
**kwargs)
+ self.model_class.create(self.sid, dict(self.session)).put()
+ self.session_store.set_secure_cookie(
+ response, self.name, {'_sid': self.sid}, **self.session_args)
-class MemcacheSession(AppEngineBaseSession):
+class MemcacheSessionFactory(AppEngineSessionFactory):
"""A session that stores data serialized in memcache."""
- @classmethod
- def _get_by_sid(cls, sid, **kwargs):
+ def _get_by_sid(self, sid):
"""Returns a session given a session id."""
data = memcache.get(sid)
if data is not None:
- return cls(data, sid)
-
- return cls(new=True)
-
- def save_session(self, response, store, name, **kwargs):
- if not self.modified:
+ return self.session_class(self, data=data)
+
+ self.sid = self._get_new_sid()
+ return self.session_class(self, new=True)
+
+ def save_session(self, response):
+ if self.session is None or not self.session.modified:
return
- memcache.set(self.sid, dict(self))
- store.set_secure_cookie(response, name, {'_sid': self.sid},
**kwargs)
+ memcache.set(self.sid, dict(self.session))
+ self.session_store.set_secure_cookie(
+ response, self.name, {'_sid': self.sid}, **self.session_args)
def _is_valid_key(key):
"""Check if a session key has the correct format."""
+ if not key:
+ return False
+
return _UUID_RE.match(key.split('.')[-1]) is not None
=======================================
--- /tipfy/sessions.py Sun Apr 3 07:14:04 2011
+++ /tipfy/sessions.py Fri Apr 15 06:52:18 2011
@@ -16,9 +16,9 @@
import time
from tipfy import APPENGINE, DEFAULT_VALUE, REQUIRED_VALUE
-from tipfy.utils import json_b64encode, json_b64decode
-
-from werkzeug import cached_property
+from tipfy.json import json_b64encode, json_b64decode
+
+from werkzeug.utils import cached_property
from werkzeug.contrib.sessions import ModificationTrackingDict
#: Default configuration values for this module. Keys are:
@@ -75,68 +75,13 @@
}
-class BaseSession(ModificationTrackingDict):
- __slots__ = ModificationTrackingDict.__slots__ + ('new',)
-
- def __init__(self, data=None, new=False):
- ModificationTrackingDict.__init__(self, data or ())
- self.new = new
-
- def get_flashes(self, key='_flash'):
- """Returns a flash message. Flash messages are deleted when first
read.
-
- :param key:
- Name of the flash key stored in the session. Default
is '_flash'.
- :returns:
- The data stored in the flash, or an empty list.
- """
- if key not in self:
- # Avoid popping if the key doesn't exist to not modify the
session.
- return []
-
- return self.pop(key, [])
-
- def add_flash(self, value, level=None, key='_flash'):
- """Adds a flash message. Flash messages are deleted when first
read.
-
- :param value:
- Value to be saved in the flash message.
- :param level:
- An optional level to set with the message. Default is `None`.
- :param key:
- Name of the flash key stored in the session. Default
is '_flash'.
- """
- self.setdefault(key, []).append((value, level))
-
- #: Alias, Flask-like interface.
- flash = add_flash
-
-
-class SecureCookieSession(BaseSession):
- """A session that stores data serialized in a signed cookie."""
- @classmethod
- def get_session(cls, store, name=None, **kwargs):
- if name:
- data = store.get_secure_cookie(name)
- if data is not None:
- return cls(data)
-
- return cls(new=True)
-
- def save_session(self, response, store, name, **kwargs):
- if not self.modified:
- return
-
- store.set_secure_cookie(response, name, dict(self), **kwargs)
-
-
-class SecureCookieStore(object):
- """Encapsulates getting and setting secure cookies.
+class SecureCookieSerializer(object):
+ """Serializes and deserializes secure cookie values.
Extracted from `Tornado`_ and modified.
"""
def __init__(self, secret_key):
- """Initilizes this secure cookie store.
+ """Initiliazes the serializer/deserializer.
:param secret_key:
A long, random sequence of bytes to be used as the HMAC secret
@@ -144,19 +89,34 @@
"""
self.secret_key = secret_key
- def get_cookie(self, request, name, max_age=None):
- """Returns the given signed cookie if it validates, or None.
-
- :param request:
- A :class:`tipfy.app.Request` object.
+ def serialize(self, name, value):
+ """Serializes a signed cookie value.
+
:param name:
Cookie name.
+ :param value:
+ Cookie value to be serialized.
+ :returns:
+ A serialized value ready to be stored in a cookie.
+ """
+ timestamp = str(self.get_timestamp())
+ value = self.encode(value)
+ signature = self._get_signature(name, value, timestamp)
+ return '|'.join([value, timestamp, signature])
+
+ def deserialize(self, name, value, max_age=None):
+ """Deserializes a signed cookie value.
+
+ :param name:
+ Cookie name.
+ :param value:
+ A cookie value to be deserialized.
:param max_age:
Maximum age in seconds for a valid cookie. If the cookie is
older
than this, returns None.
+ :returns:
+ The deserialized secure cookie, or None if it is not valid.
"""
- value = request.cookies.get(name)
-
if not value:
return
@@ -170,55 +130,33 @@
logging.warning('Invalid cookie signature %r', value)
return
- if max_age is not None and (int(parts[1]) < time.time() - max_age):
- logging.warning('Expired cookie %r', value)
- return
+ if max_age is not None:
+ if int(parts[1]) < self.get_timestamp() - max_age:
+ logging.warning('Expired cookie %r', value)
+ return
try:
- return json_b64decode(parts[0])
- except:
+ return self.decode(parts[0])
+ except Exception, e:
logging.warning('Cookie value failed to be decoded: %r',
parts[0])
- return
-
- def set_cookie(self, response, name, value, **kwargs):
- """Signs and timestamps a cookie so it cannot be forged.
-
- To read a cookie set with this method, use get_cookie().
-
- :param response:
- A :class:`tipfy.app.Response` instance.
- :param name:
- Cookie name.
- :param value:
- Cookie value.
- :param kwargs:
- Options to save the cookie.
See :meth:`SessionStore.get_session`.
- """
- response.set_cookie(name, self.get_signed_value(name, value),
**kwargs)
-
- def get_signed_value(self, name, value):
- """Returns a signed value for a cookie.
-
- :param name:
- Cookie name.
- :param value:
- Cookie value.
- :returns:
- An signed value using HMAC.
- """
- timestamp = str(int(time.time()))
- value = json_b64encode(value)
- signature = self._get_signature(name, value, timestamp)
- return '|'.join([value, timestamp, signature])
+
+ def encode(self, value):
+ return json_b64encode(value)
+
+ def decode(self, value):
+ return json_b64decode(value)
+
+ def get_timestamp(self):
+ return int(time.time())
def _get_signature(self, *parts):
- """Generated an HMAC signatures."""
- hash = hmac.new(self.secret_key, digestmod=hashlib.sha1)
- hash.update('|'.join(parts))
- return hash.hexdigest()
+ """Generates an HMAC signature."""
+ signature = hmac.new(self.secret_key, digestmod=hashlib.sha1)
+ signature.update('|'.join(parts))
+ return signature.hexdigest()
def _check_signature(self, a, b):
- """Checks if an HMAC signatures is valid."""
+ """Checks if an HMAC signature is valid."""
if len(a) != len(b):
return False
@@ -229,106 +167,125 @@
return result == 0
-class SessionStore(object):
- #: A dictionary with the default supported backends.
- default_backends = {
- 'securecookie': SecureCookieSession,
- }
-
- def __init__(self, request, backends=None):
- self.request = request
- # Base configuration.
- self.config = request.app.config[__name__]
- # A dictionary of support backend classes.
- self.backends = backends or self.default_backends
- # The default backend to use when none is provided.
- self.default_backend = self.config['default_backend']
- # Tracked sessions.
- self._sessions = {}
- # Tracked cookies.
- self._cookies = {}
-
- @cached_property
- def secure_cookie_store(self):
- """Factory for secure cookies.
-
+class SessionDict(ModificationTrackingDict):
+ __slots__ = ModificationTrackingDict.__slots__ + ('new',)
+
+ def __init__(self, data=None, new=False):
+ ModificationTrackingDict.__init__(self, data or ())
+ self.new = new
+
+ def get_flashes(self, key='_flash'):
+ """Returns a flash message. Flash messages are deleted when first
read.
+
+ :param key:
+ Name of the flash key stored in the session. Default
is '_flash'.
:returns:
- A :class:`SecureCookieStore` instance.
+ The data stored in the flash, or an empty list.
"""
- return SecureCookieStore(self.config['secret_key'])
-
- def get_session(self, key=None, backend=None, **kwargs):
- """Returns a session for a given key. If the session doesn't
exist, a
- new session is returned.
-
+ if key not in self:
+ # Avoid popping if the key doesn't exist to not modify the
session.
+ return []
+
+ return self.pop(key, [])
+
+ def add_flash(self, value, level=None, key='_flash'):
+ """Adds a flash message. Flash messages are deleted when first
read.
+
+ :param value:
+ Value to be saved in the flash message.
+ :param level:
+ An optional level to set with the message. Default is `None`.
:param key:
- Cookie name. If not provided, uses the ``cookie_name``
- value configured for this module.
- :param backend:
- Name of the session backend to be used. If not set, uses the
- default backend.
- :param kwargs:
- Options to set the session cookie. Keys are the same that can
be
- passed to ``Response.set_cookie``, and override the
``cookie_args``
- values configured for this module. If not set, use the
configured
- values.
- :returns:
- A dictionary-like session object.
+ Name of the flash key stored in the session. Default
is '_flash'.
"""
- key = key or self.config['cookie_name']
- backend = backend or self.default_backend
- sessions = self._sessions.setdefault(backend, {})
-
- if key not in sessions:
- kwargs = self.get_cookie_args(**kwargs)
- value = self.backends[backend].get_session(self, key, **kwargs)
- sessions[key] = (value, kwargs)
-
- return sessions[key][0]
-
- def set_session(self, key, value, backend=None, **kwargs):
- """Sets a session value. If a session with the same key exists, it
- will be overriden with the new value.
-
- :param key:
- Cookie name. See :meth:`get_session`.
- :param value:
- A dictionary of session values.
- :param backend:
- Name of the session backend. See :meth:`get_session`.
- :param kwargs:
- Options to save the cookie. See :meth:`get_session`.
- """
- assert isinstance(value, dict), 'Session value must be a dict.'
- backend = backend or self.default_backend
- sessions = self._sessions.setdefault(backend, {})
- session = self.backends[backend].get_session(self, **kwargs)
- session.update(value)
- kwargs = self.get_cookie_args(**kwargs)
- sessions[key] = (session, kwargs)
-
- def update_session_args(self, key, backend=None, **kwargs):
- """Updates the cookie options for a session.
-
- :param key:
- Cookie name. See :meth:`get_session`.
- :param backend:
- Name of the session backend. See :meth:`get_session`.
- :param kwargs:
- Options to save the cookie. See :meth:`get_session`.
+ self.setdefault(key, []).append((value, level))
+
+ #: Alias, Flask-like interface.
+ flash = add_flash
+
+
+class BaseSessionFactory(object):
+ def __init__(self, name, session_store):
+ self.name = name
+ self.session_store = session_store
+ self.session_args = session_store.config['cookie_args'].copy()
+ self.session = None
+
+
+class CookieSessionFactory(BaseSessionFactory):
+ """A session that stores data serialized in a ordinary cookie."""
+ def save_session(self, response):
+ if self.session is None:
+ path = self.session_args.get('path', '/')
+ domain = self.session_args.get('domain', None)
+ response.delete_cookie(self.name, path=path, domain=domain)
+ else:
+ response.set_cookie(self.name, self.session,
**self.session_args)
+
+
+class SecureCookieSessionFactory(BaseSessionFactory):
+ """A session that stores data serialized in a signed cookie."""
+ session_class = SessionDict
+
+ def get_session(self, max_age=DEFAULT_VALUE):
+ if self.session is None:
+ data = self.session_store.get_secure_cookie(self.name,
+ max_age=max_age)
+ new = data is None
+ self.session = self.session_class(self, data=data, new=new)
+
+ return self.session
+
+ def save_session(self, response):
+ if self.session is None or not self.session.modified:
+ return
+
+ self.session_store.save_secure_cookie(
+ response, self.name, dict(self.session), **self.session_args)
+
+
+class SessionStore(object):
+ def __init__(self, request):
+ self.request = request
+ # Base configuration.
+ self.config = request.app.config[__name__]
+ # Tracked sessions.
+ self.sessions = {}
+ # Serializer and deserializer for signed cookies.
+ self.cookie_serializer = SecureCookieSerializer(
+ self.config['secret_key'])
+
+ # Backend based sessions
--------------------------------------------------
+
+ def _get_session_container(self, name, factory):
+ if name not in self.sessions:
+ self.sessions[name] = factory(name, self)
+
+ return self.sessions[name]
+
+ def get_session(self, name=None, max_age=DEFAULT_VALUE,
+ factory=SecureCookieSessionFactory):
+ """Returns a session for a given name. If the session doesn't
exist, a
+ new session is returned.
+
+ :param name:
+ Cookie name. If not provided, uses the ``cookie_name``
+ value configured for this module.
:returns:
- True if the session was updated, False otherwise.
+ A dictionary-like session object.
"""
- backend = backend or self.default_backend
- sessions = self._sessions.setdefault(backend, {})
- if key in sessions:
- sessions[key][1].update(kwargs)
- return True
-
- return False
+ name = name or self.config['cookie_name']
+
+ if max_age is DEFAULT_VALUE:
+ max_age = self.config['session_max_age']
+
+ container = self._get_session_container(name, factory)
+ return container.get_session(max_age=max_age)
+
+ # Signed cookies
----------------------------------------------------------
def get_secure_cookie(self, name, max_age=DEFAULT_VALUE):
- """Returns a secure cookie from the request.
+ """Returns a deserialized secure cookie value.
:param name:
Cookie name.
@@ -341,14 +298,14 @@
if max_age is DEFAULT_VALUE:
max_age = self.config['session_max_age']
- return self.secure_cookie_store.get_cookie(self.request, name,
- max_age=max_age)
-
- def set_secure_cookie(self, response, name, value, **kwargs):
- """Sets a secure cookie in the response.
-
- :param response:
- A :class:`tipfy.app.Response` object.
+ value = self.request.cookies.get(name)
+ if value:
+ return self.cookie_serializer.deserialize(name, value,
+ max_age=max_age)
+
+ def set_secure_cookie(self, name, value, **kwargs):
+ """Sets a secure cookie to be saved.
+
:param name:
Cookie name.
:param value:
@@ -356,65 +313,148 @@
:param kwargs:
Options to save the cookie. See :meth:`get_session`.
"""
- assert isinstance(value, dict), 'Secure cookie value must be a
dict.'
- kwargs = self.get_cookie_args(**kwargs)
- self.secure_cookie_store.set_cookie(response, name, value,
**kwargs)
-
- def set_cookie(self, key, value, format=None, **kwargs):
- """Registers a cookie or secure cookie to be saved or deleted.
-
- :param key:
+ container = self._get_session_container(name,
+ SecureCookieSessionFactory)
+ container.session = value
+ container.session_args.update(kwargs)
+
+ # Ordinary cookies
--------------------------------------------------------
+
+ def get_cookie(self, name, decoder=json_b64decode):
+ """Returns a cookie from the request, decoding it.
+
+ :param name:
+ Cookie name.
+ :param decoder:
+ An decoder for the cookie value. Default is
+ func:`tipfy.json.json_b64decode`.
+ :returns:
+ A decoded cookie value, or None if a cookie with this name is
not
+ set or decoding failed.
+ """
+ value = self.request.cookies.get(name)
+ if value is not None and decoder:
+ try:
+ value = decoder(value)
+ except Exception, e:
+ return
+
+ return value
+
+ def set_cookie(self, name, value, format=None, encoder=json_b64encode,
+ **kwargs):
+ """Registers a cookie to be saved or deleted.
+
+ :param name:
Cookie name.
:param value:
Cookie value.
:param format:
If set to 'json', the value is serialized to JSON and encoded
to base64.
+
+ ..warning: Deprecated. Pass an encoder instead.
+ :param encoder:
+ An encoder for the cookie value. Default is
+ func:`tipfy.json.json_b64encode`.
:param kwargs:
Options to save the cookie. See :meth:`get_session`.
"""
- if format == 'json':
- value = json_b64encode(value)
-
- self._cookies[key] = (value, self.get_cookie_args(**kwargs))
-
- def unset_cookie(self, key):
- """Unsets a cookie previously set. This won't delete the cookie, it
- just won't be saved.
-
- :param key:
- Cookie name.
- """
- self._cookies.pop(key, None)
-
- def delete_cookie(self, key, **kwargs):
+ if format is not None:
+ from warnings import warn
+ warn(DeprecationWarning("SessionStore.set_cookie(): the "
+ "'format' argument is deprecated. Use 'encoder' instead
to "
+ "pass an encoder callable."))
+
+ if format == 'json':
+ value = json_b64encode(value)
+ elif encoder:
+ value = encoder(value)
+
+ container = self._get_session_container(name, CookieSessionFactory)
+ container.session = value
+ container.session_args.update(kwargs)
+
+ def delete_cookie(self, name, **kwargs):
"""Registers a cookie or secure cookie to be deleted.
- :param key:
+ :param name:
Cookie name.
:param kwargs:
Options to delete the cookie. See :meth:`get_session`.
"""
- self._cookies[key] = (None, self.get_cookie_args(**kwargs))
-
- def save(self, response):
+ self.set_cookie(name, None, **kwargs)
+
+ def unset_cookie(self, name):
+ """Unsets a cookie previously set. This won't delete the cookie, it
+ just won't be saved.
+
+ :param name:
+ Cookie name.
+ """
+ self.sessions.pop(name, None)
+
+ # Saving to a response object
---------------------------------------------
+
+ def save_sessions(self, response):
"""Saves all cookies and sessions to a response object.
:param response:
- A ``tipfy.Response`` object.
+ A ``tipfy.app.Response`` object.
"""
- if self._cookies:
- for key, (value, kwargs) in self._cookies.iteritems():
- if value is None:
- response.delete_cookie(key,
path=kwargs.get('path', '/'),
- domain=kwargs.get('domain', None))
- else:
- response.set_cookie(key, value, **kwargs)
-
- if self._sessions:
- for sessions in self._sessions.values():
- for key, (value, kwargs) in sessions.iteritems():
- value.save_session(response, self, key, **kwargs)
+ for session in self.sessions.values():
+ session.save_session(response)
+ # Old name
+ save = save_sessions
+
+ def save_secure_cookie(self, response, name, value, **kwargs):
+ value = self.cookie_serializer.serialize(name, value)
+ response.set_cookie(name, value, **kwargs)
+
+ # Deprecated methods
------------------------------------------------------
+
+ def set_session(self, name, value, backend=None, **kwargs):
+ """Sets a session value. If a session with the same name exists, it
+ will be overriden with the new value.
+
+ :param name:
+ Cookie name. See :meth:`get_session`.
+ :param value:
+ A dictionary of session values.
+ :param backend:
+ Name of the session backend. See :meth:`get_session`.
+ :param kwargs:
+ Options to save the cookie. See :meth:`get_session`.
+ """
+ from warnings import warn
+ warn(DeprecationWarning("SessionStore.set_session(): this "
+ "method is deprecated. Cookie arguments can be set directly
in "
+ "a session."))
+
+ self.set_secure_cookie(name, value, **kwargs)
+
+ def update_session_args(self, name, backend=None, **kwargs):
+ """Updates the cookie options for a session.
+
+ :param name:
+ Cookie name. See :meth:`get_session`.
+ :param backend:
+ Name of the session backend. See :meth:`get_session`.
+ :param kwargs:
+ Options to save the cookie. See :meth:`get_session`.
+ :returns:
+ True if the session was updated, False otherwise.
+ """
+ from warnings import warn
+ warn(DeprecationWarning("SessionStore.update_session_args(): this "
+ "method is deprecated. Cookie arguments can be set directly
in "
+ "a session."))
+
+ if name in self.sessions:
+ self.sessions[name].session_args.update(kwargs)
+ return True
+
+ return False
def get_cookie_args(self, **kwargs):
"""Returns a copy of the default cookie configuration updated with
the
@@ -425,6 +465,11 @@
:returns:
A dictionary with arguments for the session cookie.
"""
+ from warnings import warn
+ warn(DeprecationWarning("SessionStore.get_cookie_args(): this "
+ "method is deprecated. Cookie arguments can be set directly
in "
+ "a session."))
+
_kwargs = self.config['cookie_args'].copy()
_kwargs.update(kwargs)
return _kwargs
@@ -444,11 +489,3 @@
"""
handler.session_store.save(response)
return response
-
-
-if APPENGINE:
- from tipfy.appengine.sessions import DatastoreSession, MemcacheSession
- SessionStore.default_backends.update({
- 'datastore': DatastoreSession,
- 'memcache': MemcacheSession,
- })
==============================================================================
Revision: 3f96e9cc186c
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sun Apr 17 06:19:50 2011
Log: Reverted sessions.
http://code.google.com/p/tipfy/source/detail?r=3f96e9cc186c
Modified:
/tests/sessions_test.py
/tipfy/appengine/sessions.py
/tipfy/sessions.py
=======================================
--- /tests/sessions_test.py Fri Apr 15 06:52:18 2011
+++ /tests/sessions_test.py Sun Apr 17 06:19:50 2011
@@ -1,56 +1,449 @@
-import datetime
-import functools
-import time
-
-from tipfy.sessions import SecureCookieSerializer
+from __future__ import with_statement
+
+import os
+import unittest
+
+from werkzeug import cached_property
+
+from tipfy.app import App, Request, Response
+from tipfy.handler import RequestHandler
+from tipfy.json import json_b64decode
+from tipfy.local import local
+from tipfy.routing import Rule
+from tipfy.sessions import (SecureCookieSession, SecureCookieStore,
+ SessionMiddleware, SessionStore)
+from tipfy.appengine.sessions import (DatastoreSession, MemcacheSession,
+ SessionModel)
import test_utils
-class SessionsTest(test_utils.BaseTestCase):
- def test_secure_cookie_serializer(self):
- def get_timestamp(*args):
- d = datetime.datetime(*args)
- return int(time.mktime(d.timetuple()))
-
- value = ['a', 'b', 'c']
- result = 'WyJhIiwiYiIsImMiXQ==|1293847200|
f7f95c30ba7cc2c1d49677e6b0eaf477504ad548'
-
- serializer = SecureCookieSerializer('secret-key')
- serializer.get_timestamp = functools.partial(get_timestamp, 2011,
1,
- 1, 0, 0, 0)
- # ok
- rv = serializer.serialize('foo', value)
- self.assertEqual(rv, result)
-
- # ok
- rv = serializer.deserialize('foo', result)
- self.assertEqual(rv, value)
-
- # no value
- rv = serializer.deserialize('foo', None)
- self.assertEqual(rv, None)
-
- # not 3 parts
- rv = serializer.deserialize('foo', 'a|b')
- self.assertEqual(rv, None)
-
- # bad signature
- rv = serializer.deserialize('foo', result + 'foo')
- self.assertEqual(rv, None)
-
- # too old
- serializer.get_timestamp = functools.partial(get_timestamp, 2011,
1,
- 3, 0, 0, 0)
- rv = serializer.deserialize('foo', result, max_age=86400)
- self.assertEqual(rv, None)
-
- # not correctly encoded
- serializer2 = SecureCookieSerializer('foo')
- serializer2.encode = lambda x: 'foo'
- result2 = serializer2.serialize('foo', value)
- rv2 = serializer2.deserialize('foo', result2)
- self.assertEqual(rv2, None)
+class BaseHandler(RequestHandler):
+ middleware = [SessionMiddleware()]
+
+
+class TestSessionStoreBase(test_utils.BaseTestCase):
+ def _get_app(self):
+ return App(config={
+ 'tipfy.sessions': {
+ 'secret_key': 'secret',
+ }
+ })
+
+ def test_secure_cookie_store(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+ self.assertEqual(isinstance(store.secure_cookie_store,
SecureCookieStore), True)
+
+ def test_secure_cookie_store_no_secret_key(self):
+ with App().get_test_context() as request:
+ store = request.session_store
+ self.assertRaises(KeyError, getattr,
store, 'secure_cookie_store')
+
+ def test_get_cookie_args(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+
+ self.assertEqual(store.get_cookie_args(), {
+ 'max_age': None,
+ 'domain': None,
+ 'path': '/',
+ 'secure': None,
+ 'httponly': False,
+ })
+
+ self.assertEqual(store.get_cookie_args(max_age=86400,
domain='.foo.com'), {
+ 'max_age': 86400,
+ 'domain': '.foo.com',
+ 'path': '/',
+ 'secure': None,
+ 'httponly': False,
+ })
+
+ def test_get_save_session(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+
+ session = store.get_session()
+ self.assertEqual(isinstance(session, SecureCookieSession),
True)
+ self.assertEqual(session, {})
+
+ session['foo'] = 'bar'
+
+ response = Response()
+ store.save(response)
+
+ with self._get_app().get_test_context('/',
headers={'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}) as
request:
+ store = request.session_store
+
+ session = store.get_session()
+ self.assertEqual(isinstance(session, SecureCookieSession),
True)
+ self.assertEqual(session, {'foo': 'bar'})
+
+ def test_set_delete_cookie(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+
+ store.set_cookie('foo', 'bar')
+ store.set_cookie('baz', 'ding')
+
+ response = Response()
+ store.save(response)
+
+ headers =
{'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}
+ with self._get_app().get_test_context('/', headers=headers) as
request:
+ store = request.session_store
+
+ self.assertEqual(request.cookies.get('foo'), 'bar')
+ self.assertEqual(request.cookies.get('baz'), 'ding')
+
+ store.delete_cookie('foo')
+ store.save(response)
+
+ headers =
{'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}
+ with self._get_app().get_test_context('/', headers=headers) as
request:
+ self.assertEqual(request.cookies.get('foo', None), '')
+ self.assertEqual(request.cookies['baz'], 'ding')
+
+ def test_set_cookie_encoded(self):
+ with self._get_app().get_test_context() as request:
+ store = request.session_store
+
+ store.set_cookie('foo', 'bar', format='json')
+ store.set_cookie('baz', 'ding', format='json')
+
+ response = Response()
+ store.save(response)
+
+ headers =
{'Cookie': '\n'.join(response.headers.getlist('Set-Cookie'))}
+ with self._get_app().get_test_context('/', headers=headers) as
request:
+ store = request.session_store
+
+
self.assertEqual(json_b64decode(request.cookies.get('foo')), 'bar')
+
self.assertEqual(json_b64decode(request.cookies.get('baz')), 'ding')
+
+
+class TestSessionStore(test_utils.BaseTestCase):
+ def setUp(self):
+ SessionStore.default_backends.update({
+ 'datastore': DatastoreSession,
+ 'memcache': MemcacheSession,
+ 'securecookie': SecureCookieSession,
+ })
+ test_utils.BaseTestCase.setUp(self)
+
+ def _get_app(self, *args, **kwargs):
+ app = App(config={
+ 'tipfy.sessions': {
+ 'secret_key': 'secret',
+ },
+ })
+ return app
+
+ def test_set_session(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ res = self.session.get('key')
+ if not res:
+ res = 'undefined'
+ session = SecureCookieSession()
+ session['key'] = 'a session value'
+
self.session_store.set_session(self.session_store.config['cookie_name'],
session)
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a session value')
+
+ def test_set_session_datastore(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ session =
self.session_store.get_session(backend='datastore')
+ res = session.get('key')
+ if not res:
+ res = 'undefined'
+ session = DatastoreSession(None, 'a_random_session_id')
+ session['key'] = 'a session value'
+
self.session_store.set_session(self.session_store.config['cookie_name'],
session, backend='datastore')
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a session value')
+
+ def test_get_memcache_session(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ session =
self.session_store.get_session(backend='memcache')
+ res = session.get('test')
+ if not res:
+ res = 'undefined'
+ session['test'] = 'a memcache session value'
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a memcache session value')
+
+ def test_get_datastore_session(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ session =
self.session_store.get_session(backend='datastore')
+ res = session.get('test')
+ if not res:
+ res = 'undefined'
+ session['test'] = 'a datastore session value'
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a datastore session value')
+
+ def test_set_delete_cookie(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ res = self.request.cookies.get('test')
+ if not res:
+ res = 'undefined'
+ self.session_store.set_cookie('test', 'a cookie value')
+ else:
+ self.session_store.delete_cookie('test')
+
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a cookie value')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a cookie value')
+
+ def test_set_unset_cookie(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ res = self.request.cookies.get('test')
+ if not res:
+ res = 'undefined'
+ self.session_store.set_cookie('test', 'a cookie value')
+
+ self.session_store.unset_cookie('test')
+ return Response(res)
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'undefined')
+
+ def test_set_get_secure_cookie(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ response = Response()
+
+ cookie = self.session_store.get_secure_cookie('test') or {}
+ res = cookie.get('test')
+ if not res:
+ res = 'undefined'
+ self.session_store.set_secure_cookie(response, 'test',
{'test': 'a secure cookie value'})
+
+ response.data = res
+ return response
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a secure cookie value')
+
+ def test_set_get_flashes(self):
+ class MyHandler(BaseHandler):
+ def get(self):
+ res = [msg for msg, level in self.session.get_flashes()]
+ if not res:
+ res = [{'body': 'undefined'}]
+ self.session.flash({'body': 'a flash value'})
+
+ return Response(res[0]['body'])
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'undefined')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a flash value')
+
+ def test_set_get_messages(self):
+ class MyHandler(BaseHandler):
+ @cached_property
+ def messages(self):
+ """A list of status messages to be displayed to the
user."""
+ messages = []
+ flashes = self.session.get_flashes(key='_messages')
+ for msg, level in flashes:
+ msg['level'] = level
+ messages.append(msg)
+
+ return messages
+
+ def set_message(self, level, body, title=None, life=None,
flash=False):
+ """Adds a status message.
+
+ :param level:
+ Message level. Common values
are "success", "error", "info" or
+ "alert".
+ :param body:
+ Message contents.
+ :param title:
+ Optional message title.
+ :param life:
+ Message life time in seconds. User interface can
implement
+ a mechanism to make the message disappear after the
elapsed time.
+ If not set, the message is permanent.
+ :returns:
+ None.
+ """
+ message = {'title': title, 'body': body, 'life': life}
+ if flash is True:
+ self.session.flash(message, level, '_messages')
+ else:
+ self.messages.append(message)
+
+ def get(self):
+ self.set_message('success', 'a normal message value')
+ self.set_message('success', 'a flash message value',
flash=True)
+ return Response('|'.join(msg['body'] for msg in
self.messages))
+
+ rules = [Rule('/', name='test', handler=MyHandler)]
+
+ app = self._get_app('/')
+ app.router.add(rules)
+ client = app.get_test_client()
+
+ response = client.get('/')
+ self.assertEqual(response.data, 'a normal message value')
+
+ response = client.get('/', headers={
+ 'Cookie': '\n'.join(response.headers.getlist('Set-Cookie')),
+ })
+ self.assertEqual(response.data, 'a flash message value|a normal
message value')
+
+
+class TestSessionModel(test_utils.BaseTestCase):
+ def setUp(self):
+ self.app = App()
+ test_utils.BaseTestCase.setUp(self)
+
+ def test_get_by_sid_without_cache(self):
+ sid = 'test'
+ entity = SessionModel.create(sid, {'foo': 'bar', 'baz': 'ding'})
+ entity.put()
+
+ cached_data = SessionModel.get_cache(sid)
+ self.assertNotEqual(cached_data, None)
+
+ entity.delete_cache()
+ cached_data = SessionModel.get_cache(sid)
+ self.assertEqual(cached_data, None)
+
+ entity = SessionModel.get_by_sid(sid)
+ self.assertNotEqual(entity, None)
+
+ # Now will fetch cache.
+ entity = SessionModel.get_by_sid(sid)
+ self.assertNotEqual(entity, None)
+
+ self.assertEqual('foo' in entity.data, True)
+ self.assertEqual('baz' in entity.data, True)
+ self.assertEqual(entity.data['foo'], 'bar')
+ self.assertEqual(entity.data['baz'], 'ding')
+
+ entity.delete()
+ entity = SessionModel.get_by_sid(sid)
+ self.assertEqual(entity, None)
if __name__ == '__main__':
=======================================
--- /tipfy/appengine/sessions.py Fri Apr 15 06:52:18 2011
+++ /tipfy/appengine/sessions.py Sun Apr 17 06:19:50 2011
@@ -5,7 +5,7 @@
App Engine session backends.
- :copyright: 2010 by tipfy.org.
+ :copyright: 2011 by tipfy.org.
:license: BSD, see LICENSE.txt for more details.
"""
import re
@@ -14,8 +14,8 @@
from google.appengine.api import memcache
from google.appengine.ext import db
-from tipfy.config import DEFAULT_VALUE
-from tipfy.sessions import BaseSessionFactory, SessionDict
+from tipfy.sessions import BaseSession
+
from tipfy.appengine.db import (PickleProperty, get_protobuf_from_entity,
get_entity_from_protobuf)
@@ -101,73 +101,76 @@
db.delete(self)
-class AppEngineSessionFactory(BaseSessionFactory):
- session_class = SessionDict
- sid = None
-
- def get_session(self, max_age=DEFAULT_VALUE):
- if self.session is None:
- data = self.session_store.get_secure_cookie(self.name,
- max_age=max_age)
- if data is not None:
- self.sid = data.get('_sid')
- if _is_valid_key(self.sid):
- self.session = self._get_by_sid(self.sid)
-
- if self.session is None:
- self.sid = self._get_new_sid()
- self.session = self.session_class(self, new=True)
-
- return self.session
-
- def _get_new_sid(self):
- return uuid.uuid4().hex
+class AppEngineBaseSession(BaseSession):
+ __slots__ = BaseSession.__slots__ + ('sid',)
+
+ def __init__(self, data=None, sid=None, new=False):
+ BaseSession.__init__(self, data, new)
+ if new:
+ self.sid = self.__class__._get_new_sid()
+ elif sid is None:
+ raise ValueError('A session id is required for existing
sessions.')
+ else:
+ self.sid = sid
+
+ @classmethod
+ def _get_new_sid(cls):
+ # Force a namespace in the key, to not pollute the namespace in
case
+ # global namespaces are in use.
+ return cls.__module__ + '.' + cls.__name__ + '.' + uuid.uuid4().hex
+
+ @classmethod
+ def get_session(cls, store, name=None, **kwargs):
+ if name:
+ cookie = store.get_secure_cookie(name)
+ if cookie is not None:
+ sid = cookie.get('_sid')
+ if sid and _is_valid_key(sid):
+ return cls._get_by_sid(sid, **kwargs)
+
+ return cls(new=True)
-class DatastoreSessionFactory(AppEngineSessionFactory):
+class DatastoreSession(AppEngineBaseSession):
+ """A session that stores data serialized in the datastore."""
model_class = SessionModel
- def _get_by_sid(self, sid):
+ @classmethod
+ def _get_by_sid(cls, sid, **kwargs):
"""Returns a session given a session id."""
- entity = self.model_class.get_by_sid(sid)
+ entity = cls.model_class.get_by_sid(sid)
if entity is not None:
- return self.session_class(self, data=entity.data)
-
- self.sid = self._get_new_sid()
- return self.session_class(self, new=True)
-
- def save_session(self, response):
- if self.session is None or not self.session.modified:
+ return cls(entity.data, sid)
+
+ return cls(new=True)
+
+ def save_session(self, response, store, name, **kwargs):
+ if not self.modified:
return
- self.model_class.create(self.sid, dict(self.session)).put()
- self.session_store.set_secure_cookie(
- response, self.name, {'_sid': self.sid}, **self.session_args)
+ self.model_class.create(self.sid, dict(self)).put()
+ store.set_secure_cookie(response, name, {'_sid': self.sid},
**kwargs)
-class MemcacheSessionFactory(AppEngineSessionFactory):
+class MemcacheSession(AppEngineBaseSession):
"""A session that stores data serialized in memcache."""
- def _get_by_sid(self, sid):
+ @classmethod
+ def _get_by_sid(cls, sid, **kwargs):
"""Returns a session given a session id."""
data = memcache.get(sid)
if data is not None:
- return self.session_class(self, data=data)
-
- self.sid = self._get_new_sid()
- return self.session_class(self, new=True)
-
- def save_session(self, response):
- if self.session is None or not self.session.modified:
+ return cls(data, sid)
+
+ return cls(new=True)
+
+ def save_session(self, response, store, name, **kwargs):
+ if not self.modified:
return
- memcache.set(self.sid, dict(self.session))
- self.session_store.set_secure_cookie(
- response, self.name, {'_sid': self.sid}, **self.session_args)
+ memcache.set(self.sid, dict(self))
+ store.set_secure_cookie(response, name, {'_sid': self.sid},
**kwargs)
def _is_valid_key(key):
"""Check if a session key has the correct format."""
- if not key:
- return False
-
return _UUID_RE.match(key.split('.')[-1]) is not None
=======================================
--- /tipfy/sessions.py Fri Apr 15 06:52:18 2011
+++ /tipfy/sessions.py Sun Apr 17 06:19:50 2011
@@ -16,9 +16,9 @@
import time
from tipfy import APPENGINE, DEFAULT_VALUE, REQUIRED_VALUE
-from tipfy.json import json_b64encode, json_b64decode
-
-from werkzeug.utils import cached_property
+from tipfy.utils import json_b64encode, json_b64decode
+
+from werkzeug import cached_property
from werkzeug.contrib.sessions import ModificationTrackingDict
#: Default configuration values for this module. Keys are:
@@ -75,13 +75,68 @@
}
-class SecureCookieSerializer(object):
- """Serializes and deserializes secure cookie values.
+class BaseSession(ModificationTrackingDict):
+ __slots__ = ModificationTrackingDict.__slots__ + ('new',)
+
+ def __init__(self, data=None, new=False):
+ ModificationTrackingDict.__init__(self, data or ())
+ self.new = new
+
+ def get_flashes(self, key='_flash'):
+ """Returns a flash message. Flash messages are deleted when first
read.
+
+ :param key:
+ Name of the flash key stored in the session. Default
is '_flash'.
+ :returns:
+ The data stored in the flash, or an empty list.
+ """
+ if key not in self:
+ # Avoid popping if the key doesn't exist to not modify the
session.
+ return []
+
+ return self.pop(key, [])
+
+ def add_flash(self, value, level=None, key='_flash'):
+ """Adds a flash message. Flash messages are deleted when first
read.
+
+ :param value:
+ Value to be saved in the flash message.
+ :param level:
+ An optional level to set with the message. Default is `None`.
+ :param key:
+ Name of the flash key stored in the session. Default
is '_flash'.
+ """
+ self.setdefault(key, []).append((value, level))
+
+ #: Alias, Flask-like interface.
+ flash = add_flash
+
+
+class SecureCookieSession(BaseSession):
+ """A session that stores data serialized in a signed cookie."""
+ @classmethod
+ def get_session(cls, store, name=None, **kwargs):
+ if name:
+ data = store.get_secure_cookie(name)
+ if data is not None:
+ return cls(data)
+
+ return cls(new=True)
+
+ def save_session(self, response, store, name, **kwargs):
+ if not self.modified:
+ return
+
+ store.set_secure_cookie(response, name, dict(self), **kwargs)
+
+
+class SecureCookieStore(object):
+ """Encapsulates getting and setting secure cookies.
Extracted from `Tornado`_ and modified.
"""
def __init__(self, secret_key):
- """Initiliazes the serializer/deserializer.
+ """Initilizes this secure cookie store.
:param secret_key:
A long, random sequence of bytes to be used as the HMAC secret
@@ -89,34 +144,19 @@
"""
self.secret_key = secret_key
- def serialize(self, name, value):
- """Serializes a signed cookie value.
-
+ def get_cookie(self, request, name, max_age=None):
+ """Returns the given signed cookie if it validates, or None.
+
+ :param request:
+ A :class:`tipfy.app.Request` object.
:param name:
Cookie name.
- :param value:
- Cookie value to be serialized.
- :returns:
- A serialized value ready to be stored in a cookie.
- """
- timestamp = str(self.get_timestamp())
- value = self.encode(value)
- signature = self._get_signature(name, value, timestamp)
- return '|'.join([value, timestamp, signature])
-
- def deserialize(self, name, value, max_age=None):
- """Deserializes a signed cookie value.
-
- :param name:
- Cookie name.
- :param value:
- A cookie value to be deserialized.
:param max_age:
Maximum age in seconds for a valid cookie. If the cookie is
older
than this, returns None.
- :returns:
- The deserialized secure cookie, or None if it is not valid.
"""
+ value = request.cookies.get(name)
+
if not value:
return
@@ -130,33 +170,55 @@
logging.warning('Invalid cookie signature %r', value)
return
- if max_age is not None:
- if int(parts[1]) < self.get_timestamp() - max_age:
- logging.warning('Expired cookie %r', value)
- return
+ if max_age is not None and (int(parts[1]) < time.time() - max_age):
+ logging.warning('Expired cookie %r', value)
+ return
try:
- return self.decode(parts[0])
- except Exception, e:
+ return json_b64decode(parts[0])
+ except:
logging.warning('Cookie value failed to be decoded: %r',
parts[0])
-
- def encode(self, value):
- return json_b64encode(value)
-
- def decode(self, value):
- return json_b64decode(value)
-
- def get_timestamp(self):
- return int(time.time())
+ return
+
+ def set_cookie(self, response, name, value, **kwargs):
+ """Signs and timestamps a cookie so it cannot be forged.
+
+ To read a cookie set with this method, use get_cookie().
+
+ :param response:
+ A :class:`tipfy.app.Response` instance.
+ :param name:
+ Cookie name.
+ :param value:
+ Cookie value.
+ :param kwargs:
+ Options to save the cookie.
See :meth:`SessionStore.get_session`.
+ """
+ response.set_cookie(name, self.get_signed_value(name, value),
**kwargs)
+
+ def get_signed_value(self, name, value):
+ """Returns a signed value for a cookie.
+
+ :param name:
+ Cookie name.
+ :param value:
+ Cookie value.
+ :returns:
+ An signed value using HMAC.
+ """
+ timestamp = str(int(time.time()))
+ value = json_b64encode(value)
+ signature = self._get_signature(name, value, timestamp)
+ return '|'.join([value, timestamp, signature])
def _get_signature(self, *parts):
- """Generates an HMAC signature."""
- signature = hmac.new(self.secret_key, digestmod=hashlib.sha1)
- signature.update('|'.join(parts))
- return signature.hexdigest()
+ """Generated an HMAC signatures."""
+ hash = hmac.new(self.secret_key, digestmod=hashlib.sha1)
+ hash.update('|'.join(parts))
+ return hash.hexdigest()
def _check_signature(self, a, b):
- """Checks if an HMAC signature is valid."""
+ """Checks if an HMAC signatures is valid."""
if len(a) != len(b):
return False
@@ -167,125 +229,106 @@
return result == 0
-class SessionDict(ModificationTrackingDict):
- __slots__ = ModificationTrackingDict.__slots__ + ('new',)
-
- def __init__(self, data=None, new=False):
- ModificationTrackingDict.__init__(self, data or ())
- self.new = new
-
- def get_flashes(self, key='_flash'):
- """Returns a flash message. Flash messages are deleted when first
read.
-
- :param key:
- Name of the flash key stored in the session. Default
is '_flash'.
- :returns:
- The data stored in the flash, or an empty list.
- """
- if key not in self:
- # Avoid popping if the key doesn't exist to not modify the
session.
- return []
-
- return self.pop(key, [])
-
- def add_flash(self, value, level=None, key='_flash'):
- """Adds a flash message. Flash messages are deleted when first
read.
-
- :param value:
- Value to be saved in the flash message.
- :param level:
- An optional level to set with the message. Default is `None`.
- :param key:
- Name of the flash key stored in the session. Default
is '_flash'.
- """
- self.setdefault(key, []).append((value, level))
-
- #: Alias, Flask-like interface.
- flash = add_flash
-
-
-class BaseSessionFactory(object):
- def __init__(self, name, session_store):
- self.name = name
- self.session_store = session_store
- self.session_args = session_store.config['cookie_args'].copy()
- self.session = None
-
-
-class CookieSessionFactory(BaseSessionFactory):
- """A session that stores data serialized in a ordinary cookie."""
- def save_session(self, response):
- if self.session is None:
- path = self.session_args.get('path', '/')
- domain = self.session_args.get('domain', None)
- response.delete_cookie(self.name, path=path, domain=domain)
- else:
- response.set_cookie(self.name, self.session,
**self.session_args)
-
-
-class SecureCookieSessionFactory(BaseSessionFactory):
- """A session that stores data serialized in a signed cookie."""
- session_class = SessionDict
-
- def get_session(self, max_age=DEFAULT_VALUE):
- if self.session is None:
- data = self.session_store.get_secure_cookie(self.name,
- max_age=max_age)
- new = data is None
- self.session = self.session_class(self, data=data, new=new)
-
- return self.session
-
- def save_session(self, response):
- if self.session is None or not self.session.modified:
- return
-
- self.session_store.save_secure_cookie(
- response, self.name, dict(self.session), **self.session_args)
-
-
class SessionStore(object):
- def __init__(self, request):
+ #: A dictionary with the default supported backends.
+ default_backends = {
+ 'securecookie': SecureCookieSession,
+ }
+
+ def __init__(self, request, backends=None):
self.request = request
# Base configuration.
self.config = request.app.config[__name__]
+ # A dictionary of support backend classes.
+ self.backends = backends or self.default_backends
+ # The default backend to use when none is provided.
+ self.default_backend = self.config['default_backend']
# Tracked sessions.
- self.sessions = {}
- # Serializer and deserializer for signed cookies.
- self.cookie_serializer = SecureCookieSerializer(
- self.config['secret_key'])
-
- # Backend based sessions
--------------------------------------------------
-
- def _get_session_container(self, name, factory):
- if name not in self.sessions:
- self.sessions[name] = factory(name, self)
-
- return self.sessions[name]
-
- def get_session(self, name=None, max_age=DEFAULT_VALUE,
- factory=SecureCookieSessionFactory):
- """Returns a session for a given name. If the session doesn't
exist, a
+ self._sessions = {}
+ # Tracked cookies.
+ self._cookies = {}
+
+ @cached_property
+ def secure_cookie_store(self):
+ """Factory for secure cookies.
+
+ :returns:
+ A :class:`SecureCookieStore` instance.
+ """
+ return SecureCookieStore(self.config['secret_key'])
+
+ def get_session(self, key=None, backend=None, **kwargs):
+ """Returns a session for a given key. If the session doesn't
exist, a
new session is returned.
- :param name:
+ :param key:
Cookie name. If not provided, uses the ``cookie_name``
value configured for this module.
+ :param backend:
+ Name of the session backend to be used. If not set, uses the
+ default backend.
+ :param kwargs:
+ Options to set the session cookie. Keys are the same that can
be
+ passed to ``Response.set_cookie``, and override the
``cookie_args``
+ values configured for this module. If not set, use the
configured
+ values.
:returns:
A dictionary-like session object.
"""
- name = name or self.config['cookie_name']
-
- if max_age is DEFAULT_VALUE:
- max_age = self.config['session_max_age']
-
- container = self._get_session_container(name, factory)
- return container.get_session(max_age=max_age)
-
- # Signed cookies
----------------------------------------------------------
+ key = key or self.config['cookie_name']
+ backend = backend or self.default_backend
+ sessions = self._sessions.setdefault(backend, {})
+
+ if key not in sessions:
+ kwargs = self.get_cookie_args(**kwargs)
+ value = self.backends[backend].get_session(self, key, **kwargs)
+ sessions[key] = (value, kwargs)
+
+ return sessions[key][0]
+
+ def set_session(self, key, value, backend=None, **kwargs):
+ """Sets a session value. If a session with the same key exists, it
+ will be overriden with the new value.
+
+ :param key:
+ Cookie name. See :meth:`get_session`.
+ :param value:
+ A dictionary of session values.
+ :param backend:
+ Name of the session backend. See :meth:`get_session`.
+ :param kwargs:
+ Options to save the cookie. See :meth:`get_session`.
+ """
+ assert isinstance(value, dict), 'Session value must be a dict.'
+ backend = backend or self.default_backend
+ sessions = self._sessions.setdefault(backend, {})
+ session = self.backends[backend].get_session(self, **kwargs)
+ session.update(value)
+ kwargs = self.get_cookie_args(**kwargs)
+ sessions[key] = (session, kwargs)
+
+ def update_session_args(self, key, backend=None, **kwargs):
+ """Updates the cookie options for a session.
+
+ :param key:
+ Cookie name. See :meth:`get_session`.
+ :param backend:
+ Name of the session backend. See :meth:`get_session`.
+ :param kwargs:
+ Options to save the cookie. See :meth:`get_session`.
+ :returns:
+ True if the session was updated, False otherwise.
+ """
+ backend = backend or self.default_backend
+ sessions = self._sessions.setdefault(backend, {})
+ if key in sessions:
+ sessions[key][1].update(kwargs)
+ return True
+
+ return False
def get_secure_cookie(self, name, max_age=DEFAULT_VALUE):
- """Returns a deserialized secure cookie value.
+ """Returns a secure cookie from the request.
:param name:
Cookie name.
@@ -298,14 +341,14 @@
if max_age is DEFAULT_VALUE:
max_age = self.config['session_max_age']
- value = self.request.cookies.get(name)
- if value:
- return self.cookie_serializer.deserialize(name, value,
- max_age=max_age)
-
- def set_secure_cookie(self, name, value, **kwargs):
- """Sets a secure cookie to be saved.
-
+ return self.secure_cookie_store.get_cookie(self.request, name,
+ max_age=max_age)
+
+ def set_secure_cookie(self, response, name, value, **kwargs):
+ """Sets a secure cookie in the response.
+
+ :param response:
+ A :class:`tipfy.app.Response` object.
:param name:
Cookie name.
:param value:
@@ -313,148 +356,65 @@
:param kwargs:
Options to save the cookie. See :meth:`get_session`.
"""
- container = self._get_session_container(name,
- SecureCookieSessionFactory)
- container.session = value
- container.session_args.update(kwargs)
-
- # Ordinary cookies
--------------------------------------------------------
-
- def get_cookie(self, name, decoder=json_b64decode):
- """Returns a cookie from the request, decoding it.
-
- :param name:
- Cookie name.
- :param decoder:
- An decoder for the cookie value. Default is
- func:`tipfy.json.json_b64decode`.
- :returns:
- A decoded cookie value, or None if a cookie with this name is
not
- set or decoding failed.
- """
- value = self.request.cookies.get(name)
- if value is not None and decoder:
- try:
- value = decoder(value)
- except Exception, e:
- return
-
- return value
-
- def set_cookie(self, name, value, format=None, encoder=json_b64encode,
- **kwargs):
- """Registers a cookie to be saved or deleted.
-
- :param name:
+ assert isinstance(value, dict), 'Secure cookie value must be a
dict.'
+ kwargs = self.get_cookie_args(**kwargs)
+ self.secure_cookie_store.set_cookie(response, name, value,
**kwargs)
+
+ def set_cookie(self, key, value, format=None, **kwargs):
+ """Registers a cookie or secure cookie to be saved or deleted.
+
+ :param key:
Cookie name.
:param value:
Cookie value.
:param format:
If set to 'json', the value is serialized to JSON and encoded
to base64.
-
- ..warning: Deprecated. Pass an encoder instead.
- :param encoder:
- An encoder for the cookie value. Default is
- func:`tipfy.json.json_b64encode`.
:param kwargs:
Options to save the cookie. See :meth:`get_session`.
"""
- if format is not None:
- from warnings import warn
- warn(DeprecationWarning("SessionStore.set_cookie(): the "
- "'format' argument is deprecated. Use 'encoder' instead
to "
- "pass an encoder callable."))
-
- if format == 'json':
- value = json_b64encode(value)
- elif encoder:
- value = encoder(value)
-
- container = self._get_session_container(name, CookieSessionFactory)
- container.session = value
- container.session_args.update(kwargs)
-
- def delete_cookie(self, name, **kwargs):
+ if format == 'json':
+ value = json_b64encode(value)
+
+ self._cookies[key] = (value, self.get_cookie_args(**kwargs))
+
+ def unset_cookie(self, key):
+ """Unsets a cookie previously set. This won't delete the cookie, it
+ just won't be saved.
+
+ :param key:
+ Cookie name.
+ """
+ self._cookies.pop(key, None)
+
+ def delete_cookie(self, key, **kwargs):
"""Registers a cookie or secure cookie to be deleted.
- :param name:
+ :param key:
Cookie name.
:param kwargs:
Options to delete the cookie. See :meth:`get_session`.
"""
- self.set_cookie(name, None, **kwargs)
-
- def unset_cookie(self, name):
- """Unsets a cookie previously set. This won't delete the cookie, it
- just won't be saved.
-
- :param name:
- Cookie name.
- """
- self.sessions.pop(name, None)
-
- # Saving to a response object
---------------------------------------------
-
- def save_sessions(self, response):
+ self._cookies[key] = (None, self.get_cookie_args(**kwargs))
+
+ def save(self, response):
"""Saves all cookies and sessions to a response object.
:param response:
- A ``tipfy.app.Response`` object.
+ A ``tipfy.Response`` object.
"""
- for session in self.sessions.values():
- session.save_session(response)
- # Old name
- save = save_sessions
-
- def save_secure_cookie(self, response, name, value, **kwargs):
- value = self.cookie_serializer.serialize(name, value)
- response.set_cookie(name, value, **kwargs)
-
- # Deprecated methods
------------------------------------------------------
-
- def set_session(self, name, value, backend=None, **kwargs):
- """Sets a session value. If a session with the same name exists, it
- will be overriden with the new value.
-
- :param name:
- Cookie name. See :meth:`get_session`.
- :param value:
- A dictionary of session values.
- :param backend:
- Name of the session backend. See :meth:`get_session`.
- :param kwargs:
- Options to save the cookie. See :meth:`get_session`.
- """
- from warnings import warn
- warn(DeprecationWarning("SessionStore.set_session(): this "
- "method is deprecated. Cookie arguments can be set directly
in "
- "a session."))
-
- self.set_secure_cookie(name, value, **kwargs)
-
- def update_session_args(self, name, backend=None, **kwargs):
- """Updates the cookie options for a session.
-
- :param name:
- Cookie name. See :meth:`get_session`.
- :param backend:
- Name of the session backend. See :meth:`get_session`.
- :param kwargs:
- Options to save the cookie. See :meth:`get_session`.
- :returns:
- True if the session was updated, False otherwise.
- """
- from warnings import warn
- warn(DeprecationWarning("SessionStore.update_session_args(): this "
- "method is deprecated. Cookie arguments can be set directly
in "
- "a session."))
-
- if name in self.sessions:
- self.sessions[name].session_args.update(kwargs)
- return True
-
- return False
+ if self._cookies:
+ for key, (value, kwargs) in self._cookies.iteritems():
+ if value is None:
+ response.delete_cookie(key,
path=kwargs.get('path', '/'),
+ domain=kwargs.get('domain', None))
+ else:
+ response.set_cookie(key, value, **kwargs)
+
+ if self._sessions:
+ for sessions in self._sessions.values():
+ for key, (value, kwargs) in sessions.iteritems():
+ value.save_session(response, self, key, **kwargs)
def get_cookie_args(self, **kwargs):
"""Returns a copy of the default cookie configuration updated with
the
@@ -465,11 +425,6 @@
:returns:
A dictionary with arguments for the session cookie.
"""
- from warnings import warn
- warn(DeprecationWarning("SessionStore.get_cookie_args(): this "
- "method is deprecated. Cookie arguments can be set directly
in "
- "a session."))
-
_kwargs = self.config['cookie_args'].copy()
_kwargs.update(kwargs)
return _kwargs
@@ -489,3 +444,11 @@
"""
handler.session_store.save(response)
return response
+
+
+if APPENGINE:
+ from tipfy.appengine.sessions import DatastoreSession, MemcacheSession
+ SessionStore.default_backends.update({
+ 'datastore': DatastoreSession,
+ 'memcache': MemcacheSession,
+ })
==============================================================================
Revision: a07fb37a97b0
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sun Apr 17 06:49:01 2011
Log: Organized imports.
http://code.google.com/p/tipfy/source/detail?r=a07fb37a97b0
Modified:
/tipfy/routing.py
=======================================
--- /tipfy/routing.py Sun Apr 3 07:14:04 2011
+++ /tipfy/routing.py Sun Apr 17 06:49:01 2011
@@ -8,13 +8,21 @@
:copyright: 2011 by tipfy.org.
:license: BSD, see LICENSE.txt for more details.
"""
-from werkzeug import import_string, url_quote
-from werkzeug.routing import (BaseConverter, EndpointPrefix, Map,
- Rule as BaseRule, RuleFactory, Subdomain, Submount)
-from werkzeug.wrappers import BaseResponse
+from werkzeug import routing
+from werkzeug import urls
+from werkzeug import utils
+from werkzeug import wrappers
from .local import get_request, local
+# For export.
+BaseConverter = routing.BaseConverter
+EndpointPrefix = routing.EndpointPrefix
+Map = routing.Map
+RuleFactory = routing.RuleFactory
+Subdomain = routing.Subdomain
+Submount = routing.Submount
+
class Router(object):
def __init__(self, app, rules=None):
@@ -86,12 +94,13 @@
handler = rule.handler
if isinstance(handler, basestring):
if handler not in self.handlers:
- self.handlers[handler] = import_string(handler)
+ self.handlers[handler] = utils.import_string(handler)
rule.handler = handler = self.handlers[handler]
rv = local.current_handler = handler(request)
- if not isinstance(rv, BaseResponse) and hasattr(rv, '__call__'):
+ if not isinstance(rv, wrappers.BaseResponse) and \
+ hasattr(rv, '__call__'):
# If it is a callable but not a response, we call it again.
rv = rv()
@@ -137,7 +146,7 @@
url = '%s://%s%s' % (scheme or 'http', netloc or request.host,
url)
if anchor:
- url += '#%s' % url_quote(anchor)
+ url += '#%s' % urls.url_quote(anchor)
return url
@@ -178,7 +187,7 @@
build = url_for
-class Rule(BaseRule):
+class Rule(routing.Rule):
"""A Rule represents one URL pattern. Tipfy extends Werkzeug's Rule
to support handler and name definitions. Handler is the
:class:`tipfy.RequestHandler` class that will handle the request and
name
@@ -300,7 +309,7 @@
self.handler_method = handler_method
if isinstance(handler, basestring) and handler.rfind(':') != -1:
if handler_method:
- raise ValueError(
+ raise BadArgumentError(
"If handler_method is defined in a Rule, handler "
"can't have a colon (got %r)." % handler)
else:
==============================================================================
Revision: 00e03f9a9e4f
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sat Apr 23 15:22:32 2011
Log: Set correct version number.
http://code.google.com/p/tipfy/source/detail?r=00e03f9a9e4f
Modified:
/tipfy/__init__.py
=======================================
--- /tipfy/__init__.py Sat Apr 2 14:59:44 2011
+++ /tipfy/__init__.py Sat Apr 23 15:22:32 2011
@@ -8,8 +8,8 @@
:copyright: 2011 by tipfy.org.
:license: BSD, see LICENSE.txt for more details.
"""
-__version__ = '0.7'
-__version_info__ = tuple(int(n) for n in __version__.split('.'))
+__version__ = '1.0b'
+__version_info__ = (1, 0)
#: Default configuration values for this module. Keys are:
#:
==============================================================================
Revision: 29099f92601e
Author: Rodrigo Moraes <rodrigo...@gmail.com>
Date: Sat Apr 23 15:36:10 2011
Log: Fixed weird set_sys_path side effect. Thanks for the the report
and solution, guys. :) (Fixes issue 81)
http://code.google.com/p/tipfy/source/detail?r=29099f92601e
Modified:
/docs/Makefile
/docs/make.bat
/project/app/main.py
/project/app/set_sys_path.py
=======================================
--- /docs/Makefile Fri Jul 9 20:34:09 2010
+++ /docs/Makefile Sat Apr 23 15:36:10 2011
@@ -5,30 +5,33 @@
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
-BUILDDIR = build
+BUILDDIR = _build
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
-ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER))
$(SPHINXOPTS) source
-ALLCREOLEOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER))
$(SPHINXOPTS) source_creole
-
-.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes
linkcheck doctest
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER))
$(SPHINXOPTS) .
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp
devhelp epub latex latexpdf text man changes linkcheck doctest
help:
@echo "Please use \`make <target>' where <target> is one of"
- @echo " html to make standalone HTML files"
- @echo " dirhtml to make HTML files named index.html in directories"
- @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 " 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 " changes to make an overview of all changed/added/deprecated
items"
- @echo " linkcheck to check all external links for integrity"
- @echo " doctest to run all doctests embedded in the documentation (if
enabled)"
+ @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 " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " changes to make an overview of all changed/added/deprecated
items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if
enabled)"
clean:
-rm -rf $(BUILDDIR)/*
@@ -38,21 +41,16 @@
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
-text:
- $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
- @echo
- @echo "Build finished. The HTML pages are in $(BUILDDIR)/text."
-
-creole:
- $(SPHINXBUILD) -b creole $(ALLCREOLEOPTS) $(BUILDDIR)/creole
- @echo
- @echo "Build finished. The creole files are in $(BUILDDIR)/creole."
-
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@@ -79,26 +77,41 @@
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Tipfy.qhc"
devhelp:
- $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) build/devhelp
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/Tipfy"
- @echo "# ln -s build/devhelp $$HOME/.local/share/devhelp/Tipfy"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Tipfy"
@echo "# devhelp"
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
- @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \
- "run these through (pdf)latex."
-
-latexpdf: latex
- $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) build/latex
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
- make -C build/latex all-pdf
- @echo "pdflatex finished; the PDF files are in build/latex."
+ make -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
=======================================
--- /docs/make.bat Wed Nov 11 05:00:42 2009
+++ /docs/make.bat Sat Apr 23 15:36:10 2011
@@ -2,9 +2,11 @@
REM Command file for Sphinx documentation
-set SPHINXBUILD=sphinx-build
-set BUILDDIR=build
-set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source
+if "%SPHINXBUILD%" == "" (
+ set SPHINXBUILD=sphinx-build
+)
+set BUILDDIR=_build
+set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
)
@@ -14,17 +16,21 @@
if "%1" == "help" (
:help
echo.Please use `make ^<target^>` where ^<target^> is one of
- echo. html to make standalone HTML files
- echo. dirhtml to make HTML files named index.html in directories
- 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. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
- echo. changes to make an overview over all changed/added/deprecated
items
- echo. linkcheck to check all external links for integrity
- echo. doctest to run all doctests embedded in the documentation if
enabled
+ 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. changes to make an overview over all changed/added/deprecated
items
+ echo. linkcheck to check all external links for integrity
+ echo. doctest to run all doctests embedded in the documentation if
enabled
goto end
)
@@ -36,6 +42,7 @@
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
@@ -43,13 +50,23 @@
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
@@ -57,6 +74,7 @@
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
@@ -64,6 +82,7 @@
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.
@@ -72,6 +91,7 @@
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:
@@ -82,21 +102,48 @@
)
if "%1" == "devhelp" (
- %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% build/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" == "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" == "changes" (
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
+ if errorlevel 1 exit /b 1
echo.
echo.The overview file is in %BUILDDIR%/changes.
goto end
@@ -104,6 +151,7 @@
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.
@@ -112,6 +160,7 @@
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.
=======================================
--- /project/app/main.py Sat Apr 2 14:29:27 2011
+++ /project/app/main.py Sat Apr 23 15:36:10 2011
@@ -33,4 +33,5 @@
app.run()
if __name__ == '__main__':
+ set_sys_path.set_path()
main()
=======================================
--- /project/app/set_sys_path.py Sat Apr 2 18:05:21 2011
+++ /project/app/set_sys_path.py Sat Apr 23 15:36:10 2011
@@ -1,14 +1,23 @@
# -*- coding: utf-8 -*-
-"""Sets sys.path for the library directories."""
+"""Sets sys.path for the library directories.
+
+The purpose of this file is to define extra paths in a single place. This
is
+convenient in case many entry points are used instead of a single main.py.
+"""
import os
import sys
current_path = os.path.abspath(os.path.dirname(__file__))
-
-# Add lib as primary libraries directory, with fallback to lib/dist
-# and optionally to lib/dist.zip, loaded using zipimport.
-sys.path[0:0] = [
- os.path.join(current_path, 'lib'),
- os.path.join(current_path, 'lib', 'dist'),
- os.path.join(current_path, 'lib', 'dist.zip'),
-]
+lib_path = os.path.join(current_path, 'lib')
+
+def set_path():
+ # Add lib as primary libraries directory, with fallback to lib/dist
+ # and optionally to lib/dist.zip, loaded using zipimport.
+ if lib not in sys.path:
+ sys.path[0:0] = [
+ lib_path,
+ os.path.join(lib_path, 'dist'),
+ os.path.join(lib_path, 'dist.zip'),
+ ]
+
+set_path()