After a full-scale scan with pep8.py and pyflakes, identified and

resolved most of our PEP8 compliance issues.
This commit is contained in:
Jonathan LaCour
2012-03-11 09:52:25 -07:00
parent 20a996b58d
commit bed5cbfa14
34 changed files with 1942 additions and 1591 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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:

View File

@@ -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()

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -1,4 +1,5 @@
from core import load_app
def deploy(config):
return load_app(config)

View File

@@ -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]

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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:

View File

@@ -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'

View File

@@ -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
#

View File

@@ -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

View File

@@ -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'] == ''

View File

@@ -1,3 +1,3 @@
if false
var = 3

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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):

View File

@@ -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

View File

@@ -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')