Added a traversal dispatch method and cleaned up the router
* Created a dispatcher protocol for classes to implement for path resolution * Implemented a traversal dispatcher * Updated the router to implement the dispatcher protocol * Added unit tests for both dispatchers * Replaced the Controller class with an Application classmaster
parent
5d4b1cf61f
commit
6cf47a0339
|
@ -1,3 +1,5 @@
|
||||||
'''Milla is an extremely simple WSGI framework for web applications
|
'''Milla is an extremely simple WSGI framework for web applications
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
from app import *
|
||||||
|
from webob.exc import *
|
|
@ -0,0 +1,54 @@
|
||||||
|
'''Module milla.app
|
||||||
|
|
||||||
|
Please give me a docstring!
|
||||||
|
|
||||||
|
:Created: Mar 26, 2011
|
||||||
|
:Author: dustin
|
||||||
|
:Updated: $Date$
|
||||||
|
:Updater: $Author$
|
||||||
|
'''
|
||||||
|
import milla.dispatch.traversal
|
||||||
|
from webob.exc import HTTPNotFound, WSGIHTTPException
|
||||||
|
import webob
|
||||||
|
|
||||||
|
class Application(object):
|
||||||
|
'''Represents a Milla web application
|
||||||
|
|
||||||
|
Constructing an ``Application`` instance needs a dispatcher, or
|
||||||
|
alternatively, a root object that will be passed to a new
|
||||||
|
:py:class:``milla.dispatch.traversal.Traverser`.
|
||||||
|
|
||||||
|
:param root: A root object, passed to a traverser, which is
|
||||||
|
automatically created if a root is given
|
||||||
|
:param dispatcher: An object implementing the dispatcher protocol
|
||||||
|
|
||||||
|
``Application`` instances are WSGI applications
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, root=None, dispatcher=None):
|
||||||
|
if not dispatcher:
|
||||||
|
if root:
|
||||||
|
self.dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
else:
|
||||||
|
raise ValueError('Must specify either a root object or a '
|
||||||
|
'dispatcher')
|
||||||
|
else:
|
||||||
|
self.dispatcher = dispatcher
|
||||||
|
|
||||||
|
def __call__(self, environ, start_response):
|
||||||
|
try:
|
||||||
|
func = self.dispatcher.resolve(environ['PATH_INFO'])
|
||||||
|
except milla.dispatch.UnresolvedPath:
|
||||||
|
return HTTPNotFound()(environ, start_response)
|
||||||
|
|
||||||
|
request = webob.Request(environ)
|
||||||
|
try:
|
||||||
|
response = func(request)
|
||||||
|
except WSGIHTTPException as e:
|
||||||
|
return e(environ, start_response)
|
||||||
|
|
||||||
|
if isinstance(response, basestring):
|
||||||
|
response = webob.Response(response)
|
||||||
|
|
||||||
|
start_response(response.status, response.headerlist)
|
||||||
|
return response.app_iter
|
|
@ -1,28 +0,0 @@
|
||||||
'''Module milla.controller
|
|
||||||
|
|
||||||
Please give me a docstring!
|
|
||||||
|
|
||||||
:Created: Mar 13, 2011
|
|
||||||
:Author: dustin
|
|
||||||
:Updated: $Date$
|
|
||||||
:Updater: $Author$
|
|
||||||
'''
|
|
||||||
import webob
|
|
||||||
import webob.exc
|
|
||||||
|
|
||||||
class Controller(object):
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
|
||||||
request = webob.Request(environ)
|
|
||||||
try:
|
|
||||||
response = getattr(self, request.method)(request, **request.urlvars)
|
|
||||||
except AttributeError:
|
|
||||||
return webob.exc.HTTPMethodNotAllowed()(environ, start_response)
|
|
||||||
except webob.exc.WSGIHTTPException as e:
|
|
||||||
return e(environ, start_response)
|
|
||||||
|
|
||||||
if isinstance(response, basestring):
|
|
||||||
response = webob.Response(response)
|
|
||||||
|
|
||||||
start_response(response.status, response.headerlist)
|
|
||||||
return response.app_iter
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
class UnresolvedPath(Exception):
|
||||||
|
'''Raised when a path cannot be resolved into a handler'''
|
|
@ -0,0 +1,164 @@
|
||||||
|
'''URL router
|
||||||
|
|
||||||
|
:Created: Mar 13, 2011
|
||||||
|
:Author: dustin
|
||||||
|
:Updated: $Date$
|
||||||
|
:Updater: $Author$
|
||||||
|
'''
|
||||||
|
from milla.dispatch import UnresolvedPath
|
||||||
|
import functools
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
class Router(object):
|
||||||
|
'''A dispatcher that maps arbitrary paths to controller callables
|
||||||
|
|
||||||
|
Typical usage::
|
||||||
|
|
||||||
|
router = Router()
|
||||||
|
router.add_route('/foo/{bar}/{baz:\d+}', some_func)
|
||||||
|
app = milla.Application(dispatcher=router)
|
||||||
|
'''
|
||||||
|
|
||||||
|
#: Compiled regular expression for variable segments
|
||||||
|
template_re = re.compile(r'\{(\w+)(?::([^}]+))?\}')
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.routes = []
|
||||||
|
self._cache = {}
|
||||||
|
|
||||||
|
def resolve(self, path_info):
|
||||||
|
'''Find a controller for a given path
|
||||||
|
|
||||||
|
:param path_info: Path for which to locate a controller
|
||||||
|
:returns: A :py:class:`functools.partial` instance that sets
|
||||||
|
the values collected from variable segments as keyword
|
||||||
|
arguments to the callable
|
||||||
|
|
||||||
|
This method walks through the routing table created with calls
|
||||||
|
to :py:meth:`add_route` and finds the first whose template
|
||||||
|
matches the given path. Variable segments are added as keywords
|
||||||
|
to the controller function.
|
||||||
|
'''
|
||||||
|
try:
|
||||||
|
return self._cache[path_info]
|
||||||
|
except KeyError:
|
||||||
|
for regex, controller, vars in self.routes:
|
||||||
|
match = regex.match(path_info)
|
||||||
|
if match:
|
||||||
|
urlvars = match.groupdict()
|
||||||
|
urlvars.update(vars)
|
||||||
|
func = functools.partial(controller, **urlvars)
|
||||||
|
func.__name__ = controller.__name__
|
||||||
|
func.__doc__ = controller.__doc__
|
||||||
|
self._cache[path_info] = func
|
||||||
|
return func
|
||||||
|
raise UnresolvedPath
|
||||||
|
|
||||||
|
def _compile_template(self, template):
|
||||||
|
'''Compiles a template into a real regular expression
|
||||||
|
|
||||||
|
:param template: A route template string
|
||||||
|
|
||||||
|
Converts the ``{name}`` or ``{name:regexp}`` syntax into a full
|
||||||
|
regular expression for later parsing.
|
||||||
|
'''
|
||||||
|
|
||||||
|
regex = ''
|
||||||
|
last_pos = 0
|
||||||
|
for match in self.template_re.finditer(template):
|
||||||
|
regex += re.escape(template[last_pos:match.start()])
|
||||||
|
var_name = match.group(1)
|
||||||
|
expr = match.group(2) or '[^/]+'
|
||||||
|
expr = '(?P<%s>%s)' % (var_name, expr)
|
||||||
|
regex += expr
|
||||||
|
last_pos = match.end()
|
||||||
|
regex += re.escape(template[last_pos:])
|
||||||
|
regex = '^%s$' % regex
|
||||||
|
return re.compile(regex)
|
||||||
|
|
||||||
|
def _import_controller(self, name):
|
||||||
|
'''Resolves a string Python path to a callable'''
|
||||||
|
|
||||||
|
module_name, func_name = name.split(':', 1)
|
||||||
|
__import__(module_name)
|
||||||
|
module = sys.modules[module_name]
|
||||||
|
func = getattr(module, func_name)
|
||||||
|
return func
|
||||||
|
|
||||||
|
def add_route(self, template, controller, **vars):
|
||||||
|
'''Add a route to the routing table
|
||||||
|
|
||||||
|
:param template: Route template string
|
||||||
|
:param controller: Controller callable or string Python path
|
||||||
|
|
||||||
|
Route template strings are path segments, beginning with ``/``.
|
||||||
|
Paths can also contain variable segments, delimited with curly
|
||||||
|
braces.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
/some/other/{variable}/{path}
|
||||||
|
|
||||||
|
By default, variable segments will match any character except a
|
||||||
|
``/``. Alternate expressions can be passed by specifying them
|
||||||
|
alongside the name, separated by a ``:``.
|
||||||
|
|
||||||
|
Example::
|
||||||
|
|
||||||
|
/some/other/{alternate:[a-zA-Z]}
|
||||||
|
|
||||||
|
Variable path segments will be passed as keywords to the
|
||||||
|
controller. In the first example above, assuming ``controller``
|
||||||
|
is the name of the callable passed, and the request path was
|
||||||
|
``/some/other/great/place``::
|
||||||
|
|
||||||
|
controller(request, variable='great', path='place')
|
||||||
|
|
||||||
|
The ``controller`` argument itself can be any callable that
|
||||||
|
accepts a *WebOb* request as its first argument, and any
|
||||||
|
keywords that may be passed from variable segments. It can
|
||||||
|
also be a string Python path to such a callable. For example::
|
||||||
|
|
||||||
|
`some.module:function`
|
||||||
|
|
||||||
|
This string will resolve to the function ``function`` in the
|
||||||
|
module ``some.module``.
|
||||||
|
'''
|
||||||
|
|
||||||
|
if isinstance(controller, basestring):
|
||||||
|
controller = self._import_controller(controller)
|
||||||
|
self.routes.append((self._compile_template(template),
|
||||||
|
controller, vars))
|
||||||
|
|
||||||
|
class Generator(object):
|
||||||
|
'''URL generator
|
||||||
|
|
||||||
|
Creates URL references based on a *WebOb* request.
|
||||||
|
|
||||||
|
Typical usage:
|
||||||
|
|
||||||
|
>>> generator = Generator(request)
|
||||||
|
>>> generator.generate('foo', 'bar')
|
||||||
|
'/foo/bar'
|
||||||
|
|
||||||
|
A common pattern is to wrap this in a stub function::
|
||||||
|
|
||||||
|
url = Generator(request).generate
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, request):
|
||||||
|
self.request = request
|
||||||
|
|
||||||
|
def generate(self, *segments, **vars):
|
||||||
|
'''Combines segments and the application's URL into a new URL
|
||||||
|
'''
|
||||||
|
|
||||||
|
base_url = self.request.application_url
|
||||||
|
path = '/'.join(str(s) for s in segments)
|
||||||
|
if not path.startswith('/'):
|
||||||
|
path = '/' + path
|
||||||
|
if vars:
|
||||||
|
path += '?' + urllib.urlencode(vars)
|
||||||
|
return base_url + path
|
|
@ -0,0 +1,60 @@
|
||||||
|
'''URL Dispatching
|
||||||
|
|
||||||
|
:Created: Mar 26, 2011
|
||||||
|
:Author: dustin
|
||||||
|
:Updated: $Date$
|
||||||
|
:Updater: $Author$
|
||||||
|
'''
|
||||||
|
from milla.dispatch import UnresolvedPath
|
||||||
|
|
||||||
|
class Traverser(object):
|
||||||
|
'''Default URL dispatcher
|
||||||
|
|
||||||
|
:param root: The root object at which lookup will begin
|
||||||
|
|
||||||
|
The default URL dispatcher uses object attribute traversal to
|
||||||
|
locate a handler for a given path. For example, consider the
|
||||||
|
following class::
|
||||||
|
|
||||||
|
class Root(object):
|
||||||
|
|
||||||
|
def foo(self):
|
||||||
|
return 'Hello, world!'
|
||||||
|
|
||||||
|
The path ``/foo`` would resolve to the ``foo`` method of the
|
||||||
|
``Root`` class.
|
||||||
|
|
||||||
|
If a path cannot be resolved, :py:exc:`UnresolvedPath` will be
|
||||||
|
raised.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
|
||||||
|
def resolve(self, path_info):
|
||||||
|
'''Find a handler given a path
|
||||||
|
|
||||||
|
:param path_info: Path for which to find a handler
|
||||||
|
:returns: A handler callable
|
||||||
|
'''
|
||||||
|
|
||||||
|
def walk_path(handler, parts):
|
||||||
|
if not parts or not parts[0]:
|
||||||
|
# No more parts, or the last part is blank, we're done
|
||||||
|
return handler
|
||||||
|
try:
|
||||||
|
return walk_path(getattr(handler, parts[0]), parts[1:])
|
||||||
|
except AttributeError:
|
||||||
|
# The handler doesn't have an attribute with the current
|
||||||
|
# segment value, try the default
|
||||||
|
try:
|
||||||
|
return handler.default
|
||||||
|
except AttributeError:
|
||||||
|
# No default either, can't resolve
|
||||||
|
raise UnresolvedPath
|
||||||
|
|
||||||
|
# Strip the leading slash and split the path
|
||||||
|
split_path = path_info.lstrip('/').split('/')
|
||||||
|
|
||||||
|
handler = walk_path(self.root, split_path)
|
||||||
|
return handler
|
|
@ -1,71 +0,0 @@
|
||||||
'''URL router
|
|
||||||
|
|
||||||
TODO: Document me!
|
|
||||||
|
|
||||||
:Created: Mar 13, 2011
|
|
||||||
:Author: dustin
|
|
||||||
:Updated: $Date$
|
|
||||||
:Updater: $Author$
|
|
||||||
'''
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import urllib
|
|
||||||
import webob
|
|
||||||
|
|
||||||
class Router(object):
|
|
||||||
|
|
||||||
template_re = re.compile(r'\{(\w+)(?::([^}]+))?\}')
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.routes = []
|
|
||||||
|
|
||||||
def __call__(self, environ, start_response):
|
|
||||||
request = webob.Request(environ)
|
|
||||||
for regex, controller, vars in self.routes:
|
|
||||||
match = regex.match(request.path_info)
|
|
||||||
if match:
|
|
||||||
request.urlvars = match.groupdict()
|
|
||||||
request.urlvars.update(vars)
|
|
||||||
return controller(environ, start_response)
|
|
||||||
return webob.exc.HTTPNotFound()(environ, start_response)
|
|
||||||
|
|
||||||
def _compile_template(self, template):
|
|
||||||
regex = ''
|
|
||||||
last_pos = 0
|
|
||||||
for match in self.template_re.finditer(template):
|
|
||||||
regex += re.escape(template[last_pos:match.start()])
|
|
||||||
var_name = match.group(1)
|
|
||||||
expr = match.group(2) or '[^/]+'
|
|
||||||
expr = '(?P<%s>%s)' % (var_name, expr)
|
|
||||||
regex += expr
|
|
||||||
last_pos = match.end()
|
|
||||||
regex += re.escape(template[last_pos:])
|
|
||||||
regex = '^%s$' % regex
|
|
||||||
return re.compile(regex)
|
|
||||||
|
|
||||||
def _import_controller(self, name):
|
|
||||||
module_name, func_name = name.split(':', 1)
|
|
||||||
__import__(module_name)
|
|
||||||
module = sys.modules[module_name]
|
|
||||||
func = getattr(module, func_name)
|
|
||||||
return func
|
|
||||||
|
|
||||||
def add_route(self, template, controller, **vars):
|
|
||||||
if isinstance(controller, basestring):
|
|
||||||
controller = self._import_controller(controller)
|
|
||||||
self.routes.append((self._compile_template(template),
|
|
||||||
controller, vars))
|
|
||||||
|
|
||||||
class Generator(object):
|
|
||||||
|
|
||||||
def __init__(self, request):
|
|
||||||
self.request = request
|
|
||||||
|
|
||||||
def generate(self, *segments, **vars):
|
|
||||||
base_url = self.request.application_url
|
|
||||||
path = '/'.join(str(s) for s in segments)
|
|
||||||
if not path.startswith('/'):
|
|
||||||
path = '/' + path
|
|
||||||
if vars:
|
|
||||||
path += '?' + urllib.urlencode(vars)
|
|
||||||
return base_url + path
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
'''Tests for the routing URL dispatcher
|
||||||
|
|
||||||
|
:Created: Mar 26, 2011
|
||||||
|
:Author: dustin
|
||||||
|
:Updated: $Date$
|
||||||
|
:Updater: $Author$
|
||||||
|
'''
|
||||||
|
import milla.dispatch.routing
|
||||||
|
|
||||||
|
def fake_controller():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_static():
|
||||||
|
'''Ensure the dispatcher can resolve a static path
|
||||||
|
|
||||||
|
Given the path ``/foo/bar/baz`` and a route for the exact same
|
||||||
|
path, the resolver should return the controller mapped to the
|
||||||
|
route.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def controller():
|
||||||
|
pass
|
||||||
|
|
||||||
|
router = milla.dispatch.routing.Router()
|
||||||
|
router.add_route('/foo/bar/baz', controller)
|
||||||
|
func = router.resolve('/foo/bar/baz')
|
||||||
|
assert func.func == controller
|
||||||
|
|
||||||
|
def test_urlvars():
|
||||||
|
'''Ensure the dispatcher can resolve a path with variable segments
|
||||||
|
|
||||||
|
Given the path ``/foo/abc/def`` and a route ``/foo/{bar}/{baz}``,
|
||||||
|
the resolver should return the controller mapped to the route with
|
||||||
|
preset keywords ``bar='abc', baz='def'``.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def controller():
|
||||||
|
pass
|
||||||
|
|
||||||
|
router = milla.dispatch.routing.Router()
|
||||||
|
router.add_route('/foo/{bar}/{baz}', controller)
|
||||||
|
func = router.resolve('/foo/abc/def')
|
||||||
|
assert func.func == controller
|
||||||
|
assert func.keywords['bar'] == 'abc'
|
||||||
|
assert func.keywords['baz'] == 'def'
|
||||||
|
|
||||||
|
def test_regexp_urlvar():
|
||||||
|
'''Ensure the dispatcher can resolve alternate regexps in urlvars
|
||||||
|
|
||||||
|
Given a route ``/test/{arg:[a-z]+}``, the resolver should return
|
||||||
|
the mapped controller for the path ``/test/abcde``, but not the
|
||||||
|
path ``/test/1234``.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def controller():
|
||||||
|
pass
|
||||||
|
|
||||||
|
router = milla.dispatch.routing.Router()
|
||||||
|
router.add_route('/test/{arg:[a-z]+}', controller)
|
||||||
|
func = router.resolve('/test/abcde')
|
||||||
|
assert func.func == controller
|
||||||
|
assert func.keywords['arg'] == 'abcde'
|
||||||
|
|
||||||
|
try:
|
||||||
|
func = router.resolve('/test/1234')
|
||||||
|
except milla.dispatch.UnresolvedPath:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError
|
||||||
|
|
||||||
|
def test_unresolved():
|
||||||
|
'''Ensure the resolver raises an exception for unresolved paths
|
||||||
|
|
||||||
|
Given a route ``/test``, the resolver should raise
|
||||||
|
:py:exc:`~milla.dispatch.UnresolvedPath` for the path ``/tset``.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def controller():
|
||||||
|
pass
|
||||||
|
|
||||||
|
router = milla.dispatch.routing.Router()
|
||||||
|
router.add_route('/test', controller)
|
||||||
|
try:
|
||||||
|
router.resolve('/tset')
|
||||||
|
except milla.dispatch.UnresolvedPath:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError
|
||||||
|
|
||||||
|
def test_unrelated():
|
||||||
|
'''Ensure the dispatcher is not confused by unrelated paths
|
||||||
|
|
||||||
|
Given routes for ``/testA`` and ``/testB``, the resolver should
|
||||||
|
return the controller mapped to the former for the path ``/testA``,
|
||||||
|
without regard for the latter.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def controller_a():
|
||||||
|
pass
|
||||||
|
|
||||||
|
def controller_b():
|
||||||
|
pass
|
||||||
|
|
||||||
|
router = milla.dispatch.routing.Router()
|
||||||
|
router.add_route('/testA', controller_a)
|
||||||
|
router.add_route('/testB', controller_b)
|
||||||
|
func = router.resolve('/testA')
|
||||||
|
assert func.func == controller_a
|
||||||
|
|
||||||
|
def test_string_controller():
|
||||||
|
'''Ensure the dispatcher can find a controller given a string
|
||||||
|
|
||||||
|
Given a string path to a controller function, the callable defined
|
||||||
|
therein should be returned by the resolver for the corresponding
|
||||||
|
path.
|
||||||
|
'''
|
||||||
|
|
||||||
|
router = milla.dispatch.routing.Router()
|
||||||
|
router.add_route('/test', 'milla.tests.test_routing:fake_controller')
|
||||||
|
func = router.resolve('/test')
|
||||||
|
assert func.func == fake_controller
|
|
@ -0,0 +1,166 @@
|
||||||
|
'''Unit tests for the URL dispatcher
|
||||||
|
|
||||||
|
:Created: Mar 26, 2011
|
||||||
|
:Author: dustin
|
||||||
|
:Updated: $Date$
|
||||||
|
:Updater: $Author$
|
||||||
|
'''
|
||||||
|
import milla.dispatch.traversal
|
||||||
|
|
||||||
|
def test_root():
|
||||||
|
'''Ensure the root path resolves to the root handler
|
||||||
|
|
||||||
|
Given the path ``/``, the resolver should return the root handler,
|
||||||
|
which was given to it at initialization
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Root(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
root = Root()
|
||||||
|
dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
func = dispatcher.resolve('/')
|
||||||
|
assert func == root
|
||||||
|
|
||||||
|
def test_unrelated():
|
||||||
|
'''Ensure unrelated attributes do not confuse the dispatcher
|
||||||
|
|
||||||
|
Given the path ``/`` and a root handler with attributes and
|
||||||
|
methods, the resolver should still return the root handler
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Root(object):
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
foo = 'bar'
|
||||||
|
|
||||||
|
root = Root()
|
||||||
|
dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
func = dispatcher.resolve('/')
|
||||||
|
assert func == root
|
||||||
|
|
||||||
|
def test_unresolved():
|
||||||
|
'''Ensure that the resolver returns remaining parts
|
||||||
|
|
||||||
|
Given the path ``/foo/bar/baz`` and a root handler with no
|
||||||
|
children, the resolver should raise
|
||||||
|
:py:exc:`~milla.dispatch.UnresolvedPath`
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Root(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
root = Root()
|
||||||
|
dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
try:
|
||||||
|
dispatcher.resolve('/foo/bar/baz')
|
||||||
|
except milla.dispatch.UnresolvedPath:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError
|
||||||
|
|
||||||
|
def test_method():
|
||||||
|
'''Ensure the resolver finds an instance method handler
|
||||||
|
|
||||||
|
Given the path ``/test`` and a root handler with an instance
|
||||||
|
method named ``test``, the resolver should return that method.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Root(object):
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
root = Root()
|
||||||
|
dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
func = dispatcher.resolve('/test')
|
||||||
|
assert func == root.test
|
||||||
|
|
||||||
|
def test_nested_class():
|
||||||
|
'''Ensure the resolver finds a nested class handler
|
||||||
|
|
||||||
|
Given the path ``/test`` and a root handler with an inner class
|
||||||
|
named ``test``, the resolver should return the inner class.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Root(object):
|
||||||
|
class test(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
root = Root()
|
||||||
|
dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
func = dispatcher.resolve('/test')
|
||||||
|
assert func == root.test
|
||||||
|
|
||||||
|
def test_nested_class_method():
|
||||||
|
'''Ensure the resolver finds an instance method of a nested class
|
||||||
|
|
||||||
|
Given the path ``/test/test`` and a root handler with an inner
|
||||||
|
class named ``test``, which in turn has an instance method named
|
||||||
|
``test``, the resolver should return the ``test`` method of the
|
||||||
|
inner class.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Root(object):
|
||||||
|
class test(object):
|
||||||
|
def test(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
root = Root()
|
||||||
|
dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
func = dispatcher.resolve('/test/test')
|
||||||
|
assert func == root.test.test
|
||||||
|
|
||||||
|
def test_attribute():
|
||||||
|
'''Ensure the resolver finds a handler in an instance attribute
|
||||||
|
|
||||||
|
Given the path ``/test`` and a root handler with an attribute named
|
||||||
|
``test`` containing another class, the resolver should return that
|
||||||
|
class.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Test(object):
|
||||||
|
pass
|
||||||
|
class Root(object):
|
||||||
|
test = Test()
|
||||||
|
|
||||||
|
root = Root()
|
||||||
|
dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
func = dispatcher.resolve('/test')
|
||||||
|
assert func == Root.test
|
||||||
|
|
||||||
|
def test_default():
|
||||||
|
'''Ensure the resolver finds the default handler
|
||||||
|
|
||||||
|
Given the path ``/test`` and a root handler with a method named
|
||||||
|
``default``, but no method named ``test``, the resolver should
|
||||||
|
return the ``default`` method.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Root(object):
|
||||||
|
def default(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
root = Root()
|
||||||
|
dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
func = dispatcher.resolve('/test')
|
||||||
|
assert func == root.default
|
||||||
|
|
||||||
|
def test_nested_default():
|
||||||
|
'''Ensure the resolver finds a nested default handler
|
||||||
|
|
||||||
|
Given the path ``/test/bar`` and a root handler with a ``test``
|
||||||
|
attribute containing a class instance with a ``default`` method but
|
||||||
|
no ``bar`` method, the resolver should return the ``default``
|
||||||
|
of the nested instance.
|
||||||
|
'''
|
||||||
|
|
||||||
|
class Test(object):
|
||||||
|
def default(self):
|
||||||
|
pass
|
||||||
|
class Root(object):
|
||||||
|
test = Test()
|
||||||
|
|
||||||
|
root = Root()
|
||||||
|
dispatcher = milla.dispatch.traversal.Traverser(root)
|
||||||
|
func = dispatcher.resolve('/test/bar')
|
||||||
|
assert func == root.test.default
|
Loading…
Reference in New Issue