Skip to content

Commit

Permalink
Merge pull request #116 from coderpete/next
Browse files Browse the repository at this point in the history
API Documentation
  • Loading branch information
ryanpetrello committed Mar 27, 2012
2 parents 264d68d + 98356b0 commit f328aa7
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 9 deletions.
37 changes: 29 additions & 8 deletions pecan/rest.py
Expand Up @@ -12,12 +12,16 @@ 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):

'''
Routes a request to the appropriate controller and returns its result.
Performs a bit of validation - refuses to route delete and put actions
via a GET request).
'''
# convention uses "_method" to handle browser-unsupported methods
if request.environ.get('pecan.validation_redirected', False) == True:
#
Expand Down Expand Up @@ -46,14 +50,20 @@ def _route(self, args):
return result

def _find_controller(self, *args):
'''
Returns the appropriate controller for routing a custom action.
'''
for name in args:
obj = getattr(self, name, None)
if obj and iscontroller(obj):
return obj
return None

def _find_sub_controllers(self, remainder):

'''
Identifies the correct controller to route to by analyzing the
request URI.
'''
# need either a get_one or get to parse args
method = None
for name in ('get_one', 'get'):
Expand Down Expand Up @@ -89,7 +99,9 @@ def _find_sub_controllers(self, remainder):
)

def _handle_custom(self, method, remainder):

'''
Routes ``_custom`` actions to the appropriate controller.
'''
# try finding a post_{custom} or {custom} method first
controller = self._find_controller('post_%s' % method, method)
if controller:
Expand All @@ -107,7 +119,9 @@ def _handle_custom(self, method, remainder):
abort(404)

def _handle_get(self, method, remainder):

'''
Routes ``GET`` actions to the appropriate controller.
'''
# route to a get_all or get if no additional parts are available
if not remainder or remainder == ['']:
controller = self._find_controller('get_all', 'get')
Expand Down Expand Up @@ -144,7 +158,9 @@ def _handle_get(self, method, remainder):
abort(404)

def _handle_delete(self, method, remainder):

'''
Routes ``DELETE`` actions to the appropriate controller.
'''
# check for post_delete/delete requests first
controller = self._find_controller('post_delete', 'delete')
if controller:
Expand All @@ -162,7 +178,9 @@ def _handle_delete(self, method, remainder):
abort(404)

def _handle_post(self, method, remainder):

'''
Routes ``POST`` requests.
'''
# check for custom POST/PUT requests
if remainder:
method_name = remainder[-1]
Expand All @@ -187,4 +205,7 @@ def _handle_post(self, method, remainder):
_handle_put = _handle_post

def _set_routing_args(self, args):
'''
Sets default routing arguments.
'''
request.pecan.setdefault('routing_args', []).extend(args)
16 changes: 16 additions & 0 deletions pecan/routing.py
Expand Up @@ -7,12 +7,23 @@


class NonCanonicalPath(Exception):
'''
Exception Raised when a non-canonical path is encountered when 'walking'
the URI. This is typically a ``POST`` request which requires a trailing
slash.
'''
def __init__(self, controller, remainder):
self.controller = controller
self.remainder = remainder


def lookup_controller(obj, url_path):
'''
Traverses the requested url path and returns the appropriate controller
object, including default routes.
Handles common errors gracefully.
'''
remainder = url_path
notfound_handlers = []

Expand Down Expand Up @@ -51,6 +62,11 @@ def lookup_controller(obj, url_path):


def find_object(obj, remainder, notfound_handlers):
'''
'Walks' the url path in search of an action for which a controller is
implemented and returns that controller object along with what's left
of the remainder.
'''
prev_obj = None
while True:
if obj is None:
Expand Down
84 changes: 83 additions & 1 deletion pecan/templating.py
Expand Up @@ -9,15 +9,22 @@


class JsonRenderer(object):
'''
Defines the builtin ``JSON`` renderer.
'''
def __init__(self, path, extra_vars):
pass

def render(self, template_path, namespace):
'''
Implements ``JSON`` rendering.
'''
from jsonify import encode
return encode(namespace)

_builtin_renderers['json'] = JsonRenderer
# TODO: add error formatter for json (pass it through json lint?)

_builtin_renderers['json'] = JsonRenderer

#
# Genshi rendering engine
Expand All @@ -28,18 +35,27 @@ def render(self, template_path, namespace):
TemplateError as gTemplateError)

class GenshiRenderer(object):
'''
Defines the builtin ``Genshi`` renderer.
'''
def __init__(self, path, extra_vars):
self.loader = TemplateLoader([path], auto_reload=True)
self.extra_vars = extra_vars

