"""
Construct a WSGI router from a configuration file.
A configuration file consists of lines specifying URLs, request methods, and
handlers, allowing construction of :class:`~potpy.wsgi.PathRouter` and
:class:`~potpy.wsgi.MethodRouter` instances using a hierarchical syntax.
At the top level, you specify URLs with optional names and parameter type
converters. See the :class:`~potpy.template.Template` class documentation for
the converter and URL specification format.
::
foo /foo/{foo_id:\d+} (foo_id: int):
...
Following each URL is a list of handlers, one on each line. Handlers may also
specify a name (in parentheses), in which case the result of the handler is
added to the routing context under that name.
::
read_foo (foo)
save_foo
It is also possible to specify request method handlers using ``* METHOD:``
blocks. Adjacent method blocks are combined into a single
:class:`~potpy.wsgi.MethodRouter` instance.
::
* GET, HEAD:
show_foo
* POST:
edit_foo
Exception handlers can be specified for a given handler by ending the handler
line with a colon (``:``) and listing exception types and handlers on the
following lines.
::
read_foo (foo):
ValidationError, BadFooError: show_foo_errors
IOError: show_system_errors
Complete Example::
index /:
* GET, HEAD:
views.index
article /{article_id:\d+} (article_id: int):
* GET, HEAD:
views.show_article
* POST:
auth.require_user (user)
views.edit_article:
views.InvalidArticleError: views.show_article_errors
admin /admin/:
auth.require_admin (user) # run this regardless of request_method
* GET, HEAD:
views.admin_console
"""
import re
import sys
from pkg_resources import resource_stream
from .router import Route
from .wsgi import PathRouter, MethodRouter
_trailing_spaces_or_comment = re.compile(r'\s*(?:#.*)?$')
_tabsp = ' ' * 8
_leading_spaces = re.compile(r'\s*')
_identifier = '[a-zA-Z_][a-zA-Z0-9_]*'
_dotted_identifier = r'%s(?:\.%s)*' % (_identifier, _identifier)
_path_spec = re.compile(
r'(?:(%s)\s+)?(.+?)(?:\s+\((%s:\s*%s(?:,\s*%s:\s*%s)*)\))?:$' % (
_identifier,
_identifier, _dotted_identifier,
_identifier, _dotted_identifier))
_method_name = '[^%s\x7f()<>@,;:\\\\"/[\]?={} \t%s]+' % (
''.join(chr(x) for x in xrange(32)),
''.join(chr(x) for x in xrange(128, 256))
)
_method_spec = re.compile(r'\*\s+(%s(?:,\s*%s)*):$' % (
_method_name, _method_name))
_handler_spec = re.compile(r'(%s)(?:\s+\((%s)\))?:?$' % (
_dotted_identifier, _identifier))
_exc_spec = re.compile(r'(%s(?:,\s*%s)*):\s*(%s)$' % (
_dotted_identifier, _dotted_identifier, _dotted_identifier))
def parse_path_spec(spec):
m = _path_spec.match(spec)
if not m:
raise SyntaxError('expecting path spec')
name, path, types = m.groups()
types = dict(tuple(s.strip() for s in t.split(':'))
for t in types.split(',')) if types else {}
return name, path, types
def parse_method_spec(spec):
m = _method_spec.match(spec)
if not m:
raise SyntaxError('expecting method spec')
return [method.strip() for method in m.group(1).split(',')]
def parse_handler_spec(spec):
m = _handler_spec.match(spec)
if not m:
raise SyntaxError('expecting handler spec')
return tuple(m.groups())
def parse_exception_handler_spec(spec):
m = _exc_spec.match(spec)
if not m:
raise SyntaxError('expecting exception handler spec')
types, handler = m.groups()
types = tuple(t.strip() for t in types.split(','))
return types, handler
def split_indent(line):
spaces = _leading_spaces.match(line).group(0).replace('\t', _tabsp)
trailing = _trailing_spaces_or_comment.search(line)
if trailing:
line = line[:trailing.start()]
return len(spaces), line.strip()
class IndentChecker(object):
def __init__(self, lines):
self.lines = iter(lines)
self.indents = []
self._it = self._iter()
def back(self):
self._yield = True
def _iter(self):
for line in self.lines:
if _trailing_spaces_or_comment.match(line):
continue
self._yield = True
indent, line = split_indent(line)
if not self.indents:
self.indents.append(indent)
if indent > self.indents[-1]:
self.indents.append(indent)
while indent < self.indents[-1]:
self.indents.pop()
if indent > self.indents[-1]:
raise SyntaxError('incorrect indent')
while self._yield:
self._yield = False
yield len(self.indents) - 1, line
def __iter__(self):
return self._it
def find_object(module, name):
path = name.split('.')
try:
obj = getattr(module, path[0])
except AttributeError:
import __builtin__
try:
obj = getattr(__builtin__, path[0])
except AttributeError:
raise LookupError()
for name in path[1:]:
try:
obj = getattr(obj, name)
except AttributeError:
raise LookupError()
return obj
class _scope(object):
def __init__(self, locals, globals):
self.__dict__.update(globals)
self.__dict__.update(locals)
def _calling_scope(depth=1):
try:
frm = sys._getframe(depth)
scope = _scope(frm.f_locals, frm.f_globals)
finally:
del frm
return scope
def read_exception_handler_block(lines, module):
exc_handlers = []
for depth, line in lines:
if depth < 3:
lines.back()
break
types, handler = parse_exception_handler_spec(line)
types = tuple(find_object(module, t) for t in types)
handler = find_object(module, handler)
exc_handlers.append((types, handler))
return exc_handlers
def read_handler_block(lines, module):
handlers = []
method_router = None
last_depth = -1
for depth, line in lines:
if depth < last_depth:
lines.back()
break
last_depth = depth
if _method_spec.match(line):
if method_router is None:
method_router = MethodRouter()
method_router.add(
tuple(parse_method_spec(line)),
read_handler_block(lines, module)
)
else:
if method_router is not None:
handlers.append(method_router)
method_router = None
handler, name = parse_handler_spec(line)
handler = find_object(module, handler)
if line.endswith(':'):
exc_handlers = read_exception_handler_block(lines, module)
else:
exc_handlers = ()
handlers.append((handler, name, exc_handlers))
if method_router is not None:
handlers.append(method_router)
return handlers
[docs]def parse_config(lines, module=None):
"""Parse a config file.
Names referenced within the config file are found within the calling
scope. For example::
>>> from potpy.configparser import parse_config
>>> class foo:
... @staticmethod
... def bar():
... pass
...
>>> config = '''
... /foo:
... foo.bar
... '''
>>> router = parse_config(config.splitlines())
would find the ``bar`` method of the ``foo`` class, because ``foo`` is in
the same scope as the call to parse_config.
:param lines: An iterable of configuration lines (an open file object will
do).
:param module: Optional. If provided and not None, look for referenced
names within this object instead of the calling module.
"""
if module is None:
module = _calling_scope(2)
lines = IndentChecker(lines)
path_router = PathRouter()
for depth, line in lines:
if depth > 0:
raise SyntaxError('unexpected indent')
name, path, types = parse_path_spec(line)
if types:
template_arg = (path, dict(
(k, find_object(module, v))
for k, v in types.iteritems()
))
else:
template_arg = path
handler = read_handler_block(lines, module)
path_router.add(name, template_arg, handler)
return path_router
[docs]def load_config(name='urls.conf'):
"""Load a config from a resource file.
The resource is found using `pkg_resources.resource_stream()`_,
relative to the calling module.
See :func:`parse_config` for config file details.
:param name: The name of the resource, relative to the calling module.
.. _pkg_resources.resource_stream(): http://packages.python.org/distribute/pkg_resources.html#basic-resource-access
"""
module = _calling_scope(2)
config = resource_stream(module.__name__, name)
return parse_config(config, module)