After a full-scale scan with pep8.py and pyflakes, identified and
resolved most of our PEP8 compliance issues.
This commit is contained in:
@@ -9,7 +9,10 @@ from paste.urlparser import StaticURLParser
|
||||
from weberror.errormiddleware import ErrorMiddleware
|
||||
from weberror.evalexception import EvalException
|
||||
|
||||
from core import abort, error_for, override_template, Pecan, load_app, redirect, render, request, response, ValidationException
|
||||
from core import (
|
||||
abort, override_template, Pecan, load_app, redirect, render,
|
||||
request, response, ValidationException
|
||||
)
|
||||
from decorators import expose
|
||||
from hooks import RequestViewerHook
|
||||
from templating import error_formatters
|
||||
@@ -18,12 +21,16 @@ from configuration import set_config
|
||||
from configuration import _runtime_conf as conf
|
||||
|
||||
__all__ = [
|
||||
'make_app', 'load_app', 'Pecan', 'request', 'response', 'override_template', 'expose', 'conf', 'set_config'
|
||||
'make_app', 'load_app', 'Pecan', 'request', 'response',
|
||||
'override_template', 'expose', 'conf', 'set_config', 'render',
|
||||
'abort', 'ValidationException', 'redirect'
|
||||
]
|
||||
|
||||
def make_app(root, static_root=None, debug=False, errorcfg={}, wrap_app=None, logging=False, **kw):
|
||||
|
||||
def make_app(root, static_root=None, debug=False, errorcfg={},
|
||||
wrap_app=None, logging=False, **kw):
|
||||
'''
|
||||
|
||||
|
||||
'''
|
||||
if hasattr(conf, 'requestviewer'):
|
||||
existing_hooks = kw.get('hooks', [])
|
||||
@@ -35,7 +42,11 @@ def make_app(root, static_root=None, debug=False, errorcfg={}, wrap_app=None, lo
|
||||
app = wrap_app(app)
|
||||
app = RecursiveMiddleware(app)
|
||||
if debug:
|
||||
app = EvalException(app, templating_formatters=error_formatters, **errorcfg)
|
||||
app = EvalException(
|
||||
app,
|
||||
templating_formatters=error_formatters,
|
||||
**errorcfg
|
||||
)
|
||||
else:
|
||||
app = ErrorMiddleware(app, **errorcfg)
|
||||
app = make_errordocument(app, conf, **conf.app.errors)
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""
|
||||
PasteScript commands for Pecan.
|
||||
"""
|
||||
from runner import CommandRunner
|
||||
from create import CreateCommand
|
||||
from shell import ShellCommand
|
||||
from serve import ServeCommand
|
||||
|
||||
from runner import CommandRunner # noqa
|
||||
from create import CreateCommand # noqa
|
||||
from shell import ShellCommand # noqa
|
||||
from serve import ServeCommand # noqa
|
||||
|
||||
@@ -2,30 +2,28 @@
|
||||
PasteScript base command for Pecan.
|
||||
"""
|
||||
from pecan import load_app
|
||||
from pecan.configuration import _runtime_conf, set_config
|
||||
from paste.script import command as paste_command
|
||||
|
||||
import os.path
|
||||
import sys
|
||||
|
||||
|
||||
class Command(paste_command.Command):
|
||||
"""
|
||||
Base class for Pecan commands.
|
||||
|
||||
This provides some standard functionality for interacting with Pecan
|
||||
|
||||
This provides some standard functionality for interacting with Pecan
|
||||
applications and handles some of the basic PasteScript command cruft.
|
||||
|
||||
|
||||
See ``paste.script.command.Command`` for more information.
|
||||
"""
|
||||
|
||||
|
||||
# command information
|
||||
group_name = 'Pecan'
|
||||
summary = ''
|
||||
|
||||
|
||||
# command parser
|
||||
parser = paste_command.Command.standard_parser()
|
||||
|
||||
|
||||
def run(self, args):
|
||||
try:
|
||||
return paste_command.Command.run(self, args)
|
||||
@@ -35,15 +33,17 @@ class Command(paste_command.Command):
|
||||
|
||||
def load_app(self):
|
||||
return load_app(self.validate_file(self.args))
|
||||
|
||||
|
||||
def logging_file_config(self, config_file):
|
||||
if os.path.splitext(config_file)[1].lower() == '.ini':
|
||||
paste_command.Command.logging_file_config(self, config_file)
|
||||
|
||||
def validate_file(self, argv):
|
||||
if not argv or not os.path.isfile(argv[0]):
|
||||
raise paste_command.BadCommand('This command needs a valid config file.')
|
||||
raise paste_command.BadCommand(
|
||||
'This command needs a valid config file.'
|
||||
)
|
||||
return argv[0]
|
||||
|
||||
|
||||
def command(self):
|
||||
pass
|
||||
|
||||
@@ -13,16 +13,16 @@ import sys
|
||||
class CreateCommand(CreateDistroCommand, Command):
|
||||
"""
|
||||
Creates the file layout for a new Pecan distribution.
|
||||
|
||||
For a template to show up when using this command, its name must begin
|
||||
with "pecan-". Although not required, it should also include the "Pecan"
|
||||
|
||||
For a template to show up when using this command, its name must begin
|
||||
with "pecan-". Although not required, it should also include the "Pecan"
|
||||
egg plugin for user convenience.
|
||||
"""
|
||||
|
||||
|
||||
# command information
|
||||
summary = __doc__.strip().splitlines()[0].rstrip('.')
|
||||
description = None
|
||||
|
||||
|
||||
def command(self):
|
||||
if not self.options.list_templates:
|
||||
if not self.options.templates:
|
||||
@@ -33,7 +33,7 @@ class CreateCommand(CreateDistroCommand, Command):
|
||||
sys.stderr.write('%s\n\n' % ex)
|
||||
CreateDistroCommand.list_templates(self)
|
||||
return 2
|
||||
|
||||
|
||||
def all_entry_points(self):
|
||||
entry_points = []
|
||||
for entry in CreateDistroCommand.all_entry_points(self):
|
||||
|
||||
@@ -13,50 +13,52 @@ import warnings
|
||||
class CommandRunner(object):
|
||||
"""
|
||||
Dispatches command execution requests.
|
||||
|
||||
This is a custom PasteScript command runner that is specific to Pecan
|
||||
commands. For a command to show up, its name must begin with "pecan-".
|
||||
It is also recommended that its group name be set to "Pecan" so that it
|
||||
|
||||
This is a custom PasteScript command runner that is specific to Pecan
|
||||
commands. For a command to show up, its name must begin with "pecan-".
|
||||
It is also recommended that its group name be set to "Pecan" so that it
|
||||
shows up under that group when using ``paster`` directly.
|
||||
"""
|
||||
|
||||
|
||||
def __init__(self):
|
||||
|
||||
|
||||
# set up the parser
|
||||
self.parser = optparse.OptionParser(add_help_option=False,
|
||||
version='Pecan %s' % self.get_version(),
|
||||
usage='%prog [options] COMMAND [command_options]')
|
||||
self.parser = optparse.OptionParser(
|
||||
add_help_option=False,
|
||||
version='Pecan %s' % self.get_version(),
|
||||
usage='%prog [options] COMMAND [command_options]'
|
||||
)
|
||||
self.parser.disable_interspersed_args()
|
||||
self.parser.add_option('-h', '--help',
|
||||
action='store_true',
|
||||
dest='show_help',
|
||||
help='show detailed help message')
|
||||
|
||||
|
||||
# suppress BaseException.message warnings for BadCommand
|
||||
if sys.version_info < (2, 7):
|
||||
warnings.filterwarnings(
|
||||
'ignore',
|
||||
'BaseException\.message has been deprecated as of Python 2\.6',
|
||||
DeprecationWarning,
|
||||
'ignore',
|
||||
'BaseException\.message has been deprecated as of Python 2\.6',
|
||||
DeprecationWarning,
|
||||
paste_command.__name__.replace('.', '\\.'))
|
||||
|
||||
|
||||
# register Pecan as a system plugin when using the custom runner
|
||||
paste_command.system_plugins.append('Pecan')
|
||||
|
||||
|
||||
def get_command_template(self, command_names):
|
||||
if not command_names:
|
||||
max_length = 10
|
||||
else:
|
||||
max_length = max([len(name) for name in command_names])
|
||||
return ' %%-%ds %%s\n' % max_length
|
||||
|
||||
|
||||
def get_commands(self):
|
||||
commands = {}
|
||||
for name, command in paste_command.get_commands().iteritems():
|
||||
if name.startswith('pecan-'):
|
||||
commands[name[6:]] = command.load()
|
||||
return commands
|
||||
|
||||
|
||||
def get_version(self):
|
||||
try:
|
||||
dist = pkg_resources.get_distribution('Pecan')
|
||||
@@ -66,7 +68,7 @@ class CommandRunner(object):
|
||||
return '(development)'
|
||||
except:
|
||||
return '(development)'
|
||||
|
||||
|
||||
def print_usage(self, file=sys.stdout):
|
||||
self.parser.print_help(file=file)
|
||||
file.write('\n')
|
||||
@@ -88,7 +90,7 @@ class CommandRunner(object):
|
||||
file.write(command_template % (name, command.summary))
|
||||
if i + 1 < len(command_groups):
|
||||
file.write('\n')
|
||||
|
||||
|
||||
def print_known_commands(self, file=sys.stderr):
|
||||
commands = self.get_commands()
|
||||
command_names = sorted(commands.keys())
|
||||
@@ -99,7 +101,7 @@ class CommandRunner(object):
|
||||
command_template = self.get_command_template(command_names)
|
||||
for name in command_names:
|
||||
file.write(command_template % (name, commands[name].summary))
|
||||
|
||||
|
||||
def run(self, args):
|
||||
options, args = self.parser.parse_args(args)
|
||||
if not args:
|
||||
@@ -117,7 +119,7 @@ class CommandRunner(object):
|
||||
return command.run(['-h'])
|
||||
else:
|
||||
return command.run(args)
|
||||
|
||||
|
||||
@classmethod
|
||||
def handle_command_line(cls):
|
||||
try:
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
"""
|
||||
PasteScript serve command for Pecan.
|
||||
"""
|
||||
from paste import httpserver
|
||||
from paste.script import command as paste_command
|
||||
from paste import httpserver
|
||||
from paste.script.serve import ServeCommand as _ServeCommand
|
||||
|
||||
from base import Command
|
||||
import os
|
||||
import re
|
||||
|
||||
|
||||
class ServeCommand(_ServeCommand, Command):
|
||||
"""
|
||||
Serves a Pecan web application.
|
||||
|
||||
This command serves a Pecan web application using the provided
|
||||
|
||||
This command serves a Pecan web application using the provided
|
||||
configuration file for the server and application.
|
||||
|
||||
If start/stop/restart is given, then --daemon is implied, and it will
|
||||
|
||||
If start/stop/restart is given, then --daemon is implied, and it will
|
||||
start (normal operation), stop (--stop-daemon), or do both.
|
||||
"""
|
||||
|
||||
|
||||
# command information
|
||||
usage = 'CONFIG_FILE [start|stop|restart|status]'
|
||||
summary = __doc__.strip().splitlines()[0].rstrip('.')
|
||||
description = '\n'.join(map(lambda s: s.rstrip(), __doc__.strip().splitlines()[2:]))
|
||||
|
||||
description = '\n'.join(
|
||||
map(lambda s: s.rstrip(), __doc__.strip().splitlines()[2:])
|
||||
)
|
||||
|
||||
# command options/arguments
|
||||
max_args = 2
|
||||
|
||||
|
||||
# command parser
|
||||
parser = _ServeCommand.parser
|
||||
parser.remove_option('-n')
|
||||
parser.remove_option('-s')
|
||||
parser.remove_option('--server-name')
|
||||
|
||||
|
||||
# configure scheme regex
|
||||
_scheme_re = re.compile(r'.*')
|
||||
|
||||
|
||||
def command(self):
|
||||
|
||||
|
||||
# set defaults for removed options
|
||||
setattr(self.options, 'app_name', None)
|
||||
setattr(self.options, 'server', None)
|
||||
@@ -47,9 +47,11 @@ class ServeCommand(_ServeCommand, Command):
|
||||
|
||||
# run the base command
|
||||
_ServeCommand.command(self)
|
||||
|
||||
|
||||
def loadserver(self, server_spec, name, relative_to, **kw):
|
||||
return (lambda app: httpserver.serve(app, app.config.server.host, app.config.server.port))
|
||||
|
||||
return (lambda app: httpserver.serve(
|
||||
app, app.config.server.host, app.config.server.port
|
||||
))
|
||||
|
||||
def loadapp(self, app_spec, name, relative_to, **kw):
|
||||
return self.load_app()
|
||||
|
||||
@@ -12,41 +12,47 @@ class ShellCommand(Command):
|
||||
"""
|
||||
Open an interactive shell with the Pecan app loaded.
|
||||
"""
|
||||
|
||||
|
||||
# command information
|
||||
usage = 'CONFIG_NAME'
|
||||
summary = __doc__.strip().splitlines()[0].rstrip('.')
|
||||
|
||||
|
||||
# command options/arguments
|
||||
min_args = 1
|
||||
max_args = 1
|
||||
|
||||
|
||||
def command(self):
|
||||
|
||||
|
||||
# load the application
|
||||
app = self.load_app()
|
||||
|
||||
|
||||
# prepare the locals
|
||||
locs = dict(__name__='pecan-admin')
|
||||
locs['wsgiapp'] = app
|
||||
locs['app'] = TestApp(app)
|
||||
|
||||
|
||||
model = self.load_model(app.config)
|
||||
if model:
|
||||
locs['model'] = model
|
||||
|
||||
|
||||
# insert the pecan locals
|
||||
exec('from pecan import abort, conf, redirect, request, response') in locs
|
||||
|
||||
exec(
|
||||
'from pecan import abort, conf, redirect, request, response'
|
||||
) in locs
|
||||
|
||||
# prepare the banner
|
||||
banner = ' The following objects are available:\n'
|
||||
banner += ' %-10s - This project\'s WSGI App instance\n' % 'wsgiapp'
|
||||
banner += ' %-10s - The current configuration\n' % 'conf'
|
||||
banner += ' %-10s - webtest.TestApp wrapped around wsgiapp\n' % 'app'
|
||||
if model:
|
||||
model_name = getattr(model, '__module__', getattr(model, '__name__', 'model'))
|
||||
model_name = getattr(
|
||||
model,
|
||||
'__module__',
|
||||
getattr(model, '__name__', 'model')
|
||||
)
|
||||
banner += ' %-10s - Models from %s\n' % ('model', model_name)
|
||||
|
||||
|
||||
# launch the shell, using IPython if available
|
||||
try:
|
||||
from IPython.Shell import IPShellEmbed
|
||||
@@ -60,7 +66,7 @@ class ShellCommand(Command):
|
||||
(py_prefix, sys.version)
|
||||
shell = code.InteractiveConsole(locals=locs)
|
||||
try:
|
||||
import readline
|
||||
import readline # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
shell.interact(shell_banner + banner)
|
||||
|
||||
@@ -7,43 +7,45 @@ IDENTIFIER = re.compile(r'[a-z_](\w)*$', re.IGNORECASE)
|
||||
|
||||
DEFAULT = {
|
||||
# Server Specific Configurations
|
||||
'server' : {
|
||||
'port' : '8080',
|
||||
'host' : '0.0.0.0'
|
||||
'server': {
|
||||
'port': '8080',
|
||||
'host': '0.0.0.0'
|
||||
},
|
||||
|
||||
# Pecan Application Configurations
|
||||
'app' : {
|
||||
'root' : None,
|
||||
'modules' : [],
|
||||
'static_root' : 'public',
|
||||
'template_path' : '',
|
||||
'debug' : False,
|
||||
'logging' : False,
|
||||
'force_canonical' : True,
|
||||
'errors' : {
|
||||
'__force_dict__' : True
|
||||
'app': {
|
||||
'root': None,
|
||||
'modules': [],
|
||||
'static_root': 'public',
|
||||
'template_path': '',
|
||||
'debug': False,
|
||||
'logging': False,
|
||||
'force_canonical': True,
|
||||
'errors': {
|
||||
'__force_dict__': True
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ConfigDict(dict):
|
||||
pass
|
||||
|
||||
|
||||
class Config(object):
|
||||
'''
|
||||
Base class for Pecan configurations.
|
||||
'''
|
||||
|
||||
|
||||
def __init__(self, conf_dict={}, filename=''):
|
||||
'''
|
||||
Create a Pecan configuration object from a dictionary or a
|
||||
Create a Pecan configuration object from a dictionary or a
|
||||
filename.
|
||||
|
||||
|
||||
:param conf_dict: A python dictionary to use for the configuration.
|
||||
:param filename: A filename to use for the configuration.
|
||||
'''
|
||||
|
||||
|
||||
self.__values__ = {}
|
||||
self.__file__ = filename
|
||||
self.update(conf_dict)
|
||||
@@ -51,16 +53,17 @@ class Config(object):
|
||||
def update(self, conf_dict):
|
||||
'''
|
||||
Updates this configuration with a dictionary.
|
||||
|
||||
:param conf_dict: A python dictionary to update this configuration with.
|
||||
|
||||
:param conf_dict: A python dictionary to update this configuration
|
||||
with.
|
||||
'''
|
||||
|
||||
|
||||
if isinstance(conf_dict, dict):
|
||||
iterator = conf_dict.iteritems()
|
||||
else:
|
||||
iterator = iter(conf_dict)
|
||||
|
||||
for k,v in iterator:
|
||||
|
||||
for k, v in iterator:
|
||||
if not IDENTIFIER.match(k):
|
||||
raise ValueError('\'%s\' is not a valid indentifier' % k)
|
||||
|
||||
@@ -94,11 +97,11 @@ class Config(object):
|
||||
def as_dict(self, prefix=None):
|
||||
'''
|
||||
Converts recursively the Config object into a valid dictionary.
|
||||
|
||||
:param prefix: A string to optionally prefix all key elements in the
|
||||
|
||||
:param prefix: A string to optionally prefix all key elements in the
|
||||
returned dictonary.
|
||||
'''
|
||||
|
||||
|
||||
conf_obj = dict(self)
|
||||
return self.__dictify__(conf_obj, prefix)
|
||||
|
||||
@@ -106,7 +109,8 @@ class Config(object):
|
||||
try:
|
||||
return self.__values__[name]
|
||||
except KeyError:
|
||||
raise AttributeError, "'pecan.conf' object has no attribute '%s'" % name
|
||||
msg = "'pecan.conf' object has no attribute '%s'" % name
|
||||
raise AttributeError(msg)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.__values__[key]
|
||||
@@ -129,7 +133,8 @@ class Config(object):
|
||||
|
||||
def __dir__(self):
|
||||
"""
|
||||
When using dir() returns a list of the values in the config. Note: This function only works in Python2.6 or later.
|
||||
When using dir() returns a list of the values in the config. Note:
|
||||
This function only works in Python2.6 or later.
|
||||
"""
|
||||
return self.__values__.keys()
|
||||
|
||||
@@ -140,10 +145,10 @@ class Config(object):
|
||||
def conf_from_file(filepath):
|
||||
'''
|
||||
Creates a configuration dictionary from a file.
|
||||
|
||||
|
||||
:param filepath: The path to the file.
|
||||
'''
|
||||
|
||||
|
||||
abspath = os.path.abspath(os.path.expanduser(filepath))
|
||||
conf_dict = {}
|
||||
|
||||
@@ -156,26 +161,26 @@ def conf_from_file(filepath):
|
||||
def conf_from_dict(conf_dict):
|
||||
'''
|
||||
Creates a configuration dictionary from a dictionary.
|
||||
|
||||
|
||||
:param conf_dict: The configuration dictionary.
|
||||
'''
|
||||
|
||||
|
||||
conf = Config(filename=conf_dict.get('__file__', ''))
|
||||
|
||||
for k,v in conf_dict.iteritems():
|
||||
for k, v in conf_dict.iteritems():
|
||||
if k.startswith('__'):
|
||||
continue
|
||||
elif inspect.ismodule(v):
|
||||
continue
|
||||
|
||||
|
||||
conf[k] = v
|
||||
return conf
|
||||
|
||||
|
||||
def initconf():
|
||||
'''
|
||||
Initializes the default configuration and exposes it at ``pecan.configuration.conf``,
|
||||
which is also exposed at ``pecan.conf``.
|
||||
Initializes the default configuration and exposes it at
|
||||
``pecan.configuration.conf``, which is also exposed at ``pecan.conf``.
|
||||
'''
|
||||
return conf_from_dict(DEFAULT)
|
||||
|
||||
@@ -183,8 +188,9 @@ def initconf():
|
||||
def set_config(config, overwrite=False):
|
||||
'''
|
||||
Updates the global configuration a filename.
|
||||
|
||||
:param config: Can be a dictionary containing configuration, or a string which
|
||||
|
||||
:param config: Can be a dictionary containing configuration, or a string
|
||||
which
|
||||
represents a (relative) configuration filename.
|
||||
'''
|
||||
|
||||
@@ -200,4 +206,3 @@ def set_config(config, overwrite=False):
|
||||
|
||||
|
||||
_runtime_conf = initconf()
|
||||
|
||||
|
||||
347
pecan/core.py
347
pecan/core.py
@@ -14,8 +14,8 @@ from urlparse import urlsplit, urlunsplit
|
||||
|
||||
try:
|
||||
from simplejson import loads
|
||||
except ImportError: # pragma: no cover
|
||||
from json import loads
|
||||
except ImportError: # pragma: no cover
|
||||
from json import loads # noqa
|
||||
|
||||
import urllib
|
||||
|
||||
@@ -30,63 +30,76 @@ def proxy(key):
|
||||
def __getattr__(self, attr):
|
||||
obj = getattr(state, key)
|
||||
return getattr(obj, attr)
|
||||
|
||||
def __setattr__(self, attr, value):
|
||||
obj = getattr(state, key)
|
||||
return setattr(obj, attr, value)
|
||||
|
||||
def __delattr__(self, attr):
|
||||
obj = getattr(state, key)
|
||||
return delattr(obj, attr)
|
||||
return ObjectProxy()
|
||||
|
||||
|
||||
request = proxy('request')
|
||||
response = proxy('response')
|
||||
request = proxy('request')
|
||||
response = proxy('response')
|
||||
|
||||
|
||||
def override_template(template, content_type=None):
|
||||
'''
|
||||
Call within a controller to override the template that is used in
|
||||
your response.
|
||||
|
||||
:param template: a valid path to a template file, just as you would specify in an ``@expose``.
|
||||
:param content_type: a valid MIME type to use for the response.
|
||||
|
||||
:param template: a valid path to a template file, just as you would specify
|
||||
in an ``@expose``.
|
||||
:param content_type: a valid MIME type to use for the response.func_closure
|
||||
'''
|
||||
|
||||
|
||||
request.pecan['override_template'] = template
|
||||
if content_type:
|
||||
request.pecan['override_content_type'] = content_type
|
||||
request.pecan['override_content_type'] = content_type
|
||||
|
||||
|
||||
def abort(status_code=None, detail='', headers=None, comment=None, **kw):
|
||||
'''
|
||||
Raise an HTTP status code, as specified. Useful for returning status
|
||||
codes like 401 Unauthorized or 403 Forbidden.
|
||||
|
||||
|
||||
:param status_code: The HTTP status code as an integer.
|
||||
:param detail: The message to send along, as a string.
|
||||
:param headers: A dictionary of headers to send along with the response.
|
||||
:param comment: A comment to include in the response.
|
||||
'''
|
||||
|
||||
raise exc.status_map[status_code](detail=detail, headers=headers, comment=comment, **kw)
|
||||
|
||||
raise exc.status_map[status_code](
|
||||
detail=detail,
|
||||
headers=headers,
|
||||
comment=comment,
|
||||
**kw
|
||||
)
|
||||
|
||||
|
||||
def redirect(location=None, internal=False, code=None, headers={}, add_slash=False):
|
||||
def redirect(location=None, internal=False, code=None, headers={},
|
||||
add_slash=False):
|
||||
'''
|
||||
Perform a redirect, either internal or external. An internal redirect
|
||||
performs the redirect server-side, while the external redirect utilizes
|
||||
an HTTP 302 status code.
|
||||
|
||||
|
||||
:param location: The HTTP location to redirect to.
|
||||
:param internal: A boolean indicating whether the redirect should be internal.
|
||||
:param internal: A boolean indicating whether the redirect should be
|
||||
internal.
|
||||
:param code: The HTTP status code to use for the redirect. Defaults to 302.
|
||||
:param headers: Any HTTP headers to send with the response, as a dictionary.
|
||||
:param headers: Any HTTP headers to send with the response, as a
|
||||
dictionary.
|
||||
'''
|
||||
|
||||
|
||||
if add_slash:
|
||||
if location is None:
|
||||
split_url = list(urlsplit(state.request.url))
|
||||
new_proto = state.request.environ.get('HTTP_X_FORWARDED_PROTO', split_url[0])
|
||||
new_proto = state.request.environ.get(
|
||||
'HTTP_X_FORWARDED_PROTO', split_url[0]
|
||||
)
|
||||
split_url[0] = new_proto
|
||||
else:
|
||||
split_url = urlsplit(location)
|
||||
@@ -110,10 +123,10 @@ def error_for(field):
|
||||
A convenience function for fetching the validation error for a
|
||||
particular field in a form. Useful within templates when not using
|
||||
``htmlfill`` for forms.
|
||||
|
||||
|
||||
:param field: The name of the field to get the error for.
|
||||
'''
|
||||
|
||||
|
||||
return request.pecan['validation_errors'].get(field, '')
|
||||
|
||||
|
||||
@@ -122,13 +135,13 @@ def static(name, value):
|
||||
When using ``htmlfill`` validation support, this function indicates
|
||||
that ``htmlfill`` should not fill in a value for this field, and
|
||||
should instead use the value specified.
|
||||
|
||||
|
||||
:param name: The name of the field.
|
||||
:param value: The value to specify.
|
||||
'''
|
||||
|
||||
|
||||
if 'pecan.params' not in request.environ:
|
||||
request.environ['pecan.params'] = dict(request.params)
|
||||
request.environ['pecan.params'] = dict(request.params)
|
||||
request.environ['pecan.params'][name] = value
|
||||
return value
|
||||
|
||||
@@ -138,11 +151,13 @@ def render(template, namespace):
|
||||
Render the specified template using the Pecan rendering framework
|
||||
with the specified template namespace as a dictionary. Useful in a
|
||||
controller where you have no template specified in the ``@expose``.
|
||||
|
||||
:param template: The path to your template, as you would specify in ``@expose``.
|
||||
:param namespace: The namespace to use for rendering the template, as a dictionary.
|
||||
|
||||
:param template: The path to your template, as you would specify in
|
||||
``@expose``.
|
||||
:param namespace: The namespace to use for rendering the template, as a
|
||||
dictionary.
|
||||
'''
|
||||
|
||||
|
||||
return state.app.render(template, namespace)
|
||||
|
||||
|
||||
@@ -151,7 +166,7 @@ class ValidationException(ForwardRequestException):
|
||||
This exception is raised when a validation error occurs using Pecan's
|
||||
built-in validation framework.
|
||||
'''
|
||||
|
||||
|
||||
def __init__(self, location=None, errors={}):
|
||||
if state.controller is not None:
|
||||
cfg = _cfg(state.controller)
|
||||
@@ -164,7 +179,9 @@ class ValidationException(ForwardRequestException):
|
||||
merge_dicts(request.pecan['validation_errors'], errors)
|
||||
if 'pecan.params' not in request.environ:
|
||||
request.environ['pecan.params'] = dict(request.params)
|
||||
request.environ['pecan.validation_errors'] = request.pecan['validation_errors']
|
||||
request.environ[
|
||||
'pecan.validation_errors'
|
||||
] = request.pecan['validation_errors']
|
||||
if cfg.get('htmlfill') is not None:
|
||||
request.environ['pecan.htmlfill'] = cfg['htmlfill']
|
||||
request.environ['REQUEST_METHOD'] = 'GET'
|
||||
@@ -177,8 +194,8 @@ def load_app(config):
|
||||
Used to load a ``Pecan`` application and its environment based on passed
|
||||
configuration.
|
||||
|
||||
:param config: Can be a dictionary containing configuration, or a string which
|
||||
represents a (relative) configuration filename.
|
||||
:param config: Can be a dictionary containing configuration, or a string
|
||||
which represents a (relative) configuration filename.
|
||||
:returns a pecan.Pecan object
|
||||
'''
|
||||
set_config(config, overwrite=True)
|
||||
@@ -189,7 +206,9 @@ def load_app(config):
|
||||
app = module.app.setup_app(_runtime_conf)
|
||||
app.config = _runtime_conf
|
||||
return app
|
||||
raise RuntimeError('No app.setup_app found in any of the configured app.modules')
|
||||
raise RuntimeError(
|
||||
'No app.setup_app found in any of the configured app.modules'
|
||||
)
|
||||
|
||||
|
||||
class Pecan(object):
|
||||
@@ -197,37 +216,42 @@ class Pecan(object):
|
||||
Base Pecan application object. Generally created using ``pecan.make_app``,
|
||||
rather than being created manually.
|
||||
'''
|
||||
|
||||
def __init__(self, root,
|
||||
default_renderer = 'mako',
|
||||
template_path = 'templates',
|
||||
hooks = [],
|
||||
custom_renderers = {},
|
||||
extra_template_vars = {},
|
||||
force_canonical = True
|
||||
):
|
||||
|
||||
def __init__(self, root,
|
||||
default_renderer='mako',
|
||||
template_path='templates',
|
||||
hooks=[],
|
||||
custom_renderers={},
|
||||
extra_template_vars={},
|
||||
force_canonical=True
|
||||
):
|
||||
'''
|
||||
Creates a Pecan application instance, which is a WSGI application.
|
||||
|
||||
|
||||
:param root: A string representing a root controller object (e.g.,
|
||||
"myapp.controller.root.RootController")
|
||||
:param default_renderer: The default rendering engine to use. Defaults to mako.
|
||||
:param template_path: The default relative path to use for templates. Defaults to 'templates'.
|
||||
:param default_renderer: The default rendering engine to use. Defaults
|
||||
to mako.
|
||||
:param template_path: The default relative path to use for templates.
|
||||
Defaults to 'templates'.
|
||||
:param hooks: A list of Pecan hook objects to use for this application.
|
||||
:param custom_renderers: Custom renderer objects, as a dictionary keyed by engine name.
|
||||
:param extra_template_vars: Any variables to inject into the template namespace automatically.
|
||||
:param force_canonical: A boolean indicating if this project should require canonical URLs.
|
||||
:param custom_renderers: Custom renderer objects, as a dictionary keyed
|
||||
by engine name.
|
||||
:param extra_template_vars: Any variables to inject into the template
|
||||
namespace automatically.
|
||||
:param force_canonical: A boolean indicating if this project should
|
||||
require canonical URLs.
|
||||
'''
|
||||
|
||||
if isinstance(root, basestring):
|
||||
root = self.__translate_root__(root)
|
||||
|
||||
self.root = root
|
||||
self.renderers = RendererFactory(custom_renderers, extra_template_vars)
|
||||
self.root = root
|
||||
self.renderers = RendererFactory(custom_renderers, extra_template_vars)
|
||||
self.default_renderer = default_renderer
|
||||
self.hooks = hooks
|
||||
self.template_path = template_path
|
||||
self.force_canonical = force_canonical
|
||||
self.hooks = hooks
|
||||
self.template_path = template_path
|
||||
self.force_canonical = force_canonical
|
||||
|
||||
def __translate_root__(self, item):
|
||||
'''
|
||||
@@ -235,8 +259,8 @@ class Pecan(object):
|
||||
|
||||
> __translate_root__("myproject.controllers.RootController")
|
||||
myproject.controllers.RootController()
|
||||
|
||||
:param item: The string to the item
|
||||
|
||||
:param item: The string to the item
|
||||
'''
|
||||
|
||||
if '.' in item:
|
||||
@@ -247,68 +271,75 @@ class Pecan(object):
|
||||
try:
|
||||
module = __import__(name, fromlist=fromlist)
|
||||
kallable = getattr(module, parts[-1])
|
||||
assert hasattr(kallable, '__call__'), "%s does not represent a callable class or function." % item
|
||||
msg = "%s does not represent a callable class or function."
|
||||
assert hasattr(kallable, '__call__'), msg % item
|
||||
return kallable()
|
||||
except AttributeError, e:
|
||||
except AttributeError:
|
||||
raise ImportError('No item named %s' % item)
|
||||
|
||||
raise ImportError('No item named %s' % item)
|
||||
|
||||
|
||||
def route(self, node, path):
|
||||
'''
|
||||
Looks up a controller from a node based upon the specified path.
|
||||
|
||||
|
||||
:param node: The node, such as a root controller object.
|
||||
:param path: The path to look up on this node.
|
||||
'''
|
||||
|
||||
|
||||
path = path.split('/')[1:]
|
||||
try:
|
||||
node, remainder = lookup_controller(node, path)
|
||||
return node, remainder
|
||||
except NonCanonicalPath, e:
|
||||
if self.force_canonical and not _cfg(e.controller).get('accept_noncanonical', False):
|
||||
if self.force_canonical and \
|
||||
not _cfg(e.controller).get('accept_noncanonical', False):
|
||||
if request.method == 'POST':
|
||||
raise RuntimeError, "You have POSTed to a URL '%s' which '\
|
||||
raise RuntimeError(
|
||||
"You have POSTed to a URL '%s' which '\
|
||||
'requires a slash. Most browsers will not maintain '\
|
||||
'POST data when redirected. Please update your code '\
|
||||
'to POST to '%s/' or set force_canonical to False" % \
|
||||
(request.pecan['routing_path'], request.pecan['routing_path'])
|
||||
(request.pecan['routing_path'],
|
||||
request.pecan['routing_path'])
|
||||
)
|
||||
redirect(code=302, add_slash=True)
|
||||
return e.controller, e.remainder
|
||||
|
||||
|
||||
def determine_hooks(self, controller=None):
|
||||
'''
|
||||
Determines the hooks to be run, in which order.
|
||||
|
||||
:param controller: If specified, includes hooks for a specific controller.
|
||||
|
||||
:param controller: If specified, includes hooks for a specific
|
||||
controller.
|
||||
'''
|
||||
|
||||
|
||||
controller_hooks = []
|
||||
if controller:
|
||||
controller_hooks = _cfg(controller).get('hooks', [])
|
||||
return list(
|
||||
sorted(
|
||||
chain(controller_hooks, self.hooks),
|
||||
lambda x,y: cmp(x.priority, y.priority)
|
||||
chain(controller_hooks, self.hooks),
|
||||
lambda x, y: cmp(x.priority, y.priority)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def handle_hooks(self, hook_type, *args):
|
||||
'''
|
||||
Processes hooks of the specified type.
|
||||
|
||||
:param hook_type: The type of hook, including ``before``, ``after``, ``on_error``, and ``on_route``.
|
||||
|
||||
:param hook_type: The type of hook, including ``before``, ``after``,
|
||||
``on_error``, and ``on_route``.
|
||||
:param *args: Arguments to pass to the hooks.
|
||||
'''
|
||||
|
||||
|
||||
if hook_type in ['before', 'on_route']:
|
||||
hooks = state.hooks
|
||||
else:
|
||||
hooks = reversed(state.hooks)
|
||||
|
||||
for hook in hooks:
|
||||
getattr(hook, hook_type)(*args)
|
||||
getattr(hook, hook_type)(*args)
|
||||
|
||||
def get_args(self, all_params, remainder, argspec, im_self):
|
||||
'''
|
||||
@@ -321,35 +352,35 @@ class Pecan(object):
|
||||
|
||||
def _decode(x):
|
||||
return urllib.unquote_plus(x) if isinstance(x, basestring) else x
|
||||
|
||||
|
||||
remainder = [_decode(x) for x in remainder]
|
||||
|
||||
|
||||
if im_self is not None:
|
||||
args.append(im_self)
|
||||
|
||||
|
||||
# grab the routing args from nested REST controllers
|
||||
if 'routing_args' in request.pecan:
|
||||
remainder = request.pecan['routing_args'] + list(remainder)
|
||||
del request.pecan['routing_args']
|
||||
|
||||
|
||||
# handle positional arguments
|
||||
if valid_args and remainder:
|
||||
args.extend(remainder[:len(valid_args)])
|
||||
remainder = remainder[len(valid_args):]
|
||||
valid_args = valid_args[len(args):]
|
||||
|
||||
|
||||
# handle wildcard arguments
|
||||
if remainder:
|
||||
if not argspec[1]:
|
||||
abort(404)
|
||||
args.extend(remainder)
|
||||
|
||||
|
||||
# get the default positional arguments
|
||||
if argspec[3]:
|
||||
defaults = dict(zip(argspec[0][-len(argspec[3]):], argspec[3]))
|
||||
else:
|
||||
defaults = dict()
|
||||
|
||||
|
||||
# handle positional GET/POST params
|
||||
for name in valid_args:
|
||||
if name in all_params:
|
||||
@@ -358,47 +389,59 @@ class Pecan(object):
|
||||
args.append(defaults[name])
|
||||
else:
|
||||
break
|
||||
|
||||
|
||||
# handle wildcard GET/POST params
|
||||
if argspec[2]:
|
||||
for name, value in all_params.iteritems():
|
||||
if name not in argspec[0]:
|
||||
kwargs[encode_if_needed(name)] = value
|
||||
|
||||
|
||||
return args, kwargs
|
||||
|
||||
|
||||
def render(self, template, namespace):
|
||||
renderer = self.renderers.get(self.default_renderer, self.template_path)
|
||||
renderer = self.renderers.get(
|
||||
self.default_renderer,
|
||||
self.template_path
|
||||
)
|
||||
if template == 'json':
|
||||
renderer = self.renderers.get('json', self.template_path)
|
||||
else:
|
||||
namespace['error_for'] = error_for
|
||||
namespace['static'] = static
|
||||
if ':' in template:
|
||||
renderer = self.renderers.get(template.split(':')[0], self.template_path)
|
||||
renderer = self.renderers.get(
|
||||
template.split(':')[0],
|
||||
self.template_path
|
||||
)
|
||||
template = template.split(':')[1]
|
||||
return renderer.render(template, namespace)
|
||||
|
||||
def validate(self, schema, params, json=False, error_handler=None,
|
||||
|
||||
def validate(self, schema, params, json=False, error_handler=None,
|
||||
htmlfill=None, variable_decode=None):
|
||||
'''
|
||||
Performs validation against a schema for any passed params,
|
||||
Performs validation against a schema for any passed params,
|
||||
including support for ``JSON``.
|
||||
|
||||
|
||||
:param schema: A ``formencode`` ``Schema`` object to validate against.
|
||||
:param params: The dictionary of parameters to validate.
|
||||
:param json: A boolean, indicating whether or not the validation should validate against JSON content.
|
||||
:param error_handler: The path to a controller which will handle errors. If not specified, validation errors will raise a ``ValidationException``.
|
||||
:param json: A boolean, indicating whether or not the validation should
|
||||
validate against JSON content.
|
||||
:param error_handler: The path to a controller which will handle
|
||||
errors. If not specified, validation errors will raise a
|
||||
``ValidationException``.
|
||||
:param htmlfill: Specifies whether or not to use htmlfill.
|
||||
:param variable_decode: Indicates whether or not to decode variables when using htmlfill.
|
||||
:param variable_decode: Indicates whether or not to decode variables
|
||||
when using htmlfill.
|
||||
'''
|
||||
|
||||
|
||||
try:
|
||||
to_validate = params
|
||||
if json:
|
||||
to_validate = loads(request.body)
|
||||
if variable_decode is not None:
|
||||
to_validate = variabledecode.variable_decode(to_validate, **variable_decode)
|
||||
to_validate = variabledecode.variable_decode(
|
||||
to_validate, **variable_decode
|
||||
)
|
||||
params = schema.to_python(to_validate)
|
||||
except Invalid, e:
|
||||
kwargs = {}
|
||||
@@ -411,21 +454,21 @@ class Pecan(object):
|
||||
if json:
|
||||
params = dict(data=params)
|
||||
return params or {}
|
||||
|
||||
|
||||
def handle_request(self):
|
||||
'''
|
||||
The main request handler for Pecan applications.
|
||||
'''
|
||||
|
||||
|
||||
# get a sorted list of hooks, by priority (no controller hooks yet)
|
||||
state.hooks = self.determine_hooks()
|
||||
|
||||
|
||||
# store the routing path to allow hooks to modify it
|
||||
request.pecan['routing_path'] = request.path
|
||||
|
||||
# handle "on_route" hooks
|
||||
self.handle_hooks('on_route', state)
|
||||
|
||||
|
||||
# lookup the controller, respecting content-type as requested
|
||||
# by the file extension on the URI
|
||||
path = request.pecan['routing_path']
|
||||
@@ -441,7 +484,7 @@ class Pecan(object):
|
||||
|
||||
if cfg.get('generic_handler'):
|
||||
raise exc.HTTPNotFound
|
||||
|
||||
|
||||
# handle generic controllers
|
||||
im_self = None
|
||||
if cfg.get('generic'):
|
||||
@@ -449,58 +492,65 @@ class Pecan(object):
|
||||
handlers = cfg['generic_handlers']
|
||||
controller = handlers.get(request.method, handlers['DEFAULT'])
|
||||
cfg = _cfg(controller)
|
||||
|
||||
|
||||
# add the controller to the state so that hooks can use it
|
||||
state.controller = controller
|
||||
|
||||
# if unsure ask the controller for the default content type
|
||||
|
||||
# if unsure ask the controller for the default content type
|
||||
if not request.pecan['content_type']:
|
||||
request.pecan['content_type'] = cfg.get('content_type', 'text/html')
|
||||
request.pecan['content_type'] = cfg.get(
|
||||
'content_type',
|
||||
'text/html'
|
||||
)
|
||||
elif cfg.get('content_type') is not None and \
|
||||
request.pecan['content_type'] not in cfg.get('content_types', {}):
|
||||
|
||||
import warnings
|
||||
warnings.warn("Controller '%s' defined does not support content_type '%s'. Supported type(s): %s" % (
|
||||
controller.__name__,
|
||||
request.pecan['content_type'],
|
||||
cfg.get('content_types', {}).keys()
|
||||
msg = "Controller '%s' defined does not support content_type " + \
|
||||
"'%s'. Supported type(s): %s"
|
||||
warnings.warn(
|
||||
msg % (
|
||||
controller.__name__,
|
||||
request.pecan['content_type'],
|
||||
cfg.get('content_types', {}).keys()
|
||||
),
|
||||
RuntimeWarning
|
||||
)
|
||||
raise exc.HTTPNotFound
|
||||
|
||||
|
||||
# get a sorted list of hooks, by priority
|
||||
state.hooks = self.determine_hooks(controller)
|
||||
|
||||
|
||||
# handle "before" hooks
|
||||
self.handle_hooks('before', state)
|
||||
|
||||
|
||||
# fetch and validate any parameters
|
||||
params = dict(request.params)
|
||||
if 'schema' in cfg:
|
||||
params = self.validate(
|
||||
cfg['schema'],
|
||||
params,
|
||||
json=cfg['validate_json'],
|
||||
error_handler=cfg.get('error_handler'),
|
||||
cfg['schema'],
|
||||
params,
|
||||
json=cfg['validate_json'],
|
||||
error_handler=cfg.get('error_handler'),
|
||||
htmlfill=cfg.get('htmlfill'),
|
||||
variable_decode=cfg.get('variable_decode')
|
||||
)
|
||||
elif 'pecan.validation_errors' in request.environ:
|
||||
request.pecan['validation_errors'] = request.environ.pop('pecan.validation_errors')
|
||||
|
||||
errors = request.environ.pop('pecan.validation_errors')
|
||||
request.pecan['validation_errors'] = errors
|
||||
|
||||
# fetch the arguments for the controller
|
||||
args, kwargs = self.get_args(
|
||||
params,
|
||||
params,
|
||||
remainder,
|
||||
cfg['argspec'],
|
||||
im_self
|
||||
)
|
||||
|
||||
|
||||
# get the result from the controller
|
||||
result = controller(*args, **kwargs)
|
||||
|
||||
# a controller can return the response object which means they've taken
|
||||
# a controller can return the response object which means they've taken
|
||||
# care of filling it out
|
||||
if result == response:
|
||||
return
|
||||
@@ -508,29 +558,41 @@ class Pecan(object):
|
||||
raw_namespace = result
|
||||
|
||||
# pull the template out based upon content type and handle overrides
|
||||
template = cfg.get('content_types', {}).get(request.pecan['content_type'])
|
||||
template = cfg.get('content_types', {}).get(
|
||||
request.pecan['content_type']
|
||||
)
|
||||
|
||||
# check if for controller override of template
|
||||
template = request.pecan.get('override_template', template)
|
||||
request.pecan['content_type'] = request.pecan.get('override_content_type', request.pecan['content_type'])
|
||||
request.pecan['content_type'] = request.pecan.get(
|
||||
'override_content_type',
|
||||
request.pecan['content_type']
|
||||
)
|
||||
|
||||
# if there is a template, render it
|
||||
if template:
|
||||
if template == 'json':
|
||||
request.pecan['content_type'] = 'application/json'
|
||||
result = self.render(template, result)
|
||||
|
||||
# pass the response through htmlfill (items are popped out of the
|
||||
|
||||
# pass the response through htmlfill (items are popped out of the
|
||||
# environment even if htmlfill won't run for proper cleanup)
|
||||
_htmlfill = cfg.get('htmlfill')
|
||||
if _htmlfill is None and 'pecan.htmlfill' in request.environ:
|
||||
_htmlfill = request.environ.pop('pecan.htmlfill')
|
||||
if 'pecan.params' in request.environ:
|
||||
params = request.environ.pop('pecan.params')
|
||||
if request.pecan['validation_errors'] and _htmlfill is not None and request.pecan['content_type'] == 'text/html':
|
||||
if request.pecan['validation_errors'] and _htmlfill is not None and \
|
||||
request.pecan['content_type'] == 'text/html':
|
||||
errors = request.pecan['validation_errors']
|
||||
result = htmlfill.render(result, defaults=params, errors=errors, text_as_default=True, **_htmlfill)
|
||||
|
||||
result = htmlfill.render(
|
||||
result,
|
||||
defaults=params,
|
||||
errors=errors,
|
||||
text_as_default=True,
|
||||
**_htmlfill
|
||||
)
|
||||
|
||||
# If we are in a test request put the namespace where it can be
|
||||
# accessed directly
|
||||
if request.environ.get('paste.testing'):
|
||||
@@ -538,32 +600,33 @@ class Pecan(object):
|
||||
testing_variables['namespace'] = raw_namespace
|
||||
testing_variables['template_name'] = template
|
||||
testing_variables['controller_output'] = result
|
||||
|
||||
|
||||
# set the body content
|
||||
if isinstance(result, unicode):
|
||||
response.unicode_body = result
|
||||
else:
|
||||
response.body = result
|
||||
|
||||
|
||||
# set the content type
|
||||
if request.pecan['content_type']:
|
||||
response.content_type = request.pecan['content_type']
|
||||
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
'''
|
||||
Implements the WSGI specification for Pecan applications, utilizing ``WebOb``.
|
||||
Implements the WSGI specification for Pecan applications, utilizing
|
||||
``WebOb``.
|
||||
'''
|
||||
|
||||
|
||||
# create the request and response object
|
||||
state.request = Request(environ)
|
||||
state.response = Response()
|
||||
state.hooks = []
|
||||
state.app = self
|
||||
state.controller = None
|
||||
|
||||
state.request = Request(environ)
|
||||
state.response = Response()
|
||||
state.hooks = []
|
||||
state.app = self
|
||||
state.controller = None
|
||||
|
||||
# handle the request
|
||||
try:
|
||||
# add context and environment to the request
|
||||
# add context and environment to the request
|
||||
state.request.context = {}
|
||||
state.request.pecan = dict(content_type=None, validation_errors={})
|
||||
|
||||
@@ -572,21 +635,21 @@ class Pecan(object):
|
||||
# if this is an HTTP Exception, set it as the response
|
||||
if isinstance(e, exc.HTTPException):
|
||||
state.response = e
|
||||
|
||||
|
||||
# if this is not an internal redirect, run error hooks
|
||||
if not isinstance(e, ForwardRequestException):
|
||||
self.handle_hooks('on_error', state, e)
|
||||
|
||||
|
||||
if not isinstance(e, exc.HTTPException):
|
||||
raise
|
||||
finally:
|
||||
# handle "after" hooks
|
||||
self.handle_hooks('after', state)
|
||||
|
||||
|
||||
# get the response
|
||||
try:
|
||||
return state.response(environ, start_response)
|
||||
finally:
|
||||
finally:
|
||||
# clean up state
|
||||
del state.hooks
|
||||
del state.request
|
||||
|
||||
@@ -2,7 +2,8 @@ from inspect import getargspec, getmembers, isclass, ismethod
|
||||
from util import _cfg
|
||||
|
||||
__all__ = [
|
||||
'expose', 'transactional', 'accept_noncanonical', 'after_commit', 'after_rollback'
|
||||
'expose', 'transactional', 'accept_noncanonical', 'after_commit',
|
||||
'after_rollback'
|
||||
]
|
||||
|
||||
|
||||
@@ -16,88 +17,107 @@ def when_for(controller):
|
||||
return decorate
|
||||
return when
|
||||
|
||||
def expose(template = None,
|
||||
content_type = 'text/html',
|
||||
schema = None,
|
||||
json_schema = None,
|
||||
variable_decode = False,
|
||||
error_handler = None,
|
||||
htmlfill = None,
|
||||
generic = False):
|
||||
|
||||
|
||||
def expose(template=None,
|
||||
content_type='text/html',
|
||||
schema=None,
|
||||
json_schema=None,
|
||||
variable_decode=False,
|
||||
error_handler=None,
|
||||
htmlfill=None,
|
||||
generic=False):
|
||||
|
||||
'''
|
||||
Decorator used to flag controller methods as being "exposed" for
|
||||
access via HTTP, and to configure that access.
|
||||
|
||||
:param template: The path to a template, relative to the base template directory.
|
||||
|
||||
:param template: The path to a template, relative to the base template
|
||||
directory.
|
||||
:param content_type: The content-type to use for this template.
|
||||
:param schema: A ``formencode`` ``Schema`` object to use for validation.
|
||||
:param json_schema: A ``formencode`` ``Schema`` object to use for validation of JSON POST/PUT content.
|
||||
:param variable_decode: A boolean indicating if you want to use ``htmlfill``'s variable decode capability of transforming flat HTML form structures into nested ones.
|
||||
:param htmlfill: Indicates whether or not you want to use ``htmlfill`` for this controller.
|
||||
:param generic: A boolean which flags this as a "generic" controller, which uses generic functions based upon ``simplegeneric`` generic functions. Allows you to split a single controller into multiple paths based upon HTTP method.
|
||||
:param json_schema: A ``formencode`` ``Schema`` object to use for
|
||||
validation of JSON POST/PUT content.
|
||||
:param variable_decode: A boolean indicating if you want to use
|
||||
``htmlfill``'s variable decode capability of transforming flat HTML form
|
||||
structures into nested ones.
|
||||
:param htmlfill: Indicates whether or not you want to use ``htmlfill`` for
|
||||
this controller.
|
||||
:param generic: A boolean which flags this as a "generic" controller, which
|
||||
uses generic functions based upon ``simplegeneric`` generic functions.
|
||||
Allows you to split a single controller into multiple paths based upon HTTP
|
||||
method.
|
||||
'''
|
||||
|
||||
if template == 'json': content_type = 'application/json'
|
||||
|
||||
if template == 'json':
|
||||
content_type = 'application/json'
|
||||
|
||||
def decorate(f):
|
||||
# flag the method as exposed
|
||||
f.exposed = True
|
||||
|
||||
|
||||
# set a "pecan" attribute, where we will store details
|
||||
cfg = _cfg(f)
|
||||
cfg['content_type'] = content_type
|
||||
cfg.setdefault('template', []).append(template)
|
||||
cfg.setdefault('content_types', {})[content_type] = template
|
||||
|
||||
|
||||
# handle generic controllers
|
||||
if generic:
|
||||
cfg['generic'] = True
|
||||
cfg['generic_handlers'] = dict(DEFAULT=f)
|
||||
f.when = when_for(f)
|
||||
|
||||
|
||||
# store the arguments for this controller method
|
||||
cfg['argspec'] = getargspec(f)
|
||||
|
||||
|
||||
# store the schema
|
||||
cfg['error_handler'] = error_handler
|
||||
if schema is not None:
|
||||
if schema is not None:
|
||||
cfg['schema'] = schema
|
||||
cfg['validate_json'] = False
|
||||
elif json_schema is not None:
|
||||
elif json_schema is not None:
|
||||
cfg['schema'] = json_schema
|
||||
cfg['validate_json'] = True
|
||||
|
||||
|
||||
# store the variable decode configuration
|
||||
if isinstance(variable_decode, dict) or variable_decode == True:
|
||||
_variable_decode = dict(dict_char='.', list_char='-')
|
||||
if isinstance(variable_decode, dict):
|
||||
_variable_decode.update(variable_decode)
|
||||
cfg['variable_decode'] = _variable_decode
|
||||
|
||||
|
||||
# store the htmlfill configuration
|
||||
if isinstance(htmlfill, dict) or htmlfill == True or schema is not None:
|
||||
if isinstance(htmlfill, dict) or htmlfill == True or \
|
||||
schema is not None:
|
||||
_htmlfill = dict(auto_insert_errors=False)
|
||||
if isinstance(htmlfill, dict):
|
||||
_htmlfill.update(htmlfill)
|
||||
cfg['htmlfill'] = _htmlfill
|
||||
return f
|
||||
return decorate
|
||||
|
||||
|
||||
|
||||
def transactional(ignore_redirects=True):
|
||||
'''
|
||||
If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
|
||||
to flag a controller method or class as being wrapped in a transaction,
|
||||
regardless of HTTP method.
|
||||
|
||||
:param ignore_redirects: Indicates if the hook should ignore redirects for this controller or not.
|
||||
|
||||
:param ignore_redirects: Indicates if the hook should ignore redirects
|
||||
for this controller or not.
|
||||
'''
|
||||
|
||||
def deco(f):
|
||||
if isclass(f):
|
||||
for method in [m[1] for m in getmembers(f) if ismethod(m[1])]:
|
||||
if getattr(method, 'exposed', False):
|
||||
_cfg(method)['transactional'] = True
|
||||
_cfg(method)['transactional_ignore_redirects'] = _cfg(method).get('transactional_ignore_redirects', ignore_redirects)
|
||||
for meth in [m[1] for m in getmembers(f) if ismethod(m[1])]:
|
||||
if getattr(meth, 'exposed', False):
|
||||
_cfg(meth)['transactional'] = True
|
||||
_cfg(meth)['transactional_ignore_redirects'] = _cfg(
|
||||
meth
|
||||
).get(
|
||||
'transactional_ignore_redirects',
|
||||
ignore_redirects
|
||||
)
|
||||
else:
|
||||
_cfg(f)['transactional'] = True
|
||||
_cfg(f)['transactional_ignore_redirects'] = ignore_redirects
|
||||
@@ -111,25 +131,26 @@ def after_action(action_type, action):
|
||||
to flag a controller method to perform a callable action after the
|
||||
action_type is successfully issued.
|
||||
|
||||
:param action: The callable to call after the commit is successfully issued.
|
||||
'''
|
||||
:param action: The callable to call after the commit is successfully
|
||||
issued. '''
|
||||
|
||||
if action_type not in ('commit', 'rollback'):
|
||||
raise Exception, 'action_type (%s) is not valid' % action_type
|
||||
|
||||
raise Exception('action_type (%s) is not valid' % action_type)
|
||||
|
||||
def deco(func):
|
||||
_cfg(func).setdefault('after_%s' % action_type, []).append(action)
|
||||
return func
|
||||
return deco
|
||||
|
||||
|
||||
def after_commit(action):
|
||||
'''
|
||||
If utilizing the :mod:`pecan.hooks` ``TransactionHook``, allows you
|
||||
to flag a controller method to perform a callable action after the
|
||||
commit is successfully issued.
|
||||
|
||||
:param action: The callable to call after the commit is successfully issued.
|
||||
:param action: The callable to call after the commit is successfully
|
||||
issued.
|
||||
'''
|
||||
return after_action('commit', action)
|
||||
|
||||
@@ -140,7 +161,8 @@ def after_rollback(action):
|
||||
to flag a controller method to perform a callable action after the
|
||||
rollback is successfully issued.
|
||||
|
||||
:param action: The callable to call after the rollback is successfully issued.
|
||||
:param action: The callable to call after the rollback is successfully
|
||||
issued.
|
||||
'''
|
||||
return after_action('rollback', action)
|
||||
|
||||
@@ -149,6 +171,6 @@ def accept_noncanonical(func):
|
||||
'''
|
||||
Flags a controller method as accepting non-canoncial URLs.
|
||||
'''
|
||||
|
||||
|
||||
_cfg(func)['accept_noncanonical'] = True
|
||||
return func
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from core import load_app
|
||||
|
||||
|
||||
def deploy(config):
|
||||
return load_app(config)
|
||||
|
||||
158
pecan/hooks.py
158
pecan/hooks.py
@@ -5,32 +5,38 @@ from webob.exc import HTTPFound
|
||||
from util import iscontroller, _cfg
|
||||
from routing import lookup_controller
|
||||
|
||||
__all__ = ['PecanHook', 'TransactionHook', 'HookController', 'RequestViewerHook']
|
||||
__all__ = [
|
||||
'PecanHook', 'TransactionHook', 'HookController',
|
||||
'RequestViewerHook'
|
||||
]
|
||||
|
||||
|
||||
def walk_controller(root_class, controller, hooks):
|
||||
if not isinstance(controller, (int, dict)):
|
||||
for name, value in getmembers(controller):
|
||||
if name == 'controller': continue
|
||||
if name.startswith('__') and name.endswith('__'): continue
|
||||
|
||||
if name == 'controller':
|
||||
continue
|
||||
if name.startswith('__') and name.endswith('__'):
|
||||
continue
|
||||
|
||||
if iscontroller(value):
|
||||
for hook in hooks:
|
||||
value._pecan.setdefault('hooks', []).append(hook)
|
||||
elif hasattr(value, '__class__'):
|
||||
if name.startswith('__') and name.endswith('__'): continue
|
||||
if name.startswith('__') and name.endswith('__'):
|
||||
continue
|
||||
walk_controller(root_class, value, hooks)
|
||||
|
||||
|
||||
class HookController(object):
|
||||
'''
|
||||
A base class for controllers that would like to specify hooks on
|
||||
their controller methods. Simply create a list of hook objects
|
||||
their controller methods. Simply create a list of hook objects
|
||||
called ``__hooks__`` as a member of the controller's namespace.
|
||||
'''
|
||||
|
||||
|
||||
__hooks__ = []
|
||||
|
||||
|
||||
class __metaclass__(type):
|
||||
def __init__(cls, name, bases, dict_):
|
||||
walk_controller(cls, cls, dict_['__hooks__'])
|
||||
@@ -42,41 +48,41 @@ class PecanHook(object):
|
||||
own hooks. Set a priority on a hook by setting the ``priority``
|
||||
attribute for the hook, which defaults to 100.
|
||||
'''
|
||||
|
||||
|
||||
priority = 100
|
||||
|
||||
|
||||
def on_route(self, state):
|
||||
'''
|
||||
Override this method to create a hook that gets called upon
|
||||
the start of routing.
|
||||
|
||||
|
||||
:param state: The Pecan ``state`` object for the current request.
|
||||
'''
|
||||
return
|
||||
|
||||
|
||||
def before(self, state):
|
||||
'''
|
||||
Override this method to create a hook that gets called after
|
||||
routing, but before the request gets passed to your controller.
|
||||
|
||||
|
||||
:param state: The Pecan ``state`` object for the current request.
|
||||
'''
|
||||
return
|
||||
|
||||
|
||||
def after(self, state):
|
||||
'''
|
||||
Override this method to create a hook that gets called after
|
||||
the request has been handled by the controller.
|
||||
|
||||
|
||||
:param state: The Pecan ``state`` object for the current request.
|
||||
'''
|
||||
return
|
||||
|
||||
|
||||
def on_error(self, state, e):
|
||||
'''
|
||||
Override this method to create a hook that gets called upon
|
||||
an exception being raised in your controller.
|
||||
|
||||
|
||||
:param state: The Pecan ``state`` object for the current request.
|
||||
:param e: The ``Exception`` object that was raised.
|
||||
'''
|
||||
@@ -90,21 +96,23 @@ class TransactionHook(PecanHook):
|
||||
requests in a transaction. Override the ``is_transactional`` method
|
||||
to define your own rules for what requests should be transactional.
|
||||
'''
|
||||
|
||||
|
||||
def __init__(self, start, start_ro, commit, rollback, clear):
|
||||
'''
|
||||
:param start: A callable that will bind to a writable database and start a transaction.
|
||||
:param start: A callable that will bind to a writable database and
|
||||
start a transaction.
|
||||
:param start_ro: A callable that will bind to a readable database.
|
||||
:param commit: A callable that will commit the active transaction.
|
||||
:param rollback: A callable that will roll back the active transaction.
|
||||
:param rollback: A callable that will roll back the active
|
||||
transaction.
|
||||
:param clear: A callable that will clear your current context.
|
||||
'''
|
||||
|
||||
self.start = start
|
||||
|
||||
self.start = start
|
||||
self.start_ro = start_ro
|
||||
self.commit = commit
|
||||
self.commit = commit
|
||||
self.rollback = rollback
|
||||
self.clear = clear
|
||||
self.clear = clear
|
||||
|
||||
def is_transactional(self, state):
|
||||
'''
|
||||
@@ -112,10 +120,10 @@ class TransactionHook(PecanHook):
|
||||
upon the state of the request. By default, wraps all but ``GET``
|
||||
and ``HEAD`` requests in a transaction, along with respecting
|
||||
the ``transactional`` decorator from :mod:pecan.decorators.
|
||||
|
||||
|
||||
:param state: The Pecan state object for the current request.
|
||||
'''
|
||||
|
||||
|
||||
controller = getattr(state, 'controller', None)
|
||||
if controller:
|
||||
force_transactional = _cfg(controller).get('transactional', False)
|
||||
@@ -147,13 +155,20 @@ class TransactionHook(PecanHook):
|
||||
# (e.g., shouldn't consider them rollback-worthy)
|
||||
# don't set `state.request.error = True`.
|
||||
#
|
||||
transactional_ignore_redirects = state.request.method not in ('GET', 'HEAD')
|
||||
trans_ignore_redirects = (
|
||||
state.request.method not in ('GET', 'HEAD')
|
||||
)
|
||||
if state.controller is not None:
|
||||
transactional_ignore_redirects = _cfg(state.controller).get('transactional_ignore_redirects', transactional_ignore_redirects)
|
||||
if type(e) is HTTPFound and transactional_ignore_redirects is True:
|
||||
trans_ignore_redirects = (
|
||||
_cfg(state.controller).get(
|
||||
'transactional_ignore_redirects',
|
||||
trans_ignore_redirects
|
||||
)
|
||||
)
|
||||
if type(e) is HTTPFound and trans_ignore_redirects is True:
|
||||
return
|
||||
state.request.error = True
|
||||
|
||||
|
||||
def after(self, state):
|
||||
if state.request.transactional:
|
||||
action_name = None
|
||||
@@ -178,21 +193,22 @@ class TransactionHook(PecanHook):
|
||||
|
||||
self.clear()
|
||||
|
||||
|
||||
class RequestViewerHook(PecanHook):
|
||||
'''
|
||||
Returns some information about what is going on in a single request. It
|
||||
accepts specific items to report on but uses a default list of items when
|
||||
none are passed in. Based on the requested ``url``, items can also be
|
||||
blacklisted.
|
||||
Configuration is flexible, can be passed in (or not) and can contain some or
|
||||
all the keys supported.
|
||||
Configuration is flexible, can be passed in (or not) and can contain
|
||||
some or all the keys supported.
|
||||
|
||||
``items``
|
||||
---------
|
||||
This key holds the items that this hook will display. When this key is passed
|
||||
only the items in the list will be used.
|
||||
Valid items are *any* item that the ``request`` object holds, by default it uses
|
||||
the following:
|
||||
This key holds the items that this hook will display. When this key is
|
||||
passed only the items in the list will be used. Valid items are *any*
|
||||
item that the ``request`` object holds, by default it uses the
|
||||
following:
|
||||
|
||||
* path
|
||||
* status
|
||||
@@ -206,11 +222,12 @@ class RequestViewerHook(PecanHook):
|
||||
|
||||
``blacklist``
|
||||
-------------
|
||||
This key holds items that will be blacklisted based on ``url``. If there is a need
|
||||
to ommit urls that start with `/javascript`, then this key would look like::
|
||||
This key holds items that will be blacklisted based on ``url``. If
|
||||
there is a need to ommit urls that start with `/javascript`, then this
|
||||
key would look like::
|
||||
|
||||
'blacklist': ['/javascript']
|
||||
|
||||
|
||||
As many blacklisting items as needed can be contained in the list. The hook
|
||||
will verify that the url is not starting with items in this list to display
|
||||
results, otherwise it will get ommited.
|
||||
@@ -218,44 +235,50 @@ class RequestViewerHook(PecanHook):
|
||||
.. :note::
|
||||
This key should always use a ``list`` of items to use.
|
||||
|
||||
For more detailed documentation about this hook, please see :ref:`requestviewerhook`
|
||||
For more detailed documentation about this hook, please see
|
||||
:ref:`requestviewerhook`
|
||||
'''
|
||||
|
||||
available = ['path', 'status', 'method', 'controller', 'params', 'hooks']
|
||||
|
||||
def __init__(self, config=None, writer=sys.stdout, terminal=True, headers=True):
|
||||
def __init__(self, config=None, writer=sys.stdout, terminal=True,
|
||||
headers=True):
|
||||
'''
|
||||
:param config: A (optional) dictionary that can hold ``items`` and/or
|
||||
``blacklist`` keys.
|
||||
:param writer: The stream writer to use. Can redirect output to other
|
||||
streams as long as the passed in stream has a ``write``
|
||||
callable method.
|
||||
:param terminal: Outputs to the chosen stream writer (usually the terminal)
|
||||
``blacklist`` keys.
|
||||
:param writer: The stream writer to use. Can redirect output to other
|
||||
streams as long as the passed in stream has a
|
||||
``write`` callable method.
|
||||
:param terminal: Outputs to the chosen stream writer (usually
|
||||
the terminal)
|
||||
:param headers: Sets values to the X-HTTP headers
|
||||
'''
|
||||
if not config:
|
||||
self.config = {'items' : self.available}
|
||||
self.config = {'items': self.available}
|
||||
else:
|
||||
if config.__class__.__name__ == 'Config':
|
||||
self.config = config.as_dict()
|
||||
else:
|
||||
self.config = config
|
||||
self.writer = writer
|
||||
self.items = self.config.get('items', self.available)
|
||||
self.blacklist = self.config.get('blacklist', [])
|
||||
self.terminal = terminal
|
||||
self.headers = headers
|
||||
self.writer = writer
|
||||
self.items = self.config.get('items', self.available)
|
||||
self.blacklist = self.config.get('blacklist', [])
|
||||
self.terminal = terminal
|
||||
self.headers = headers
|
||||
|
||||
def after(self, state):
|
||||
|
||||
# Default and/or custom response information
|
||||
responses = {
|
||||
'controller' : lambda self, state: self.get_controller(state),
|
||||
'method' : lambda self, state: state.request.method,
|
||||
'path' : lambda self, state: state.request.path,
|
||||
'params' : lambda self, state: [(p[0].encode('utf-8'), p[1].encode('utf-8')) for p in state.request.params.items()],
|
||||
'status' : lambda self, state: state.response.status,
|
||||
'hooks' : lambda self, state: self.format_hooks(state.app.hooks),
|
||||
'controller': lambda self, state: self.get_controller(state),
|
||||
'method': lambda self, state: state.request.method,
|
||||
'path': lambda self, state: state.request.path,
|
||||
'params': lambda self, state: [
|
||||
(p[0].encode('utf-8'), p[1].encode('utf-8'))
|
||||
for p in state.request.params.items()
|
||||
],
|
||||
'status': lambda self, state: state.response.status,
|
||||
'hooks': lambda self, state: self.format_hooks(state.app.hooks),
|
||||
}
|
||||
|
||||
is_available = [
|
||||
@@ -263,16 +286,19 @@ class RequestViewerHook(PecanHook):
|
||||
if i in self.available or hasattr(state.request, i)
|
||||
]
|
||||
|
||||
terminal = []
|
||||
headers = []
|
||||
will_skip = [i for i in self.blacklist if state.request.path.startswith(i)]
|
||||
terminal = []
|
||||
headers = []
|
||||
will_skip = [
|
||||
i for i in self.blacklist
|
||||
if state.request.path.startswith(i)
|
||||
]
|
||||
|
||||
if will_skip:
|
||||
return
|
||||
|
||||
|
||||
for request_info in is_available:
|
||||
try:
|
||||
value = responses.get(request_info)
|
||||
value = responses.get(request_info)
|
||||
if not value:
|
||||
value = getattr(state.request, request_info)
|
||||
else:
|
||||
@@ -289,9 +315,9 @@ class RequestViewerHook(PecanHook):
|
||||
|
||||
if self.headers:
|
||||
for h in headers:
|
||||
key = str(h[0])
|
||||
key = str(h[0])
|
||||
value = str(h[1])
|
||||
name = 'X-Pecan-%s' % key
|
||||
name = 'X-Pecan-%s' % key
|
||||
state.response.headers[name] = value
|
||||
|
||||
def get_controller(self, state):
|
||||
@@ -310,5 +336,3 @@ class RequestViewerHook(PecanHook):
|
||||
'''
|
||||
str_hooks = [str(i).split()[0].strip('<') for i in hooks]
|
||||
return [i.split('.')[-1] for i in str_hooks if '.' in i]
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
try:
|
||||
from simplejson import JSONEncoder
|
||||
except ImportError: # pragma: no cover
|
||||
from json import JSONEncoder
|
||||
except ImportError: # pragma: no cover
|
||||
from json import JSONEncoder # noqa
|
||||
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
@@ -20,15 +20,16 @@ from simplegeneric import generic
|
||||
|
||||
try:
|
||||
from sqlalchemy.engine.base import ResultProxy, RowProxy
|
||||
except ImportError: #pragma no cover
|
||||
except ImportError: # pragma no cover
|
||||
# dummy classes since we don't have SQLAlchemy installed
|
||||
class ResultProxy: pass
|
||||
class RowProxy: pass
|
||||
class ResultProxy: pass # noqa
|
||||
class RowProxy: pass # noqa
|
||||
|
||||
#
|
||||
# exceptions
|
||||
#
|
||||
|
||||
|
||||
class JsonEncodeError(Exception):
|
||||
pass
|
||||
|
||||
@@ -73,16 +74,18 @@ class GenericJSON(JSONEncoder):
|
||||
|
||||
_default = GenericJSON()
|
||||
|
||||
|
||||
@generic
|
||||
def jsonify(obj):
|
||||
return _default.default(obj)
|
||||
|
||||
|
||||
class GenericFunctionJSON(GenericJSON):
|
||||
def default(self, obj):
|
||||
return jsonify(obj)
|
||||
|
||||
_instance = GenericFunctionJSON()
|
||||
|
||||
|
||||
|
||||
def encode(obj):
|
||||
return _instance.encode(obj)
|
||||
|
||||
@@ -12,12 +12,12 @@ class RestController(object):
|
||||
to implement a REST controller. A set of custom actions can also
|
||||
be specified. For more details, see :ref:`pecan_rest`.
|
||||
'''
|
||||
|
||||
|
||||
_custom_actions = {}
|
||||
|
||||
|
||||
@expose()
|
||||
def _route(self, args):
|
||||
|
||||
|
||||
# convention uses "_method" to handle browser-unsupported methods
|
||||
if request.environ.get('pecan.validation_redirected', False) == True:
|
||||
#
|
||||
@@ -28,7 +28,7 @@ class RestController(object):
|
||||
method = request.method.lower()
|
||||
else:
|
||||
method = request.params.get('_method', request.method).lower()
|
||||
|
||||
|
||||
# make sure DELETE/PUT requests don't use GET
|
||||
if request.method == 'GET' and method in ('delete', 'put'):
|
||||
abort(405)
|
||||
@@ -37,23 +37,23 @@ class RestController(object):
|
||||
result = self._find_sub_controllers(args)
|
||||
if result:
|
||||
return result
|
||||
|
||||
|
||||
# handle the request
|
||||
handler = getattr(self, '_handle_%s' % method, self._handle_custom)
|
||||
result = handler(method, args)
|
||||
|
||||
|
||||
# return the result
|
||||
return result
|
||||
|
||||
|
||||
def _find_controller(self, *args):
|
||||
for name in args:
|
||||
obj = getattr(self, name, None)
|
||||
if obj and iscontroller(obj):
|
||||
return obj
|
||||
return None
|
||||
|
||||
|
||||
def _find_sub_controllers(self, remainder):
|
||||
|
||||
|
||||
# need either a get_one or get to parse args
|
||||
method = None
|
||||
for name in ('get_one', 'get'):
|
||||
@@ -62,12 +62,14 @@ class RestController(object):
|
||||
break
|
||||
if not method:
|
||||
return
|
||||
|
||||
|
||||
# get the args to figure out how much to chop off
|
||||
args = getargspec(getattr(self, method))
|
||||
fixed_args = len(args[0][1:]) - len(request.pecan.get('routing_args', []))
|
||||
fixed_args = len(args[0][1:]) - len(
|
||||
request.pecan.get('routing_args', [])
|
||||
)
|
||||
var_args = args[1]
|
||||
|
||||
|
||||
# attempt to locate a sub-controller
|
||||
if var_args:
|
||||
for i, item in enumerate(remainder):
|
||||
@@ -75,20 +77,25 @@ class RestController(object):
|
||||
if controller and not ismethod(controller):
|
||||
self._set_routing_args(remainder[:i])
|
||||
return lookup_controller(controller, remainder[i + 1:])
|
||||
elif fixed_args < len(remainder) and hasattr(self, remainder[fixed_args]):
|
||||
elif fixed_args < len(remainder) and hasattr(
|
||||
self, remainder[fixed_args]
|
||||
):
|
||||
controller = getattr(self, remainder[fixed_args])
|
||||
if not ismethod(controller):
|
||||
self._set_routing_args(remainder[:fixed_args])
|
||||
return lookup_controller(controller, remainder[fixed_args + 1:])
|
||||
|
||||
return lookup_controller(
|
||||
controller,
|
||||
remainder[fixed_args + 1:]
|
||||
)
|
||||
|
||||
def _handle_custom(self, method, remainder):
|
||||
|
||||
|
||||
# try finding a post_{custom} or {custom} method first
|
||||
controller = self._find_controller('post_%s' % method, method)
|
||||
if controller:
|
||||
return controller, remainder
|
||||
|
||||
# if no controller exists, try routing to a sub-controller; note that
|
||||
|
||||
# if no controller exists, try routing to a sub-controller; note that
|
||||
# since this isn't a safe GET verb, any local exposes are 405'd
|
||||
if remainder:
|
||||
if self._find_controller(remainder[0]):
|
||||
@@ -96,18 +103,18 @@ class RestController(object):
|
||||
sub_controller = getattr(self, remainder[0], None)
|
||||
if sub_controller:
|
||||
return lookup_controller(sub_controller, remainder[1:])
|
||||
|
||||
|
||||
abort(404)
|
||||
|
||||
|
||||
def _handle_get(self, method, remainder):
|
||||
|
||||
|
||||
# route to a get_all or get if no additional parts are available
|
||||
if not remainder:
|
||||
controller = self._find_controller('get_all', 'get')
|
||||
if controller:
|
||||
return controller, []
|
||||
abort(404)
|
||||
|
||||
|
||||
# check for new/edit/delete GET requests
|
||||
method_name = remainder[-1]
|
||||
if method_name in ('new', 'edit', 'delete'):
|
||||
@@ -116,31 +123,34 @@ class RestController(object):
|
||||
controller = self._find_controller(method_name)
|
||||
if controller:
|
||||
return controller, remainder[:-1]
|
||||
|
||||
|
||||
# check for custom GET requests
|
||||
if method.upper() in self._custom_actions.get(method_name, []):
|
||||
controller = self._find_controller('get_%s' % method_name, method_name)
|
||||
controller = self._find_controller(
|
||||
'get_%s' % method_name,
|
||||
method_name
|
||||
)
|
||||
if controller:
|
||||
return controller, remainder[:-1]
|
||||
controller = getattr(self, remainder[0], None)
|
||||
if controller and not ismethod(controller):
|
||||
return lookup_controller(controller, remainder[1:])
|
||||
|
||||
|
||||
# finally, check for the regular get_one/get requests
|
||||
controller = self._find_controller('get_one', 'get')
|
||||
if controller:
|
||||
return controller, remainder
|
||||
|
||||
|
||||
abort(404)
|
||||
|
||||
|
||||
def _handle_delete(self, method, remainder):
|
||||
|
||||
|
||||
# check for post_delete/delete requests first
|
||||
controller = self._find_controller('post_delete', 'delete')
|
||||
if controller:
|
||||
return controller, remainder
|
||||
|
||||
# if no controller exists, try routing to a sub-controller; note that
|
||||
|
||||
# if no controller exists, try routing to a sub-controller; note that
|
||||
# since this is a DELETE verb, any local exposes are 405'd
|
||||
if remainder:
|
||||
if self._find_controller(remainder[0]):
|
||||
@@ -148,30 +158,33 @@ class RestController(object):
|
||||
sub_controller = getattr(self, remainder[0], None)
|
||||
if sub_controller:
|
||||
return lookup_controller(sub_controller, remainder[1:])
|
||||
|
||||
|
||||
abort(404)
|
||||
|
||||
|
||||
def _handle_post(self, method, remainder):
|
||||
|
||||
# check for custom POST/PUT requests
|
||||
if remainder:
|
||||
method_name = remainder[-1]
|
||||
if method.upper() in self._custom_actions.get(method_name, []):
|
||||
controller = self._find_controller('%s_%s' % (method, method_name), method_name)
|
||||
controller = self._find_controller(
|
||||
'%s_%s' % (method, method_name),
|
||||
method_name
|
||||
)
|
||||
if controller:
|
||||
return controller, remainder[:-1]
|
||||
controller = getattr(self, remainder[0], None)
|
||||
if controller and not ismethod(controller):
|
||||
return lookup_controller(controller, remainder[1:])
|
||||
|
||||
|
||||
# check for regular POST/PUT requests
|
||||
controller = self._find_controller(method)
|
||||
if controller:
|
||||
return controller, remainder
|
||||
|
||||
|
||||
abort(404)
|
||||
|
||||
|
||||
_handle_put = _handle_post
|
||||
|
||||
|
||||
def _set_routing_args(self, args):
|
||||
request.pecan.setdefault('routing_args', []).extend(args)
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
from webob import exc
|
||||
from inspect import ismethod
|
||||
|
||||
from secure import handle_security, cross_boundary
|
||||
from util import iscontroller
|
||||
|
||||
__all__ = ['lookup_controller', 'find_object']
|
||||
|
||||
|
||||
class NonCanonicalPath(Exception):
|
||||
def __init__(self, controller, remainder):
|
||||
self.controller = controller
|
||||
self.remainder = remainder
|
||||
|
||||
|
||||
def lookup_controller(obj, url_path):
|
||||
remainder = url_path
|
||||
notfound_handlers = []
|
||||
@@ -40,8 +41,9 @@ def lookup_controller(obj, url_path):
|
||||
break
|
||||
except TypeError, te:
|
||||
import warnings
|
||||
msg = 'Got exception calling lookup(): %s (%s)'
|
||||
warnings.warn(
|
||||
'Got exception calling lookup(): %s (%s)' % (te, te.args),
|
||||
msg % (te, te.args),
|
||||
RuntimeWarning
|
||||
)
|
||||
else:
|
||||
@@ -51,19 +53,22 @@ def lookup_controller(obj, url_path):
|
||||
def find_object(obj, remainder, notfound_handlers):
|
||||
prev_obj = None
|
||||
while True:
|
||||
if obj is None: raise exc.HTTPNotFound
|
||||
if iscontroller(obj): return obj, remainder
|
||||
if obj is None:
|
||||
raise exc.HTTPNotFound
|
||||
if iscontroller(obj):
|
||||
return obj, remainder
|
||||
|
||||
# are we traversing to another controller
|
||||
cross_boundary(prev_obj, obj)
|
||||
|
||||
|
||||
if remainder and remainder[0] == '':
|
||||
index = getattr(obj, 'index', None)
|
||||
if iscontroller(index): return index, remainder[1:]
|
||||
if iscontroller(index):
|
||||
return index, remainder[1:]
|
||||
elif not remainder:
|
||||
# the URL has hit an index method without a trailing slash
|
||||
index = getattr(obj, 'index', None)
|
||||
if iscontroller(index):
|
||||
if iscontroller(index):
|
||||
raise NonCanonicalPath(index, remainder[1:])
|
||||
default = getattr(obj, '_default', None)
|
||||
if iscontroller(default):
|
||||
@@ -72,14 +77,15 @@ def find_object(obj, remainder, notfound_handlers):
|
||||
lookup = getattr(obj, '_lookup', None)
|
||||
if iscontroller(lookup):
|
||||
notfound_handlers.append(('_lookup', lookup, remainder))
|
||||
|
||||
|
||||
route = getattr(obj, '_route', None)
|
||||
if iscontroller(route):
|
||||
next, next_remainder = route(remainder)
|
||||
cross_boundary(route, next)
|
||||
return next, next_remainder
|
||||
|
||||
if not remainder: raise exc.HTTPNotFound
|
||||
|
||||
if not remainder:
|
||||
raise exc.HTTPNotFound
|
||||
next, remainder = remainder[0], remainder[1:]
|
||||
prev_obj = obj
|
||||
obj = getattr(obj, next, None)
|
||||
|
||||
@@ -6,23 +6,28 @@ from util import _cfg, iscontroller
|
||||
|
||||
__all__ = ['unlocked', 'secure', 'SecureController']
|
||||
|
||||
|
||||
class _SecureState(object):
|
||||
def __init__(self, desc, boolean_value):
|
||||
self.description = desc
|
||||
self.boolean_value = boolean_value
|
||||
|
||||
def __repr__(self):
|
||||
return '<SecureState %s>' % self.description
|
||||
|
||||
def __nonzero__(self):
|
||||
return self.boolean_value
|
||||
|
||||
Any = _SecureState('Any', False)
|
||||
Protected = _SecureState('Protected', True)
|
||||
|
||||
# security method decorators
|
||||
|
||||
# security method decorators
|
||||
def _unlocked_method(func):
|
||||
_cfg(func)['secured'] = Any
|
||||
return func
|
||||
|
||||
|
||||
def _secure_method(check_permissions_func):
|
||||
def wrap(func):
|
||||
cfg = _cfg(func)
|
||||
@@ -31,6 +36,7 @@ def _secure_method(check_permissions_func):
|
||||
return func
|
||||
return wrap
|
||||
|
||||
|
||||
# classes to assist with wrapping attributes
|
||||
class _UnlockedAttribute(object):
|
||||
def __init__(self, obj):
|
||||
@@ -41,6 +47,7 @@ class _UnlockedAttribute(object):
|
||||
def _lookup(self, *remainder):
|
||||
return self.obj, remainder
|
||||
|
||||
|
||||
class _SecuredAttribute(object):
|
||||
def __init__(self, obj, check_permissions):
|
||||
self.obj = obj
|
||||
@@ -55,6 +62,7 @@ class _SecuredAttribute(object):
|
||||
|
||||
def __get_parent(self):
|
||||
return self._parent
|
||||
|
||||
def __set_parent(self, parent):
|
||||
if ismethod(parent):
|
||||
self._parent = parent.im_self
|
||||
@@ -67,13 +75,15 @@ class _SecuredAttribute(object):
|
||||
def _lookup(self, *remainder):
|
||||
return self.obj, remainder
|
||||
|
||||
|
||||
# helper for secure decorator
|
||||
def _allowed_check_permissions_types(x):
|
||||
return (ismethod(x) or
|
||||
isfunction(x) or
|
||||
return (ismethod(x) or
|
||||
isfunction(x) or
|
||||
isinstance(x, basestring)
|
||||
)
|
||||
|
||||
|
||||
# methods that can either decorate functions or wrap classes
|
||||
# these should be the main methods used for securing or unlocking
|
||||
def unlocked(func_or_obj):
|
||||
@@ -102,13 +112,15 @@ def secure(func_or_obj, check_permissions_for_obj=None):
|
||||
return _secure_method(func_or_obj)
|
||||
else:
|
||||
if not _allowed_check_permissions_types(check_permissions_for_obj):
|
||||
raise TypeError, "When securing an object, secure() requires the second argument to be method"
|
||||
msg = "When securing an object, secure() requires the " + \
|
||||
"second argument to be method"
|
||||
raise TypeError(msg)
|
||||
return _SecuredAttribute(func_or_obj, check_permissions_for_obj)
|
||||
|
||||
|
||||
class SecureController(object):
|
||||
"""
|
||||
Used to apply security to a controller.
|
||||
Used to apply security to a controller.
|
||||
Implementations of SecureController should extend the
|
||||
`check_permissions` method to return a True or False
|
||||
value (depending on whether or not the user has permissions
|
||||
@@ -116,15 +128,23 @@ class SecureController(object):
|
||||
"""
|
||||
class __metaclass__(type):
|
||||
def __init__(cls, name, bases, dict_):
|
||||
cls._pecan = dict(secured=Protected, check_permissions=cls.check_permissions, unlocked=[])
|
||||
cls._pecan = dict(
|
||||
secured=Protected,
|
||||
check_permissions=cls.check_permissions,
|
||||
unlocked=[]
|
||||
)
|
||||
|
||||
for name, value in getmembers(cls):
|
||||
if ismethod(value):
|
||||
if iscontroller(value) and value._pecan.get('secured') is None:
|
||||
if iscontroller(value) and value._pecan.get(
|
||||
'secured'
|
||||
) is None:
|
||||
value._pecan['secured'] = Protected
|
||||
value._pecan['check_permissions'] = cls.check_permissions
|
||||
value._pecan['check_permissions'] = \
|
||||
cls.check_permissions
|
||||
elif hasattr(value, '__class__'):
|
||||
if name.startswith('__') and name.endswith('__'): continue
|
||||
if name.startswith('__') and name.endswith('__'):
|
||||
continue
|
||||
if isinstance(value, _UnlockedAttribute):
|
||||
# mark it as unlocked and remove wrapper
|
||||
cls._pecan['unlocked'].append(value.obj)
|
||||
@@ -132,7 +152,7 @@ class SecureController(object):
|
||||
elif isinstance(value, _SecuredAttribute):
|
||||
# The user has specified a different check_permissions
|
||||
# than the class level version. As far as the class
|
||||
# is concerned, this method is unlocked because
|
||||
# is concerned, this method is unlocked because
|
||||
# it is using a check_permissions function embedded in
|
||||
# the _SecuredAttribute wrapper
|
||||
cls._pecan['unlocked'].append(value)
|
||||
@@ -141,6 +161,7 @@ class SecureController(object):
|
||||
def check_permissions(cls):
|
||||
return False
|
||||
|
||||
|
||||
# methods to evaluate security during routing
|
||||
def handle_security(controller):
|
||||
""" Checks the security of a controller. """
|
||||
@@ -153,6 +174,7 @@ def handle_security(controller):
|
||||
if not check_permissions():
|
||||
raise exc.HTTPUnauthorized
|
||||
|
||||
|
||||
def cross_boundary(prev_obj, obj):
|
||||
""" Check permissions as we move between object instances. """
|
||||
if prev_obj is None:
|
||||
|
||||
@@ -2,6 +2,7 @@ from paste.script.templates import Template
|
||||
|
||||
DEFAULT_TEMPLATE = 'base'
|
||||
|
||||
|
||||
class BaseTemplate(Template):
|
||||
summary = 'Template for creating a basic Pecan project'
|
||||
_template_dir = 'project'
|
||||
|
||||
@@ -7,10 +7,11 @@ error_formatters = []
|
||||
# JSON rendering engine
|
||||
#
|
||||
|
||||
|
||||
class JsonRenderer(object):
|
||||
def __init__(self, path, extra_vars):
|
||||
pass
|
||||
|
||||
|
||||
def render(self, template_path, namespace):
|
||||
from jsonify import encode
|
||||
return encode(namespace)
|
||||
@@ -20,7 +21,7 @@ _builtin_renderers['json'] = JsonRenderer
|
||||
|
||||
#
|
||||
# Genshi rendering engine
|
||||
#
|
||||
#
|
||||
|
||||
try:
|
||||
from genshi.template import (TemplateLoader,
|
||||
@@ -30,21 +31,21 @@ try:
|
||||
def __init__(self, path, extra_vars):
|
||||
self.loader = TemplateLoader([path], auto_reload=True)
|
||||
self.extra_vars = extra_vars
|
||||
|
||||
|
||||
def render(self, template_path, namespace):
|
||||
tmpl = self.loader.load(template_path)
|
||||
stream = tmpl.generate(**self.extra_vars.make_ns(namespace))
|
||||
return stream.render('html')
|
||||
|
||||
_builtin_renderers['genshi'] = GenshiRenderer
|
||||
|
||||
|
||||
def format_genshi_error(exc_value):
|
||||
if isinstance(exc_value, (gTemplateError)):
|
||||
retval = '<h4>Genshi error %s</h4>' % cgi.escape(exc_value.message)
|
||||
retval += format_line_context(exc_value.filename, exc_value.lineno)
|
||||
return retval
|
||||
error_formatters.append(format_genshi_error)
|
||||
except ImportError: #pragma no cover
|
||||
except ImportError: # pragma no cover
|
||||
pass
|
||||
|
||||
|
||||
@@ -59,9 +60,12 @@ try:
|
||||
|
||||
class MakoRenderer(object):
|
||||
def __init__(self, path, extra_vars):
|
||||
self.loader = TemplateLookup(directories=[path], output_encoding='utf-8')
|
||||
self.loader = TemplateLookup(
|
||||
directories=[path],
|
||||
output_encoding='utf-8'
|
||||
)
|
||||
self.extra_vars = extra_vars
|
||||
|
||||
|
||||
def render(self, template_path, namespace):
|
||||
tmpl = self.loader.get_template(template_path)
|
||||
return tmpl.render(**self.extra_vars.make_ns(namespace))
|
||||
@@ -116,36 +120,43 @@ try:
|
||||
_builtin_renderers['jinja'] = JinjaRenderer
|
||||
|
||||
def format_jinja_error(exc_value):
|
||||
retval = '<h4>Jinja2 error in \'%s\' on line %d</h4><div>%s</div>'
|
||||
if isinstance(exc_value, (jTemplateSyntaxError)):
|
||||
retval = '<h4>Jinja2 template syntax error in \'%s\' on line %d</h4><div>%s</div>' % (exc_value.name, exc_value.lineno, exc_value.message)
|
||||
retval = retval % (
|
||||
exc_value.name,
|
||||
exc_value.lineno,
|
||||
exc_value.message
|
||||
)
|
||||
retval += format_line_context(exc_value.filename, exc_value.lineno)
|
||||
return retval
|
||||
error_formatters.append(format_jinja_error)
|
||||
except ImportError: # pragma no cover
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# format helper function
|
||||
#
|
||||
def format_line_context(filename, lineno, context=10):
|
||||
lines = open(filename).readlines()
|
||||
|
||||
lineno = lineno - 1 # files are indexed by 1 not 0
|
||||
lineno = lineno - 1 # files are indexed by 1 not 0
|
||||
if lineno > 0:
|
||||
start_lineno = max(lineno-context, 0)
|
||||
end_lineno = lineno+context
|
||||
start_lineno = max(lineno - context, 0)
|
||||
end_lineno = lineno + context
|
||||
|
||||
lines = [cgi.escape(l) for l in lines[start_lineno:end_lineno]]
|
||||
i = lineno-start_lineno
|
||||
i = lineno - start_lineno
|
||||
lines[i] = '<strong>%s</strong>' % lines[i]
|
||||
|
||||
else:
|
||||
lines = [cgi.escape(l) for l in lines[:context]]
|
||||
msg = '<pre style="background-color:#ccc;padding:2em;">%s</pre>'
|
||||
return msg % ''.join(lines)
|
||||
|
||||
return '<pre style="background-color:#ccc;padding:2em;">%s</pre>' % ''.join(lines)
|
||||
|
||||
#
|
||||
# Extra Vars Rendering
|
||||
# Extra Vars Rendering
|
||||
#
|
||||
class ExtraNamespace(object):
|
||||
def __init__(self, extras={}):
|
||||
@@ -163,6 +174,7 @@ class ExtraNamespace(object):
|
||||
else:
|
||||
return ns
|
||||
|
||||
|
||||
#
|
||||
# Rendering Factory
|
||||
#
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pecan import load_app
|
||||
from webtest import TestApp
|
||||
|
||||
|
||||
def load_test_app(config):
|
||||
return TestApp(load_app(config))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,11 +6,12 @@ from pecan import conf as _runtime_conf
|
||||
|
||||
__here__ = os.path.dirname(__file__)
|
||||
|
||||
|
||||
class TestConf(TestCase):
|
||||
|
||||
def test_update_config_fail_identifier(self):
|
||||
"""Fail when naming does not pass correctness"""
|
||||
bad_dict = {'bad name':'value'}
|
||||
bad_dict = {'bad name': 'value'}
|
||||
self.assertRaises(ValueError, configuration.Config, bad_dict)
|
||||
|
||||
def test_update_set_config(self):
|
||||
@@ -46,7 +47,7 @@ class TestConf(TestCase):
|
||||
|
||||
self.assertEqual(conf.server.host, '0.0.0.0')
|
||||
self.assertEqual(conf.server.port, '8080')
|
||||
|
||||
|
||||
def test_update_force_dict(self):
|
||||
"""Update an empty configuration with the default values"""
|
||||
conf = configuration.initconf()
|
||||
@@ -66,18 +67,21 @@ class TestConf(TestCase):
|
||||
self.assertTrue(isinstance(conf.beaker, dict))
|
||||
self.assertEqual(conf.beaker['session.key'], 'key')
|
||||
self.assertEqual(conf.beaker['session.type'], 'cookie')
|
||||
self.assertEqual(conf.beaker['session.validate_key'], '1a971a7df182df3e1dec0af7c6913ec7')
|
||||
self.assertEqual(
|
||||
conf.beaker['session.validate_key'],
|
||||
'1a971a7df182df3e1dec0af7c6913ec7'
|
||||
)
|
||||
self.assertEqual(conf.beaker.get('__force_dict__'), None)
|
||||
|
||||
def test_update_config_with_dict(self):
|
||||
conf = configuration.initconf()
|
||||
d = {'attr':True}
|
||||
d = {'attr': True}
|
||||
conf['attr'] = d
|
||||
self.assertTrue(conf.attr.attr)
|
||||
|
||||
def test_config_repr(self):
|
||||
conf = configuration.Config({'a':1})
|
||||
self.assertEqual(repr(conf),"Config({'a': 1})")
|
||||
conf = configuration.Config({'a': 1})
|
||||
self.assertEqual(repr(conf), "Config({'a': 1})")
|
||||
|
||||
def test_config_from_dict(self):
|
||||
conf = configuration.conf_from_dict({})
|
||||
@@ -85,7 +89,9 @@ class TestConf(TestCase):
|
||||
self.assertTrue(os.path.samefile(conf['path'], os.getcwd()))
|
||||
|
||||
def test_config_from_file(self):
|
||||
path = os.path.join(os.path.dirname(__file__), 'test_config', 'config.py')
|
||||
path = os.path.join(
|
||||
os.path.dirname(__file__), 'test_config', 'config.py'
|
||||
)
|
||||
conf = configuration.conf_from_file(path)
|
||||
self.assertTrue(conf.app.debug)
|
||||
|
||||
@@ -99,7 +105,7 @@ class TestConf(TestCase):
|
||||
|
||||
def test_config_missing_file(self):
|
||||
path = ('doesnotexist.py',)
|
||||
conf = configuration.Config({})
|
||||
configuration.Config({})
|
||||
self.assertRaises(IOError, configuration.conf_from_file, os.path.join(
|
||||
__here__,
|
||||
'test_config',
|
||||
@@ -108,7 +114,7 @@ class TestConf(TestCase):
|
||||
|
||||
def test_config_missing_file_on_path(self):
|
||||
path = ('bad', 'bad', 'doesnotexist.py',)
|
||||
conf = configuration.Config({})
|
||||
configuration.Config({})
|
||||
|
||||
self.assertRaises(IOError, configuration.conf_from_file, os.path.join(
|
||||
__here__,
|
||||
@@ -118,34 +124,41 @@ class TestConf(TestCase):
|
||||
|
||||
def test_config_with_syntax_error(self):
|
||||
path = ('bad', 'syntaxerror.py')
|
||||
conf = configuration.Config({})
|
||||
configuration.Config({})
|
||||
|
||||
self.assertRaises(SyntaxError, configuration.conf_from_file, os.path.join(
|
||||
__here__,
|
||||
'test_config',
|
||||
*path
|
||||
))
|
||||
self.assertRaises(
|
||||
SyntaxError,
|
||||
configuration.conf_from_file,
|
||||
os.path.join(__here__, 'test_config', *path)
|
||||
)
|
||||
|
||||
def test_config_with_bad_import(self):
|
||||
path = ('bad', 'importerror.py')
|
||||
conf = configuration.Config({})
|
||||
configuration.Config({})
|
||||
|
||||
self.assertRaises(ImportError, configuration.conf_from_file, os.path.join(
|
||||
__here__,
|
||||
'test_config',
|
||||
*path
|
||||
))
|
||||
self.assertRaises(
|
||||
ImportError,
|
||||
configuration.conf_from_file,
|
||||
os.path.join(
|
||||
__here__,
|
||||
'test_config',
|
||||
*path
|
||||
)
|
||||
)
|
||||
|
||||
def test_config_set_from_file(self):
|
||||
path = os.path.join(os.path.dirname(__file__), 'test_config', 'empty.py')
|
||||
path = os.path.join(
|
||||
os.path.dirname(__file__), 'test_config', 'empty.py'
|
||||
)
|
||||
configuration.set_config(path)
|
||||
assert list(_runtime_conf.server) == list(configuration.initconf().server)
|
||||
res = list(configuration.initconf().server)
|
||||
assert list(_runtime_conf.server) == res
|
||||
|
||||
def test_config_dir(self):
|
||||
if sys.version_info >= (2, 6):
|
||||
conf = configuration.Config({})
|
||||
self.assertEqual([], dir(conf))
|
||||
conf = configuration.Config({'a':1})
|
||||
conf = configuration.Config({'a': 1})
|
||||
self.assertEqual(['a'], dir(conf))
|
||||
|
||||
def test_config_bad_key(self):
|
||||
@@ -173,36 +186,35 @@ class TestConf(TestCase):
|
||||
as_dict = conf.as_dict()
|
||||
|
||||
assert isinstance(as_dict, dict)
|
||||
assert as_dict['server']['host'] == '0.0.0.0'
|
||||
assert as_dict['server']['port'] == '8080'
|
||||
assert as_dict['app']['debug'] == False
|
||||
assert as_dict['app']['errors'] == {}
|
||||
assert as_dict['server']['host'] == '0.0.0.0'
|
||||
assert as_dict['server']['port'] == '8080'
|
||||
assert as_dict['app']['debug'] == False
|
||||
assert as_dict['app']['errors'] == {}
|
||||
assert as_dict['app']['force_canonical'] == True
|
||||
assert as_dict['app']['modules'] == []
|
||||
assert as_dict['app']['root'] == None
|
||||
assert as_dict['app']['static_root'] == 'public'
|
||||
assert as_dict['app']['template_path'] == ''
|
||||
assert as_dict['app']['modules'] == []
|
||||
assert as_dict['app']['root'] == None
|
||||
assert as_dict['app']['static_root'] == 'public'
|
||||
assert as_dict['app']['template_path'] == ''
|
||||
|
||||
def test_config_as_dict_nested(self):
|
||||
"""have more than one level nesting and convert to dict"""
|
||||
conf = configuration.initconf()
|
||||
nested = {'one':{'two':2}}
|
||||
nested = {'one': {'two': 2}}
|
||||
conf['nested'] = nested
|
||||
|
||||
as_dict = conf.as_dict()
|
||||
|
||||
assert isinstance(as_dict, dict)
|
||||
assert as_dict['server']['host'] == '0.0.0.0'
|
||||
assert as_dict['server']['port'] == '8080'
|
||||
assert as_dict['app']['debug'] == False
|
||||
assert as_dict['app']['errors'] == {}
|
||||
assert as_dict['server']['host'] == '0.0.0.0'
|
||||
assert as_dict['server']['port'] == '8080'
|
||||
assert as_dict['app']['debug'] == False
|
||||
assert as_dict['app']['errors'] == {}
|
||||
assert as_dict['app']['force_canonical'] == True
|
||||
assert as_dict['app']['modules'] == []
|
||||
assert as_dict['app']['root'] == None
|
||||
assert as_dict['app']['static_root'] == 'public'
|
||||
assert as_dict['app']['template_path'] == ''
|
||||
assert as_dict['nested']['one']['two'] == 2
|
||||
|
||||
assert as_dict['app']['modules'] == []
|
||||
assert as_dict['app']['root'] == None
|
||||
assert as_dict['app']['static_root'] == 'public'
|
||||
assert as_dict['app']['template_path'] == ''
|
||||
assert as_dict['nested']['one']['two'] == 2
|
||||
|
||||
def test_config_as_dict_prefixed(self):
|
||||
"""Add a prefix for keys"""
|
||||
@@ -213,13 +225,12 @@ class TestConf(TestCase):
|
||||
as_dict = conf.as_dict('prefix_')
|
||||
|
||||
assert isinstance(as_dict, dict)
|
||||
assert as_dict['prefix_server']['prefix_host'] == '0.0.0.0'
|
||||
assert as_dict['prefix_server']['prefix_port'] == '8080'
|
||||
assert as_dict['prefix_app']['prefix_debug'] == False
|
||||
assert as_dict['prefix_app']['prefix_errors'] == {}
|
||||
assert as_dict['prefix_server']['prefix_host'] == '0.0.0.0'
|
||||
assert as_dict['prefix_server']['prefix_port'] == '8080'
|
||||
assert as_dict['prefix_app']['prefix_debug'] == False
|
||||
assert as_dict['prefix_app']['prefix_errors'] == {}
|
||||
assert as_dict['prefix_app']['prefix_force_canonical'] == True
|
||||
assert as_dict['prefix_app']['prefix_modules'] == []
|
||||
assert as_dict['prefix_app']['prefix_root'] == None
|
||||
assert as_dict['prefix_app']['prefix_static_root'] == 'public'
|
||||
assert as_dict['prefix_app']['prefix_template_path'] == ''
|
||||
|
||||
assert as_dict['prefix_app']['prefix_modules'] == []
|
||||
assert as_dict['prefix_app']['prefix_root'] == None
|
||||
assert as_dict['prefix_app']['prefix_static_root'] == 'public'
|
||||
assert as_dict['prefix_app']['prefix_template_path'] == ''
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
if false
|
||||
var = 3
|
||||
|
||||
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
|
||||
# Server Specific Configurations
|
||||
server = {
|
||||
'port' : '8081',
|
||||
'host' : '1.1.1.1',
|
||||
'port': '8081',
|
||||
'host': '1.1.1.1',
|
||||
'hostport': '{pecan.conf.server.host}:{pecan.conf.server.port}'
|
||||
}
|
||||
|
||||
# Pecan Application Configurations
|
||||
app = {
|
||||
'static_root' : 'public',
|
||||
'template_path' : 'myproject/templates',
|
||||
'debug' : True
|
||||
'static_root': 'public',
|
||||
'template_path': 'myproject/templates',
|
||||
'debug': True
|
||||
}
|
||||
|
||||
# Custom Configurations must be in Python dictionary format::
|
||||
#
|
||||
# foo = {'bar':'baz'}
|
||||
#
|
||||
#
|
||||
# All configurations are accessible at::
|
||||
# pecan.conf
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# Pecan Application Configurations
|
||||
beaker = {
|
||||
'session.key' : 'key',
|
||||
'session.type' : 'cookie',
|
||||
'session.validate_key' : '1a971a7df182df3e1dec0af7c6913ec7',
|
||||
'__force_dict__' : True
|
||||
'session.key': 'key',
|
||||
'session.type': 'cookie',
|
||||
'session.validate_key': '1a971a7df182df3e1dec0af7c6913ec7',
|
||||
'__force_dict__': True
|
||||
}
|
||||
|
||||
# Custom Configurations must be in Python dictionary format::
|
||||
#
|
||||
# foo = {'bar':'baz'}
|
||||
#
|
||||
#
|
||||
# All configurations are accessible at::
|
||||
# pecan.conf
|
||||
|
||||
@@ -1,36 +1,36 @@
|
||||
from pecan import Pecan, expose, request, response, redirect
|
||||
from pecan import Pecan, expose
|
||||
from unittest import TestCase
|
||||
from webtest import TestApp
|
||||
try:
|
||||
from simplejson import dumps
|
||||
except:
|
||||
from json import dumps
|
||||
from json import dumps # noqa
|
||||
|
||||
|
||||
class TestGeneric(TestCase):
|
||||
|
||||
def test_simple_generic(self):
|
||||
|
||||
def test_simple_generic(self):
|
||||
class RootController(object):
|
||||
@expose(generic=True)
|
||||
def index(self):
|
||||
pass
|
||||
|
||||
|
||||
@index.when(method='POST', template='json')
|
||||
def do_post(self):
|
||||
return dict(result='POST')
|
||||
|
||||
|
||||
@index.when(method='GET')
|
||||
def do_get(self):
|
||||
return 'GET'
|
||||
|
||||
|
||||
app = TestApp(Pecan(RootController()))
|
||||
r = app.get('/')
|
||||
assert r.status_int == 200
|
||||
assert r.body == 'GET'
|
||||
|
||||
|
||||
r = app.post('/')
|
||||
assert r.status_int == 200
|
||||
assert r.body == dumps(dict(result='POST'))
|
||||
|
||||
|
||||
r = app.get('/do_get', status=404)
|
||||
assert r.status_int == 404
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,47 +1,48 @@
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
from datetime import datetime, date
|
||||
from decimal import Decimal
|
||||
try:
|
||||
from simplejson import loads
|
||||
from simplejson import loads
|
||||
except:
|
||||
from json import loads
|
||||
from json import loads # noqa
|
||||
try:
|
||||
from sqlalchemy import orm, schema, types
|
||||
from sqlalchemy import orm, schema, types
|
||||
from sqlalchemy.engine import create_engine
|
||||
except ImportError:
|
||||
create_engine = None
|
||||
from unittest import TestCase
|
||||
create_engine = None # noqa
|
||||
from unittest import TestCase
|
||||
|
||||
from pecan.jsonify import jsonify, encode, ResultProxy, RowProxy
|
||||
from pecan import Pecan, expose, request
|
||||
from webtest import TestApp
|
||||
from pecan.jsonify import jsonify, encode, ResultProxy, RowProxy
|
||||
from pecan import Pecan, expose
|
||||
from webtest import TestApp
|
||||
|
||||
from webob.multidict import MultiDict
|
||||
|
||||
|
||||
def make_person():
|
||||
class Person(object):
|
||||
def __init__(self, first_name, last_name):
|
||||
self.first_name = first_name
|
||||
self.last_name = last_name
|
||||
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return '%s %s' % (self.first_name, self.last_name)
|
||||
return Person
|
||||
|
||||
|
||||
def test_simple_rule():
|
||||
def test_simple_rule():
|
||||
Person = make_person()
|
||||
|
||||
|
||||
# create a Person instance
|
||||
p = Person('Jonathan', 'LaCour')
|
||||
|
||||
|
||||
# register a generic JSON rule
|
||||
@jsonify.when_type(Person)
|
||||
def jsonify_person(obj):
|
||||
return dict(
|
||||
name=obj.name
|
||||
)
|
||||
|
||||
|
||||
# encode the object using our new rule
|
||||
result = loads(encode(p))
|
||||
assert result['name'] == 'Jonathan LaCour'
|
||||
@@ -49,40 +50,42 @@ def test_simple_rule():
|
||||
|
||||
|
||||
class TestJsonify(TestCase):
|
||||
|
||||
|
||||
def test_simple_jsonify(self):
|
||||
Person = make_person()
|
||||
|
||||
|
||||
# register a generic JSON rule
|
||||
@jsonify.when_type(Person)
|
||||
def jsonify_person(obj):
|
||||
return dict(
|
||||
name=obj.name
|
||||
)
|
||||
|
||||
|
||||
class RootController(object):
|
||||
@expose('json')
|
||||
def index(self):
|
||||
# create a Person instance
|
||||
p = Person('Jonathan', 'LaCour')
|
||||
return p
|
||||
|
||||
|
||||
app = TestApp(Pecan(RootController()))
|
||||
|
||||
r = app.get('/')
|
||||
assert r.status_int == 200
|
||||
assert loads(r.body) == {'name':'Jonathan LaCour'}
|
||||
assert loads(r.body) == {'name': 'Jonathan LaCour'}
|
||||
|
||||
|
||||
class TestJsonifyGenericEncoder(TestCase):
|
||||
def test_json_callable(self):
|
||||
class JsonCallable(object):
|
||||
def __init__(self, arg):
|
||||
self.arg = arg
|
||||
|
||||
def __json__(self):
|
||||
return {"arg":self.arg}
|
||||
return {"arg": self.arg}
|
||||
|
||||
result = encode(JsonCallable('foo'))
|
||||
assert loads(result) == {'arg':'foo'}
|
||||
assert loads(result) == {'arg': 'foo'}
|
||||
|
||||
def test_datetime(self):
|
||||
today = date.today()
|
||||
@@ -109,20 +112,22 @@ class TestJsonifyGenericEncoder(TestCase):
|
||||
assert loads(result) == {'arg': ['foo', 'bar']}
|
||||
|
||||
def test_fallback_to_builtin_encoder(self):
|
||||
class Foo(object): pass
|
||||
class Foo(object):
|
||||
pass
|
||||
|
||||
self.assertRaises(TypeError, encode, Foo())
|
||||
|
||||
|
||||
class TestJsonifySQLAlchemyGenericEncoder(TestCase):
|
||||
|
||||
|
||||
def setUp(self):
|
||||
if not create_engine:
|
||||
self.create_fake_proxies()
|
||||
else:
|
||||
self.create_sa_proxies()
|
||||
|
||||
|
||||
def create_fake_proxies(self):
|
||||
|
||||
|
||||
# create a fake SA object
|
||||
class FakeSAObject(object):
|
||||
def __init__(self):
|
||||
@@ -131,74 +136,94 @@ class TestJsonifySQLAlchemyGenericEncoder(TestCase):
|
||||
self.id = 1
|
||||
self.first_name = 'Jonathan'
|
||||
self.last_name = 'LaCour'
|
||||
|
||||
|
||||
# create a fake result proxy
|
||||
class FakeResultProxy(ResultProxy):
|
||||
def __init__(self):
|
||||
self.rowcount = -1
|
||||
self.rows = []
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self.rows)
|
||||
|
||||
def append(self, row):
|
||||
self.rows.append(row)
|
||||
|
||||
|
||||
# create a fake row proxy
|
||||
class FakeRowProxy(RowProxy):
|
||||
def __init__(self, arg=None):
|
||||
self.row = dict(arg)
|
||||
|
||||
def __getitem__(self, key):
|
||||
return self.row.__getitem__(key)
|
||||
|
||||
def keys(self):
|
||||
return self.row.keys()
|
||||
|
||||
|
||||
# get the SA objects
|
||||
self.sa_object = FakeSAObject()
|
||||
self.result_proxy = FakeResultProxy()
|
||||
self.result_proxy.append(FakeRowProxy([('id', 1), ('first_name', 'Jonathan'), ('last_name', 'LaCour')]))
|
||||
self.result_proxy.append(FakeRowProxy([('id', 2), ('first_name', 'Yoann'), ('last_name', 'Roman')]))
|
||||
self.row_proxy = FakeRowProxy([('id', 1), ('first_name', 'Jonathan'), ('last_name', 'LaCour')])
|
||||
|
||||
self.result_proxy.append(
|
||||
FakeRowProxy([
|
||||
('id', 1),
|
||||
('first_name', 'Jonathan'),
|
||||
('last_name', 'LaCour')
|
||||
])
|
||||
)
|
||||
self.result_proxy.append(
|
||||
FakeRowProxy([
|
||||
('id', 2), ('first_name', 'Yoann'), ('last_name', 'Roman')
|
||||
]))
|
||||
self.row_proxy = FakeRowProxy([
|
||||
('id', 1), ('first_name', 'Jonathan'), ('last_name', 'LaCour')
|
||||
])
|
||||
|
||||
def create_sa_proxies(self):
|
||||
|
||||
|
||||
# create the table and mapper
|
||||
metadata = schema.MetaData()
|
||||
user_table = schema.Table('user', metadata,
|
||||
user_table = schema.Table('user', metadata,
|
||||
schema.Column('id', types.Integer, primary_key=True),
|
||||
schema.Column('first_name', types.Unicode(25)),
|
||||
schema.Column('last_name', types.Unicode(25)))
|
||||
|
||||
class User(object):
|
||||
pass
|
||||
orm.mapper(User, user_table)
|
||||
|
||||
|
||||
# create the session
|
||||
engine = create_engine('sqlite:///:memory:')
|
||||
metadata.bind = engine
|
||||
metadata.create_all()
|
||||
session = orm.sessionmaker(bind=engine)()
|
||||
|
||||
|
||||
# add some dummy data
|
||||
user_table.insert().execute([
|
||||
{'first_name': u'Jonathan', 'last_name': u'LaCour'},
|
||||
{'first_name': u'Yoann', 'last_name': u'Roman'}
|
||||
])
|
||||
|
||||
|
||||
# get the SA objects
|
||||
self.sa_object = session.query(User).first()
|
||||
select = user_table.select()
|
||||
self.result_proxy = select.execute()
|
||||
self.row_proxy = select.execute().fetchone()
|
||||
|
||||
|
||||
def test_sa_object(self):
|
||||
result = encode(self.sa_object)
|
||||
assert loads(result) == {'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour'}
|
||||
|
||||
assert loads(result) == {
|
||||
'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour'
|
||||
}
|
||||
|
||||
def test_result_proxy(self):
|
||||
result = encode(self.result_proxy)
|
||||
assert loads(result) == {'count': 2, 'rows': [
|
||||
{'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour'},
|
||||
{'id': 2, 'first_name': 'Yoann', 'last_name': 'Roman'}
|
||||
]}
|
||||
|
||||
|
||||
def test_row_proxy(self):
|
||||
result = encode(self.row_proxy)
|
||||
assert loads(result) == {'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour'}
|
||||
assert loads(result) == {
|
||||
'id': 1, 'first_name': 'Jonathan', 'last_name': 'LaCour'
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from pecan import expose, make_app
|
||||
from pecan.secure import secure, unlocked, SecureController, Protected
|
||||
from pecan.secure import secure, unlocked, SecureController
|
||||
from webtest import TestApp
|
||||
|
||||
try:
|
||||
@@ -9,6 +9,7 @@ try:
|
||||
except:
|
||||
from sets import Set as set
|
||||
|
||||
|
||||
class TestSecure(TestCase):
|
||||
def test_simple_secure(self):
|
||||
authorized = False
|
||||
@@ -17,49 +18,48 @@ class TestSecure(TestCase):
|
||||
@expose()
|
||||
def index(self):
|
||||
return 'Index'
|
||||
|
||||
|
||||
@expose()
|
||||
@unlocked
|
||||
def allowed(self):
|
||||
return 'Allowed!'
|
||||
|
||||
|
||||
@classmethod
|
||||
def check_permissions(cls):
|
||||
return authorized
|
||||
|
||||
|
||||
class RootController(object):
|
||||
@expose()
|
||||
def index(self):
|
||||
return 'Hello, World!'
|
||||
|
||||
|
||||
@expose()
|
||||
@secure(lambda: False)
|
||||
def locked(self):
|
||||
return 'No dice!'
|
||||
|
||||
|
||||
@expose()
|
||||
@secure(lambda: True)
|
||||
def unlocked(self):
|
||||
return 'Sure thing'
|
||||
|
||||
|
||||
secret = SecretController()
|
||||
|
||||
|
||||
app = TestApp(make_app(RootController(), static_root='tests/static'))
|
||||
response = app.get('/')
|
||||
assert response.status_int == 200
|
||||
assert response.body == 'Hello, World!'
|
||||
|
||||
|
||||
response = app.get('/unlocked')
|
||||
assert response.status_int == 200
|
||||
assert response.body == 'Sure thing'
|
||||
|
||||
|
||||
response = app.get('/locked', expect_errors=True)
|
||||
assert response.status_int == 401
|
||||
|
||||
|
||||
response = app.get('/secret/', expect_errors=True)
|
||||
assert response.status_int == 401
|
||||
|
||||
|
||||
response = app.get('/secret/allowed')
|
||||
assert response.status_int == 200
|
||||
assert response.body == 'Allowed!'
|
||||
@@ -70,7 +70,7 @@ class TestSecure(TestCase):
|
||||
def index(self):
|
||||
return 'Index'
|
||||
|
||||
@expose()
|
||||
@expose()
|
||||
def allowed(self):
|
||||
return 'Allowed!'
|
||||
|
||||
@@ -103,7 +103,6 @@ class TestSecure(TestCase):
|
||||
|
||||
secret = SecretController()
|
||||
|
||||
|
||||
app = TestApp(make_app(RootController(), static_root='tests/static'))
|
||||
response = app.get('/')
|
||||
assert response.status_int == 200
|
||||
@@ -122,11 +121,11 @@ class TestSecure(TestCase):
|
||||
response = app.get('/secret/allowed')
|
||||
assert response.status_int == 200
|
||||
assert response.body == 'Allowed!'
|
||||
|
||||
|
||||
response = app.get('/secret/authorized/')
|
||||
assert response.status_int == 200
|
||||
assert response.body == 'Index'
|
||||
|
||||
assert response.body == 'Index'
|
||||
|
||||
response = app.get('/secret/authorized/allowed')
|
||||
assert response.status_int == 200
|
||||
assert response.body == 'Allowed!'
|
||||
@@ -168,26 +167,29 @@ class TestSecure(TestCase):
|
||||
assert bool(Protected) is True
|
||||
|
||||
def test_secure_obj_only_failure(self):
|
||||
class Foo(object): pass
|
||||
class Foo(object):
|
||||
pass
|
||||
|
||||
try:
|
||||
secure(Foo())
|
||||
except Exception, e:
|
||||
assert isinstance(e, TypeError)
|
||||
|
||||
|
||||
class TestObjectPathSecurity(TestCase):
|
||||
def setUp(self):
|
||||
permissions_checked = set()
|
||||
|
||||
class DeepSecretController(SecureController):
|
||||
authorized = False
|
||||
|
||||
@expose()
|
||||
@unlocked
|
||||
def _lookup(self, someID, *remainder):
|
||||
if someID == 'notfound':
|
||||
return None
|
||||
return SubController(someID), remainder
|
||||
|
||||
|
||||
@expose()
|
||||
def index(self):
|
||||
return 'Deep Secret'
|
||||
@@ -224,7 +226,10 @@ class TestObjectPathSecurity(TestCase):
|
||||
def independent(self):
|
||||
return 'Independent Security'
|
||||
|
||||
wrapped = secure(SubController('wrapped'), 'independent_check_permissions')
|
||||
wrapped = secure(
|
||||
SubController('wrapped'), 'independent_check_permissions'
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def check_permissions(cls):
|
||||
permissions_checked.add('secretcontroller')
|
||||
@@ -244,7 +249,6 @@ class TestObjectPathSecurity(TestCase):
|
||||
|
||||
unlocked = unlocked(SubController('unlocked'))
|
||||
|
||||
|
||||
class RootController(object):
|
||||
secret = SecretController()
|
||||
notsecret = NotSecretController()
|
||||
@@ -253,7 +257,9 @@ class TestObjectPathSecurity(TestCase):
|
||||
self.secret_cls = SecretController
|
||||
|
||||
self.permissions_checked = permissions_checked
|
||||
self.app = TestApp(make_app(RootController(), static_root='tests/static'))
|
||||
self.app = TestApp(
|
||||
make_app(RootController(), static_root='tests/static')
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
self.permissions_checked.clear()
|
||||
@@ -280,7 +286,9 @@ class TestObjectPathSecurity(TestCase):
|
||||
assert response.status_int == 404
|
||||
|
||||
def test_secret_through_lookup(self):
|
||||
response = self.app.get('/notsecret/hi/deepsecret/', expect_errors=True)
|
||||
response = self.app.get(
|
||||
'/notsecret/hi/deepsecret/', expect_errors=True
|
||||
)
|
||||
assert response.status_int == 401
|
||||
|
||||
def test_layered_protection(self):
|
||||
@@ -316,13 +324,17 @@ class TestObjectPathSecurity(TestCase):
|
||||
assert response.body == 'Index 2'
|
||||
assert 'deepsecret' not in self.permissions_checked
|
||||
|
||||
response = self.app.get('/notsecret/1/deepsecret/notfound/', expect_errors=True)
|
||||
response = self.app.get(
|
||||
'/notsecret/1/deepsecret/notfound/', expect_errors=True
|
||||
)
|
||||
assert response.status_int == 404
|
||||
assert 'deepsecret' not in self.permissions_checked
|
||||
|
||||
def test_mixed_protection(self):
|
||||
self.secret_cls.authorized = True
|
||||
response = self.app.get('/secret/1/deepsecret/notfound/', expect_errors=True)
|
||||
response = self.app.get(
|
||||
'/secret/1/deepsecret/notfound/', expect_errors=True
|
||||
)
|
||||
assert response.status_int == 404
|
||||
assert 'secretcontroller' in self.permissions_checked
|
||||
assert 'deepsecret' not in self.permissions_checked
|
||||
|
||||
@@ -3,14 +3,15 @@ from pecan import expose, make_app
|
||||
from unittest import TestCase
|
||||
from webtest import TestApp
|
||||
|
||||
|
||||
class TestStatic(TestCase):
|
||||
|
||||
def test_simple_static(self):
|
||||
|
||||
def test_simple_static(self):
|
||||
class RootController(object):
|
||||
@expose()
|
||||
def index(self):
|
||||
return 'Hello, World!'
|
||||
|
||||
|
||||
# make sure Cascade is working properly
|
||||
text = os.path.join(os.path.dirname(__file__), 'static/text.txt')
|
||||
static_root = os.path.join(os.path.dirname(__file__), 'static')
|
||||
@@ -19,7 +20,7 @@ class TestStatic(TestCase):
|
||||
response = app.get('/index.html')
|
||||
assert response.status_int == 200
|
||||
assert response.body == 'Hello, World!'
|
||||
|
||||
|
||||
# get a static resource
|
||||
response = app.get('/text.txt')
|
||||
assert response.status_int == 200
|
||||
|
||||
@@ -2,9 +2,9 @@ from unittest import TestCase
|
||||
|
||||
from pecan.templating import RendererFactory, format_line_context
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
|
||||
class TestTemplate(TestCase):
|
||||
def setUp(self):
|
||||
self.rf = RendererFactory()
|
||||
@@ -21,14 +21,14 @@ class TestTemplate(TestCase):
|
||||
self.assertEqual(extra_vars.make_ns({}), {})
|
||||
|
||||
extra_vars.update({'foo': 1})
|
||||
self.assertEqual(extra_vars.make_ns({}), {'foo':1})
|
||||
self.assertEqual(extra_vars.make_ns({}), {'foo': 1})
|
||||
|
||||
def test_update_extra_vars(self):
|
||||
extra_vars = self.rf.extra_vars
|
||||
extra_vars.update({'foo': 1})
|
||||
|
||||
self.assertEqual(extra_vars.make_ns({'bar':2}), {'foo':1, 'bar':2})
|
||||
self.assertEqual(extra_vars.make_ns({'foo':2}), {'foo':2})
|
||||
self.assertEqual(extra_vars.make_ns({'bar': 2}), {'foo': 1, 'bar': 2})
|
||||
self.assertEqual(extra_vars.make_ns({'foo': 2}), {'foo': 2})
|
||||
|
||||
|
||||
class TestTemplateLineFormat(TestCase):
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from pecan.util import compat_splitext
|
||||
|
||||
|
||||
def test_compat_splitext():
|
||||
assert ('foo', '.bar') == compat_splitext('foo.bar')
|
||||
assert ('/foo/bar', '.txt') == compat_splitext('/foo/bar.txt')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,17 @@
|
||||
import sys
|
||||
import os
|
||||
|
||||
|
||||
def iscontroller(obj):
|
||||
return getattr(obj, 'exposed', False)
|
||||
|
||||
|
||||
def _cfg(f):
|
||||
if not hasattr(f, '_pecan'): f._pecan = {}
|
||||
if not hasattr(f, '_pecan'):
|
||||
f._pecan = {}
|
||||
return f._pecan
|
||||
|
||||
|
||||
def compat_splitext(path):
|
||||
"""
|
||||
This method emulates the behavior os.path.splitext introduced in python 2.6
|
||||
@@ -18,20 +22,24 @@ def compat_splitext(path):
|
||||
if index > 0:
|
||||
root = basename[:index]
|
||||
if root.count('.') != index:
|
||||
return (os.path.join(os.path.dirname(path), root), basename[index:])
|
||||
return (
|
||||
os.path.join(os.path.dirname(path), root),
|
||||
basename[index:]
|
||||
)
|
||||
|
||||
return (path, '')
|
||||
|
||||
|
||||
# use the builtin splitext unless we're python 2.5
|
||||
if sys.version_info >= (2,6):
|
||||
if sys.version_info >= (2, 6):
|
||||
from os.path import splitext
|
||||
else: #pragma no cover
|
||||
splitext = compat_splitext
|
||||
else: # pragma no cover
|
||||
splitext = compat_splitext # noqa
|
||||
|
||||
|
||||
if sys.version_info >=(2,6,5):
|
||||
if sys.version_info >= (2, 6, 5):
|
||||
def encode_if_needed(s):
|
||||
return s
|
||||
else:
|
||||
def encode_if_needed(s):
|
||||
def encode_if_needed(s): # noqa
|
||||
return s.encode('utf-8')
|
||||
|
||||
Reference in New Issue
Block a user