def render(self, template_path, namespace):
'''
Implements ``Genshi`` rendering.
'''
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):
'''
Implements ``Genshi`` renderer error formatting.
'''
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)
Expand All @@ -59,6 +75,9 @@ def format_genshi_error(exc_value):
html_error_template

class MakoRenderer(object):
'''
Defines the builtin ``Mako`` renderer.
'''
def __init__(self, path, extra_vars):
self.loader = TemplateLookup(
directories=[path],
Expand All @@ -67,12 +86,18 @@ def __init__(self, path, extra_vars):
self.extra_vars = extra_vars

def render(self, template_path, namespace):
'''
Implements ``Mako`` rendering.
'''
tmpl = self.loader.get_template(template_path)
return tmpl.render(**self.extra_vars.make_ns(namespace))

_builtin_renderers['mako'] = MakoRenderer

def format_mako_error(exc_value):
'''
Implements ``Mako`` renderer error formatting.
'''
if isinstance(exc_value, (CompileException, SyntaxException)):
return html_error_template().render(full=False, css=False)

Expand All @@ -89,11 +114,17 @@ def format_mako_error(exc_value):
from kajiki.loader import FileLoader

class KajikiRenderer(object):
'''
Defines the builtin ``Kajiki`` renderer.
'''
def __init__(self, path, extra_vars):
self.loader = FileLoader(path, reload=True)
self.extra_vars = extra_vars

def render(self, template_path, namespace):
'''
Implements ``Kajiki`` rendering.
'''
Template = self.loader.import_(template_path)
stream = Template(self.extra_vars.make_ns(namespace))
return stream.render()
Expand All @@ -110,16 +141,25 @@ def render(self, template_path, namespace):
from jinja2.exceptions import TemplateSyntaxError as jTemplateSyntaxError

class JinjaRenderer(object):
'''
Defines the builtin ``Jinja`` renderer.
'''
def __init__(self, path, extra_vars):
self.env = Environment(loader=FileSystemLoader(path))
self.extra_vars = extra_vars

def render(self, template_path, namespace):
'''
Implements ``Jinja`` rendering.
'''
template = self.env.get_template(template_path)
return template.render(self.extra_vars.make_ns(namespace))
_builtin_renderers['jinja'] = JinjaRenderer

def format_jinja_error(exc_value):
'''
Implements ``Jinja`` renderer error formatting.
'''
retval = '<h4>Jinja2 error in \'%s\' on line %d</h4><div>%s</div>'
if isinstance(exc_value, (jTemplateSyntaxError)):
retval = retval % (
Expand All @@ -138,6 +178,14 @@ def format_jinja_error(exc_value):
# format helper function
#
def format_line_context(filename, lineno, context=10):
'''
Formats the the line context for error rendering.
:param filename: the location of the file, within which the error occurred
:param lineno: the offending line number
:param context: number of lines of code to display before and after the
offending line.
'''
lines = open(filename).readlines()

lineno = lineno - 1 # files are indexed by 1 not 0
Expand All @@ -159,13 +207,25 @@ def format_line_context(filename, lineno, context=10):
# Extra Vars Rendering
#
class ExtraNamespace(object):
'''
Extra variables for the template namespace to pass to the renderer as named
parameters.
:param extras: dictionary of extra parameters. Defaults to an empty dict.
'''
def __init__(self, extras={}):
self.namespace = dict(extras)

def update(self, d):
'''
Updates the extra variable dictionary for the namespace.
'''
self.namespace.update(d)

def make_ns(self, ns):
'''
Returns the `lazily` created template namespace.
'''
if self.namespace:
val = {}
val.update(self.namespace)
Expand All @@ -179,19 +239,41 @@ def make_ns(self, ns):
# Rendering Factory
#
class RendererFactory(object):
'''
Manufactures known Renderer objects.
:param custom_renderers: custom-defined renderers to manufacture
:param extra_vars: extra vars for the template namespace
'''
def __init__(self, custom_renderers={}, extra_vars={}):
self._renderers = {}
self._renderer_classes = dict(_builtin_renderers)
self.add_renderers(custom_renderers)
self.extra_vars = ExtraNamespace(extra_vars)

def add_renderers(self, custom_dict):
'''
Adds a custom renderer.
:param custom_dict: a dictionary of custom renderers to add
'''
self._renderer_classes.update(custom_dict)

def available(self, name):
'''
Returns true if queried renderer class is available.
:param name: renderer name
'''
return name in self._renderer_classes

def get(self, name, template_path):
'''
Returns the renderer object.
:param name: name of the requested renderer
:param template_path: path to the template
'''
if name not in self._renderers:
cls = self._renderer_classes.get(name)
if cls is None:
Expand Down

0 comments on commit f328aa7

Please sign in to comment.