diff options
| author | Chris McDonough <chrism@agendaless.com> | 2009-06-24 19:23:43 +0000 |
|---|---|---|
| committer | Chris McDonough <chrism@agendaless.com> | 2009-06-24 19:23:43 +0000 |
| commit | 05c02322f5a09c14f49c529d6fd885153e52c66f (patch) | |
| tree | c09c43dc7b82d6cc475648c33313d1c1e495311e | |
| parent | aedb399ccb4da1d055163708894f690bb96685c0 (diff) | |
| download | pyramid-05c02322f5a09c14f49c529d6fd885153e52c66f.tar.gz pyramid-05c02322f5a09c14f49c529d6fd885153e52c66f.tar.bz2 pyramid-05c02322f5a09c14f49c529d6fd885153e52c66f.zip | |
Merge noroutes branch to trunk.
| -rw-r--r-- | repoze/bfg/includes/meta.zcml | 15 | ||||
| -rw-r--r-- | repoze/bfg/interfaces.py | 3 | ||||
| -rw-r--r-- | repoze/bfg/request.py | 4 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_request.py | 34 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_traversal.py | 21 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_url.py | 61 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_urldispatch.py | 128 | ||||
| -rw-r--r-- | repoze/bfg/tests/test_zcml.py | 309 | ||||
| -rw-r--r-- | repoze/bfg/traversal.py | 20 | ||||
| -rw-r--r-- | repoze/bfg/url.py | 97 | ||||
| -rw-r--r-- | repoze/bfg/urldispatch.py | 129 | ||||
| -rw-r--r-- | repoze/bfg/zcml.py | 165 | ||||
| -rw-r--r-- | setup.py | 1 |
13 files changed, 414 insertions, 573 deletions
diff --git a/repoze/bfg/includes/meta.zcml b/repoze/bfg/includes/meta.zcml index 36129c5e3..4acdfe937 100644 --- a/repoze/bfg/includes/meta.zcml +++ b/repoze/bfg/includes/meta.zcml @@ -28,21 +28,12 @@ handler="repoze.bfg.zcml.forbidden" /> - </meta:directives> - - <meta:groupingDirective - namespace="http://namespaces.repoze.org/bfg" + <meta:directive name="route" schema="repoze.bfg.zcml.IRouteDirective" - handler="repoze.bfg.zcml.Route" + handler="repoze.bfg.zcml.route" /> - <meta:directive - name="requirement" - namespace="http://namespaces.repoze.org/bfg" - usedIn="repoze.bfg.zcml.IRouteDirective" - schema="repoze.bfg.zcml.IRouteRequirementDirective" - handler="repoze.bfg.zcml.route_requirement" - /> + </meta:directives> </configure> diff --git a/repoze/bfg/interfaces.py b/repoze/bfg/interfaces.py index b8f62c05f..2dcddd483 100644 --- a/repoze/bfg/interfaces.py +++ b/repoze/bfg/interfaces.py @@ -230,6 +230,9 @@ class IAuthorizationPolicy(Interface): def principals_allowed_by_permission(context, permission): """ Return a set of principal identifiers allowed by the permission """ +class IRequestFactories(Interface): + """ Marker interface for utilities representing request factories """ + # VH_ROOT_KEY is an interface; its imported from other packages (e.g. # traversalwrapper) VH_ROOT_KEY = 'HTTP_X_VHM_ROOT' diff --git a/repoze/bfg/request.py b/repoze/bfg/request.py index 20e8070fc..932789e0a 100644 --- a/repoze/bfg/request.py +++ b/repoze/bfg/request.py @@ -1,3 +1,4 @@ +from zope.component import getUtility from zope.interface import implements from webob import Request as WebobRequest @@ -10,6 +11,7 @@ from repoze.bfg.interfaces import IPOSTRequest from repoze.bfg.interfaces import IPUTRequest from repoze.bfg.interfaces import IDELETERequest from repoze.bfg.interfaces import IHEADRequest +from repoze.bfg.interfaces import IRequestFactories def request_factory(environ): try: @@ -19,7 +21,7 @@ def request_factory(environ): if 'bfg.routes.route' in environ: route = environ['bfg.routes.route'] - request_factories = route.request_factories + request_factories = getUtility(IRequestFactories, name=route.name or '') else: request_factories = DEFAULT_REQUEST_FACTORIES diff --git a/repoze/bfg/tests/test_request.py b/repoze/bfg/tests/test_request.py index d5823e7d7..5f61f4efc 100644 --- a/repoze/bfg/tests/test_request.py +++ b/repoze/bfg/tests/test_request.py @@ -1,4 +1,5 @@ import unittest +from repoze.bfg.testing import cleanUp class TestMakeRequestASCII(unittest.TestCase): def _callFUT(self, event): @@ -103,22 +104,35 @@ class Test_HEADRequest(TestRequestSubclass, unittest.TestCase): return DEFAULT_REQUEST_FACTORIES['HEAD']['interface'] class TestRequestFactory(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + def _callFUT(self, environ): from repoze.bfg.request import request_factory return request_factory(environ) - def _getRequestFactory(self, name_or_iface=None): - from repoze.bfg.request import DEFAULT_REQUEST_FACTORIES - return DEFAULT_REQUEST_FACTORIES[name_or_iface]['factory'] - - def _makeRoute(self): - route = DummyRoute() + def _registerRequestFactories(self, name=''): + from zope.component import getSiteManager + from repoze.bfg.interfaces import IRequestFactories factories = {} def factory(environ): return environ for name in (None, 'GET', 'POST', 'PUT', 'DELETE', 'HEAD'): factories[name] = {'factory':factory} - route.request_factories = factories + sm = getSiteManager() + sm.registerUtility(factories, IRequestFactories, name=name) + if name: + sm.registerUtility(factories, IRequestFactories, name='') + + def _getRequestFactory(self, name_or_iface=None): + from repoze.bfg.request import DEFAULT_REQUEST_FACTORIES + return DEFAULT_REQUEST_FACTORIES[name_or_iface]['factory'] + + def _makeRoute(self, name=None): + route = DummyRoute(name) return route def test_no_route_no_request_method(self): @@ -164,18 +178,21 @@ class TestRequestFactory(unittest.TestCase): self.failUnless(IHEADRequest.providedBy(result)) def test_route_no_request_method(self): + self._registerRequestFactories() route = self._makeRoute() environ = {'bfg.routes.route':route} result = self._callFUT(environ) self.assertEqual(result, environ) def test_route_unknown(self): + self._registerRequestFactories() route = self._makeRoute() environ = {'bfg.routes.route':route, 'REQUEST_METHOD':'UNKNOWN'} result = self._callFUT(environ) self.assertEqual(result, environ) def test_route_known(self): + self._registerRequestFactories() route = self._makeRoute() environ = {'bfg.routes.route':route, 'REQUEST_METHOD':'GET'} result = self._callFUT(environ) @@ -273,7 +290,8 @@ class TestDefaultRequestFactories(unittest.TestCase): class DummyRoute: - pass + def __init__(self, name): + self.name=name class DummyRequest: pass diff --git a/repoze/bfg/tests/test_traversal.py b/repoze/bfg/tests/test_traversal.py index 081105f88..68b920fa4 100644 --- a/repoze/bfg/tests/test_traversal.py +++ b/repoze/bfg/tests/test_traversal.py @@ -785,11 +785,11 @@ class TraversalContextURLTests(unittest.TestCase): one.__name__ = 'one' one.__parent__ = root route = DummyRoute() - route.non_minimized_result = False + route.raise_exc = KeyError request = DummyRequest({'bfg.routes.route':route, 'bfg.routes.matchdict':{'a':1}}) context_url = self._makeOne(one, request) - self.assertRaises(ValueError, context_url) + self.assertRaises(KeyError, context_url) class TestVirtualRoot(unittest.TestCase): def setUp(self): @@ -1005,15 +1005,10 @@ class DummyContextURL: return '123' class DummyRoute: - minimization = False - - minimized_result = 'example/' - non_minimized_result = '/example/' - - def generate_minimized(self, kw): - self.generate_kw = kw - return self.minimized_result - - def generate_non_minimized(self, kw): + result = '/example/' + raise_exc = None + def generate(self, kw): self.generate_kw = kw - return self.non_minimized_result + if self.raise_exc: + raise self.raise_exc + return self.result diff --git a/repoze/bfg/tests/test_url.py b/repoze/bfg/tests/test_url.py index 3c5bd7559..5f0fde872 100644 --- a/repoze/bfg/tests/test_url.py +++ b/repoze/bfg/tests/test_url.py @@ -168,31 +168,40 @@ class TestRouteUrl(unittest.TestCase): from repoze.bfg.url import route_url return route_url(*arg, **kw) - def test_it(self): + def test_with_elements(self): from repoze.bfg.interfaces import IRoutesMapper - mapper = DummyRoutesMapper({'flub':DummyRoute({})}) + mapper = DummyRoutesMapper(result='/1/2/3') from zope.component import getSiteManager sm = getSiteManager() sm.registerUtility(mapper, IRoutesMapper) - args = {'a':'1', 'b':'2', 'c':'3'} - environ = {'SERVER_NAME':'example.com', 'wsgi.url_scheme':'http', - 'SERVER_PORT':'80', 'wsgiorg.routing_args':((), args)} - request = DummyRequest(environ) - result = self._callFUT(request, 'flub', a=1, b=2, c=3) - self.assertEqual(result, 'http://example.com/1/2/3') + request = DummyRequest() + result = self._callFUT('flub', request, 'extra1', 'extra2', + a=1, b=2, c=3, _query={'a':1}, + _anchor=u"foo") + self.assertEqual(result, + 'http://example.com:5432/1/2/3/extra1/extra2?a=1#foo') + + def test_no_elements(self): + from repoze.bfg.interfaces import IRoutesMapper + mapper = DummyRoutesMapper(result='/1/2/3') + from zope.component import getSiteManager + sm = getSiteManager() + sm.registerUtility(mapper, IRoutesMapper) + request = DummyRequest() + result = self._callFUT('flub', request, a=1, b=2, c=3, _query={'a':1}, + _anchor=u"foo") + self.assertEqual(result, + 'http://example.com:5432/1/2/3?a=1#foo') def test_it_generation_error(self): from repoze.bfg.interfaces import IRoutesMapper - mapper = DummyRoutesMapper({'flub':DummyRoute({})}) + mapper = DummyRoutesMapper(raise_exc=KeyError) from zope.component import getSiteManager sm = getSiteManager() sm.registerUtility(mapper, IRoutesMapper) - args = {'a':'1', 'b':'2', 'c':'3'} - mapper.raise_exc = True - environ = {'SERVER_NAME':'example.com', 'wsgi.url_scheme':'http', - 'SERVER_PORT':'80', 'wsgiorg.routing_args':((), args)} - request = DummyRequest(environ) - self.assertRaises(ValueError, self._callFUT, request, 'flub', a=1) + mapper.raise_exc = KeyError + request = DummyRequest() + self.assertRaises(KeyError, self._callFUT, 'flub', request, a=1) class DummyContext(object): def __init__(self, next=None): @@ -206,24 +215,12 @@ class DummyRequest: self.environ = environ class DummyRoutesMapper: - encoding = 'utf-8' - hardcode_names = False - sub_domains = [] - raise_exc = False - def __init__(self, routes, generate_result='/1/2/3', raise_exc=False): - self._routenames = routes - self.generate_result = generate_result + raise_exc = None + def __init__(self, result='/1/2/3', raise_exc=False): + self.result = result def generate(self, *route_args, **newargs): if self.raise_exc: - from routes.util import GenerationException - raise GenerationException - return self.generate_result + raise self.raise_exc + return self.result -class DummyRoute: - filter = None - static = False - def __init__(self, defaults): - self.defaults = defaults - - diff --git a/repoze/bfg/tests/test_urldispatch.py b/repoze/bfg/tests/test_urldispatch.py index 68fda032d..cdb99b7f4 100644 --- a/repoze/bfg/tests/test_urldispatch.py +++ b/repoze/bfg/tests/test_urldispatch.py @@ -1,6 +1,33 @@ import unittest from repoze.bfg.testing import cleanUp +class TestRoute(unittest.TestCase): + def _getTargetClass(self): + from repoze.bfg.urldispatch import Route + return Route + + def _makeOne(self, *arg): + return self._getTargetClass()(*arg) + + def test_ctor(self): + route = self._makeOne('name', 'matcher', 'generator', 'factory') + self.assertEqual(route.name, 'name') + self.assertEqual(route.matcher, 'matcher') + self.assertEqual(route.generator, 'generator') + self.assertEqual(route.factory, 'factory') + + def test_match(self): + def matcher(path): + return 123 + route = self._makeOne('name', matcher, 'generator', 'factory') + self.assertEqual(route.match('whatever'), 123) + + def test_generate(self): + def generator(path): + return 123 + route = self._makeOne('name', 'matcher', generator, 'factory') + self.assertEqual(route.generate({}), 123) + class RoutesRootFactoryTests(unittest.TestCase): def setUp(self): cleanUp() @@ -27,42 +54,40 @@ class RoutesRootFactoryTests(unittest.TestCase): self.assertEqual(mapper.default_root_factory, None) def test_no_route_matches(self): - get_root = make_get_root(123) + get_root = DummyRootFactory(123) mapper = self._makeOne(get_root) environ = self._getEnviron(PATH_INFO='/') result = mapper(environ) self.assertEqual(result, 123) - self.assertEqual(mapper.environ, environ) def test_route_matches(self): - get_root = make_get_root(123) + get_root = DummyRootFactory(123) mapper = self._makeOne(get_root) - mapper.connect('foo', 'archives/:action/:article', foo='foo') + mapper.connect('foo', 'archives/:action/:article') environ = self._getEnviron(PATH_INFO='/archives/action1/article1') result = mapper(environ) self.assertEqual(result, 123) routing_args = environ['wsgiorg.routing_args'][1] - self.assertEqual(routing_args['foo'], 'foo') self.assertEqual(routing_args['action'], 'action1') self.assertEqual(routing_args['article'], 'article1') self.assertEqual(environ['bfg.routes.matchdict'], routing_args) self.assertEqual(environ['bfg.routes.route'].name, 'foo') - def test_unnamed_root_route_matches(self): - root_factory = make_get_root(123) + def test_root_route_matches(self): + root_factory = DummyRootFactory(123) mapper = self._makeOne(root_factory) - mapper.connect('') + mapper.connect('root', '') environ = self._getEnviron(PATH_INFO='/') result = mapper(environ) self.assertEqual(result, 123) - self.assertEqual(environ['bfg.routes.route'].name, None) + self.assertEqual(environ['bfg.routes.route'].name, 'root') self.assertEqual(environ['bfg.routes.matchdict'], {}) self.assertEqual(environ['wsgiorg.routing_args'], ((), {})) - def test_named_root_route_matches(self): - root_factory = make_get_root(123) + def test_root_route_matches2(self): + root_factory = DummyRootFactory(123) mapper = self._makeOne(root_factory) - mapper.connect('root', '') + mapper.connect('root', '/') environ = self._getEnviron(PATH_INFO='/') result = mapper(environ) self.assertEqual(result, 123) @@ -70,25 +95,8 @@ class RoutesRootFactoryTests(unittest.TestCase): self.assertEqual(environ['bfg.routes.matchdict'], {}) self.assertEqual(environ['wsgiorg.routing_args'], ((), {})) - def test_unicode_in_route_default(self): - root_factory = make_get_root(123) - mapper = self._makeOne(root_factory) - class DummyRoute: - routepath = ':id' - _factory = None - la = unicode('\xc3\xb1a', 'utf-8') - mapper.routematch = lambda *arg: ({la:'id'}, DummyRoute) - mapper.connect('whatever', ':la') - environ = self._getEnviron(PATH_INFO='/foo') - result = mapper(environ) - self.assertEqual(result, 123) - self.assertEqual(environ['bfg.routes.route'], DummyRoute) - self.assertEqual(environ['bfg.routes.matchdict'], {u'\xf1a': 'id'}) - routing_args = environ['wsgiorg.routing_args'][1] - self.assertEqual(routing_args[la], 'id') - def test_fallback_to_default_root_factory(self): - root_factory = make_get_root(123) + root_factory = DummyRootFactory(123) mapper = self._makeOne(root_factory) mapper.connect('wont', 'wont/:be/:found') environ = self._getEnviron(PATH_INFO='/archives/action1/article1') @@ -101,20 +109,50 @@ class RoutesRootFactoryTests(unittest.TestCase): mapper.connect('whatever', 'archives/:action/:article') self.assertEqual(mapper.has_routes(), True) - def test_url_for(self): - root_factory = make_get_root(None) - mapper = self._makeOne(root_factory) - mapper.connect('whatever', 'archives/:action/:article') - environ = self._getEnviron(PATH_INFO='/archives/action1/article1') - result = mapper(environ) - from routes import url_for - result = url_for(action='action2', article='article2') - self.assertEqual(result, '/archives/action2/article2') + def test_generate(self): + mapper = self._makeOne(None) + def generator(kw): + return 123 + route = DummyRoute(generator) + mapper.routes['abc'] = route + self.assertEqual(mapper.generate('abc', {}), 123) -def make_get_root(result): - def dummy_get_root(environ): - return result - return dummy_get_root +class TestCompileRoute(unittest.TestCase): + def _callFUT(self, path): + from repoze.bfg.urldispatch import _compile_route + return _compile_route(path) + + def test_no_star(self): + matcher, generator = self._callFUT('/foo/:baz/biz/:buz/bar') + self.assertEqual(matcher('/foo/baz/biz/buz/bar'), + {'baz':'baz', 'buz':'buz'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator({'baz':1, 'buz':2}), '/foo/1/biz/2/bar') + + def test_with_star(self): + matcher, generator = self._callFUT('/foo/:baz/biz/:buz/bar*traverse') + self.assertEqual(matcher('/foo/baz/biz/buz/bar'), + {'baz':'baz', 'buz':'buz', 'traverse':''}) + self.assertEqual(matcher('/foo/baz/biz/buz/bar/everything/else/here'), + {'baz':'baz', 'buz':'buz', + 'traverse':'/everything/else/here'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator( + {'baz':1, 'buz':2, 'traverse':u'/a/b'}), '/foo/1/biz/2/bar/a/b') + + def test_no_beginning_slash(self): + matcher, generator = self._callFUT('foo/:baz/biz/:buz/bar') + self.assertEqual(matcher('/foo/baz/biz/buz/bar'), + {'baz':'baz', 'buz':'buz'}) + self.assertEqual(matcher('foo/baz/biz/buz/bar'), None) + self.assertEqual(generator({'baz':1, 'buz':2}), '/foo/1/biz/2/bar') + + +class DummyRootFactory(object): + def __init__(self, result): + self.result = result + def __call__(self, environ): + return self.result class DummyContext(object): """ """ @@ -122,3 +160,7 @@ class DummyContext(object): class DummyRequest(object): """ """ +class DummyRoute(object): + def __init__(self, generator): + self.generate = generator + diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py index 84c794e89..1a0ee3c3f 100644 --- a/repoze/bfg/tests/test_zcml.py +++ b/repoze/bfg/tests/test_zcml.py @@ -472,12 +472,16 @@ class TestViewDirective(unittest.TestCase): self.assertEqual(regadapt['args'][2], (IFoo, IDummy)) def test_with_route_name(self): + from zope.component import getSiteManager + from repoze.bfg.interfaces import IRequestFactories class IFoo: pass class IDummyRequest: pass context = DummyContext() - context.request_factories = {'foo':{None:{'interface':IDummyRequest}}} + factories = {None:{'interface':IDummyRequest}} + sm = getSiteManager() + sm.registerUtility(factories, IRequestFactories, name='foo') view = lambda *arg: None self._callFUT(context, 'repoze.view', IFoo, view=view, route_name='foo') actions = context.actions @@ -671,19 +675,6 @@ class TestDeriveView(unittest.TestCase): self.failUnless('instance' in result.__name__) self.assertEqual(result(None, None), 'OK') -class TestRouteRequirementFunction(unittest.TestCase): - def _callFUT(self, context, attr, expr): - from repoze.bfg.zcml import route_requirement - return route_requirement(context, attr, expr) - - def test_it(self): - context = DummyContext() - context.context = DummyContext() - context.context.requirements = {} - self._callFUT(context, 'a', 'b') - self.assertEqual(context.context.requirements['a'], 'b') - self.assertRaises(ValueError, self._callFUT, context, 'a', 'b') - class TestConnectRouteFunction(unittest.TestCase): def setUp(self): cleanUp() @@ -691,9 +682,9 @@ class TestConnectRouteFunction(unittest.TestCase): def tearDown(self): cleanUp() - def _callFUT(self, directive): + def _callFUT(self, name, path, factory): from repoze.bfg.zcml import connect_route - return connect_route(directive) + return connect_route(name, path, factory) def _registerRoutesMapper(self): from zope.component import getGlobalSiteManager @@ -703,145 +694,10 @@ class TestConnectRouteFunction(unittest.TestCase): gsm.registerUtility(mapper, IRoutesMapper) return mapper - def test_no_mapper(self): - directive = DummyRouteDirective() - self._callFUT(directive) # doesn't blow up when no routes mapper reg'd - def test_defaults(self): mapper = self._registerRoutesMapper() - directive = DummyRouteDirective() - self._callFUT(directive) - self.assertEqual(len(mapper.connections), 1) - self.assertEqual(mapper.connections[0][0], ('name', 'path')) - self.assertEqual(mapper.connections[0][1], {'requirements': {}}) - - def test_name_and_path(self): - mapper = self._registerRoutesMapper() - directive = DummyRouteDirective(name='abc', path='thepath') - self._callFUT(directive) - self.assertEqual(len(mapper.connections), 1) - self.assertEqual(mapper.connections[0][0], ('abc', 'thepath',)) - self.assertEqual(mapper.connections[0][1], {'requirements': {}}) - - def test_all_directives(self): - mapper = self._registerRoutesMapper() - def foo(): - """ """ - directive = DummyRouteDirective( - minimize=False, explicit=True, encoding='utf-8', static=True, - filter=foo, absolute=True, member_name='m', collection_name='c', - parent_member_name='p', parent_collection_name='c', - condition_method='GET', condition_subdomain=True, - condition_function=foo, subdomains=['a'], - name='thename', path='thepath', - factory=foo, view='view', permission='permission') - self._callFUT(directive) - self.assertEqual(len(mapper.connections), 1) - self.assertEqual(mapper.connections[0][0], ('thename', 'thepath')) - pr = {'member_name':'p', 'collection_name':'c'} - c = {'method':'GET', 'sub_domain':['a'], 'function':foo} - D = mapper.connections[0][1] - - self.assertEqual(D['requirements'], {}) - self.assertEqual(D['_minimize'],False) - self.assertEqual(D['_explicit'],True) - self.assertEqual(D['_encoding'],'utf-8') - self.assertEqual(D['_static'],True) - self.assertEqual(D['_filter'],foo) - self.assertEqual(D['_absolute'],True) - self.assertEqual(D['_member_name'], 'm') - self.assertEqual(D['_collection_name'], 'c') - self.assertEqual(D['_parent_resource'], pr) - self.assertEqual(D['conditions'], c) - route = mapper.matchlist[-1] - self.assertEqual(route._factory, foo) - self.assertEqual(route.request_factories, - directive.context.request_factories['thename']) - - - def test_condition_subdomain_true(self): - mapper = self._registerRoutesMapper() - directive = DummyRouteDirective(static=True, explicit=True, - condition_subdomain=True) - self._callFUT(directive) - self.assertEqual(len(mapper.connections), 1) - self.assertEqual(mapper.connections[0][1], - {'requirements': {}, - '_static':True, - '_explicit':True, - 'conditions':{'sub_domain':True} - }) - - def test_condition_function(self): - mapper = self._registerRoutesMapper() - def foo(e, r): - """ """ - directive = DummyRouteDirective(static=True, explicit=True, - condition_function=foo) - self._callFUT(directive) - self.assertEqual(len(mapper.connections), 1) - self.assertEqual(mapper.connections[0][1], - {'requirements': {}, - '_static':True, - '_explicit':True, - 'conditions':{'function':foo} - }) - - def test_condition_method(self): - mapper = self._registerRoutesMapper() - directive = DummyRouteDirective(static=True, explicit=True, - condition_method='GET') - self._callFUT(directive) - self.assertEqual(len(mapper.connections), 1) - self.assertEqual(mapper.connections[0][1], - {'requirements': {}, - '_static':True, - '_explicit':True, - 'conditions':{'method':'GET'} - }) - - def test_request_type(self): - mapper = self._registerRoutesMapper() - directive = DummyRouteDirective(static=True, explicit=True, - request_type='GET') - self._callFUT(directive) - self.assertEqual(len(mapper.connections), 1) - self.assertEqual(mapper.connections[0][1], - {'requirements': {}, - '_static':True, - '_explicit':True, - 'conditions':{'method':'GET'} - }) - - def test_condition_method_and_request_type(self): - mapper = self._registerRoutesMapper() - directive = DummyRouteDirective(static=True, explicit=True, - request_type='GET', - condition_method='POST') - self._callFUT(directive) - self.assertEqual(len(mapper.connections), 1) - self.assertEqual(mapper.connections[0][1], - {'requirements': {}, - '_static':True, - '_explicit':True, - 'conditions':{'method':'POST'} - }) - - - def test_subdomains(self): - mapper = self._registerRoutesMapper() - directive = DummyRouteDirective(name='name', - static=True, explicit=True, - subdomains=['a', 'b']) - self._callFUT(directive) - self.assertEqual(len(mapper.connections), 1) - self.assertEqual(mapper.connections[0][0], ('name', 'path')) - self.assertEqual(mapper.connections[0][1], - {'requirements': {}, - '_static':True, - '_explicit':True, - 'conditions':{'sub_domain':['a', 'b']} - }) + self._callFUT('name', 'path', 'factory') + self.assertEqual(mapper.connections, [('name', 'path', 'factory')]) class TestRouteDirective(unittest.TestCase): def setUp(self): @@ -850,48 +706,32 @@ class TestRouteDirective(unittest.TestCase): def tearDown(self): cleanUp() - def _getTargetClass(self): - from repoze.bfg.zcml import Route - return Route - - def _makeOne(self, context, path, name, **kw): - return self._getTargetClass()(context, path, name, **kw) + def _callFUT(self, *arg, **kw): + from repoze.bfg.zcml import route + return route(*arg, **kw) def test_defaults(self): + from zope.component import getUtility + from repoze.bfg.interfaces import IRequestFactories context = DummyContext() - route = self._makeOne(context, 'path', 'name') - self.assertEqual(route.path, 'path') - self.assertEqual(route.name, 'name') - self.assertEqual(route.requirements, {}) - - def test_parent_collection_name_missing(self): - context = DummyContext() - from zope.configuration.exceptions import ConfigurationError - self.assertRaises(ConfigurationError, self._makeOne, context, - 'path', 'name', parent_member_name='a') - - def test_parent_collection_name_present(self): - context = DummyContext() - route = self._makeOne(context, 'path', 'name', - parent_member_name='a', - parent_collection_name='p') - self.assertEqual(route.parent_member_name, 'a') - self.assertEqual(route.parent_collection_name, 'p') + self._callFUT(context, 'name', 'path') + self.failUnless(getUtility(IRequestFactories, name='name')) - def test_after_with_view(self): + def test_with_view(self): + from zope.component import getUtility + from repoze.bfg.interfaces import IRequestFactories from repoze.bfg.zcml import handler from repoze.bfg.zcml import connect_route from repoze.bfg.interfaces import IView context = DummyContext() view = Dummy() - route = self._makeOne(context, 'path', 'name', view=view) - route.after() + self._callFUT(context, 'name', 'path', view=view) actions = context.actions self.assertEqual(len(actions), 2) - factories = context.request_factories - request_iface = factories['name'][None]['interface'] + factories = getUtility(IRequestFactories, name='name') + request_iface = factories[None]['interface'] view_action = actions[0] view_callable = view_action['callable'] @@ -913,31 +753,28 @@ class TestRouteDirective(unittest.TestCase): route_discriminator = route_action['discriminator'] route_args = route_action['args'] self.assertEqual(route_callable, connect_route) - self.assertEqual(len(route_discriminator), 7) + self.assertEqual(len(route_discriminator), 4) self.assertEqual(route_discriminator[0], 'route') self.assertEqual(route_discriminator[1], 'name') - self.assertEqual(route_discriminator[2],'{}') + self.assertEqual(route_discriminator[2], None) self.assertEqual(route_discriminator[3], None) - self.assertEqual(route_discriminator[4], None) - self.assertEqual(route_discriminator[5], None) - self.assertEqual(route_discriminator[6], None) - self.assertEqual(route_args, (route,)) + self.assertEqual(route_args, ('name', 'path', None)) - def test_after_with_view_and_view_for(self): + def test_with_view_and_view_for(self): + from zope.component import getUtility + from repoze.bfg.interfaces import IRequestFactories from repoze.bfg.zcml import handler from repoze.bfg.zcml import connect_route from repoze.bfg.interfaces import IView context = DummyContext() view = Dummy() - route = self._makeOne(context, 'path', 'name', view=view, - view_for=IDummy) - route.after() + self._callFUT(context, 'name', 'path', view=view, view_for=IDummy) actions = context.actions self.assertEqual(len(actions), 2) - factories = context.request_factories - request_iface = factories['name'][None]['interface'] + factories = getUtility(IRequestFactories, 'name') + request_iface = factories[None]['interface'] view_action = actions[0] view_callable = view_action['callable'] @@ -959,23 +796,21 @@ class TestRouteDirective(unittest.TestCase): route_discriminator = route_action['discriminator'] route_args = route_action['args'] self.assertEqual(route_callable, connect_route) - self.assertEqual(len(route_discriminator), 7) + self.assertEqual(len(route_discriminator), 4) self.assertEqual(route_discriminator[0], 'route') self.assertEqual(route_discriminator[1], 'name') - self.assertEqual(route_discriminator[2],'{}') + self.assertEqual(route_discriminator[2], IDummy) self.assertEqual(route_discriminator[3], None) - self.assertEqual(route_discriminator[4], None) - self.assertEqual(route_discriminator[5], None) - self.assertEqual(route_discriminator[6], None) - self.assertEqual(route_args, (route,)) + self.assertEqual(route_args, ('name', 'path', None,)) - def test_after_without_view(self): + def test_without_view(self): + from zope.component import getUtility + from repoze.bfg.interfaces import IRequestFactories from repoze.bfg.zcml import connect_route context = DummyContext() view = Dummy() - route = self._makeOne(context, 'path', 'name') - route.after() + self._callFUT(context, 'name', 'path') actions = context.actions self.assertEqual(len(actions), 1) @@ -984,15 +819,35 @@ class TestRouteDirective(unittest.TestCase): route_discriminator = route_action['discriminator'] route_args = route_action['args'] self.assertEqual(route_callable, connect_route) - self.assertEqual(len(route_discriminator), 7) + self.assertEqual(len(route_discriminator), 4) self.assertEqual(route_discriminator[0], 'route') self.assertEqual(route_discriminator[1], 'name') - self.assertEqual(route_discriminator[2],'{}') + self.assertEqual(route_discriminator[2], None) self.assertEqual(route_discriminator[3], None) - self.assertEqual(route_discriminator[4], None) - self.assertEqual(route_discriminator[5], None) - self.assertEqual(route_discriminator[6], None) - self.assertEqual(route_args, (route,)) + self.assertEqual(route_args, ('name','path', None)) + + def test_with_request_type(self): + from zope.component import getUtility + from repoze.bfg.interfaces import IRequestFactories + from repoze.bfg.zcml import connect_route + + context = DummyContext() + view = Dummy() + self._callFUT(context, 'name', 'path', request_type="GET") + actions = context.actions + self.assertEqual(len(actions), 1) + + route_action = actions[0] + route_callable = route_action['callable'] + route_discriminator = route_action['discriminator'] + route_args = route_action['args'] + self.assertEqual(route_callable, connect_route) + self.assertEqual(len(route_discriminator), 4) + self.assertEqual(route_discriminator[0], 'route') + self.assertEqual(route_discriminator[1], 'name') + self.assertEqual(route_discriminator[2], None) + self.assertEqual(route_discriminator[3], 'GET') + self.assertEqual(route_args, ('name','path', None)) class TestZCMLConfigure(unittest.TestCase): i = 0 @@ -1360,44 +1215,12 @@ class DummyContext: class Dummy: pass -class DummyRouteDirective: - path = 'path' - name = 'name' - view = None - view_for = None - permission = None - factory = None - minimize = True - encoding = None - static = False - filter = None - absolute = False - member_name = None - collection_name = None - condition_method = None - request_type = None - condition_subdomain = None - condition_function = None - parent_member_name = None - parent_collection_name = None - subdomains = None - explicit = False - - def __init__(self, **kw): - if not 'requirements' in kw: - kw['requirements'] = {} - self.__dict__.update(kw) - self.context = DummyContext() - self.context.request_factories = {self.name:{}} - class DummyMapper: def __init__(self): self.connections = [] - self.matchlist = [] - def connect(self, *arg, **kw): - self.connections.append((arg, kw)) - self.matchlist.append(DummyRoute()) + def connect(self, *args): + self.connections.append(args) class DummyRoute: pass diff --git a/repoze/bfg/traversal.py b/repoze/bfg/traversal.py index e15bb8f20..b79f87d4c 100644 --- a/repoze/bfg/traversal.py +++ b/repoze/bfg/traversal.py @@ -614,20 +614,14 @@ class TraversalContextURL(object): route = environ['bfg.routes.route'] matchdict = environ['bfg.routes.matchdict'].copy() matchdict['traverse'] = path - # we can't use route.generate here because our matchdict - # keys are Unicode - if route.minimization: - segments = route.generate_minimized(matchdict) - else: - segments = route.generate_non_minimized(matchdict) - if segments is False: - raise ValueError( - "Couldn't generate URL for matchdict %r" % matchdict) + try: + segments = route.generate(matchdict) + except KeyError, why: + raise KeyError( + "Couldn't generate URL for matchdict %r: %s" % + (matchdict, str(why))) app_url = request.application_url - if segments.startswith('/'): - return app_url + segments - else: - return app_url + '/' + segments + return app_url + segments else: app_url = request.application_url # never ends in a slash return app_url + path diff --git a/repoze/bfg/url.py b/repoze/bfg/url.py index f9927c431..7f505e659 100644 --- a/repoze/bfg/url.py +++ b/repoze/bfg/url.py @@ -10,43 +10,88 @@ from repoze.bfg.interfaces import IRoutesMapper from repoze.bfg.traversal import TraversalContextURL from repoze.bfg.traversal import quote_path_segment -from routes import URLGenerator -from routes.util import GenerationException - -def route_url(request, route_name, **kw): +def route_url(route_name, request, *elements, **kw): """Generates a fully qualified URL for a named BFG route. Use the request object as the first positional argument. Use the - route's ``name`` as the second positional argument. Use keyword - arguments to supply values which match any dynamic path elements - in the route definition. Raises a ValueError exception if the URL - cannot be generated when the + route's ``name`` as the second positional argument. Additional + keyword elements are appended to the URL as path segments after it + is generated. + + Use keyword arguments to supply values which match any dynamic + path elements in the route definition. Raises a KeyError + exception if the URL cannot be generated for any reason (not + enough arguments, for example). For example, if you've defined a route named "foobar" with the path ``:foo/:bar/*traverse``:: - route_url(request, 'foobar', foo='1') => <ValueError exception> - route_url(request, 'foobar', foo='1', bar='2') => <ValueError exception> - route_url('foobar', foo='1', bar='2', + route_url('foobar', request, foo='1') => <KeyError exception> + route_url('foobar', request, foo='1', bar='2') => <KeyError exception> + route_url('foobar', request, foo='1', bar='2', 'traverse='a/b) => http://e.com/1/2/a/b - All keys given to ``route_url`` are sent to the BFG Routes "mapper" - instance for generation except for:: - - anchor specifies the anchor name to be appened to the path - host overrides the default host if provided - protocol overrides the default (current) protocol if provided - qualified return a fully qualified URL (default True) + If a keyword argument ``_query`` is present, it will used to + compose a query string that will be tacked on to the end of the + URL. The value of ``query`` must be a sequence of two-tuples *or* + a data structure with an ``.items()`` method that returns a + sequence of two-tuples (presumably a dictionary). This data + structure will be turned into a query string per the documentation + of ``repoze.url.urlencode`` function. After the query data is + turned into a query string, a leading ``?`` is prepended, and the + the resulting string is appended to the generated URL. + .. note:: Python data structures that are passed as ``_query`` + which are sequences or dictionaries are turned into a + string under the same rules as when run through + urllib.urlencode with the ``doseq`` argument equal to + ``True``. This means that sequences can be passed as + values, and a k=v pair will be placed into the query + string for each value. + + If a keyword argument ``_anchor`` is present, its string + representation will be used as a named anchor in the generated URL + (e.g. if ``anchor`` is passed as ``foo`` and the model URL is + ``http://example.com/model/url``, the resulting generated URL will + be ``http://example.com/model/url#foo``). + + .. note:: If ``_anchor`` is passed as a string, it should be UTF-8 + encoded. If ``anchor`` is passed as a Unicode object, it + will be converted to UTF-8 before being appended to the + URL. The anchor value is not quoted in any way before + being appended to the generated URL. + + If both ``anchor`` and ``query`` are specified, the anchor element + will always follow the query element, + e.g. ``http://example.com?foo=1#bar``. + + This function raises a ``KeyError`` if the route cannot be + generated due to missing replacement names. Extra replacement + names are ignored. """ - if not 'qualified' in kw: - kw['qualified'] = True - try: - mapper = getUtility(IRoutesMapper) - generator = URLGenerator(mapper, request.environ) - return generator(route_name, **kw) - except GenerationException, why: - raise ValueError(str(why)) + mapper = getUtility(IRoutesMapper) + path = mapper.generate(route_name, kw) # raises KeyError if generate fails + + anchor = '' + qs = '' + + if '_query' in kw: + qs = '?' + urlencode(kw['_query'], doseq=True) + + if '_anchor' in kw: + anchor = kw['_anchor'] + if isinstance(anchor, unicode): + anchor = anchor.encode('utf-8') + anchor = '#' + anchor + + if elements: + suffix = '/'.join([quote_path_segment(s) for s in elements]) + if not path.endswith('/'): + suffix = '/' + suffix + else: + suffix = '' + + return request.application_url + path + suffix + qs + anchor def model_url(model, request, *elements, **kw): """ diff --git a/repoze/bfg/urldispatch.py b/repoze/bfg/urldispatch.py index 0281b52b3..497d3980e 100644 --- a/repoze/bfg/urldispatch.py +++ b/repoze/bfg/urldispatch.py @@ -1,53 +1,102 @@ -from routes import Mapper -from routes import request_config +import re _marker = object() -class RoutesRootFactory(Mapper): - def __init__(self, default_root_factory, **kw): +class Route(object): + def __init__(self, name, matcher, generator, factory): + self.name = name + self.matcher = matcher + self.generator = generator + self.factory = factory + + def match(self, path): + return self.matcher(path) + + def generate(self, kw): + return self.generator(kw) + +class RoutesRootFactory(object): + def __init__(self, default_root_factory): self.default_root_factory = default_root_factory - kw['controller_scan'] = None - kw['always_scan'] = False - kw['directory'] = None - kw['explicit'] = True - Mapper.__init__(self, **kw) - self._regs_created = False + self.routelist = [] + self.routes = {} def has_routes(self): - return bool(self.matchlist) + return bool(self.routelist) - def connect(self, *arg, **kw): - result = Mapper.connect(self, *arg, **kw) - route = self.matchlist[-1] - route._factory = None # overridden by ZCML - return result + def connect(self, name, path, factory=None): + matcher, generator = _compile_route(path) + route = Route(name, matcher, generator, factory) + self.routelist.append(route) + self.routes[name] = route + return route + + def generate(self, name, kw): + return self.routes[name].generate(kw) def __call__(self, environ): - if not self._regs_created: - self.create_regs([]) - self._regs_created = True path = environ.get('PATH_INFO', '/') - self.environ = environ # sets the thread local - match = self.routematch(path) - if match: - args, route = match - else: - args = None - if isinstance(args, dict): # might be an empty dict - args = args.copy() - config = request_config() - config.mapper = self - config.mapper_dict = args - config.host = environ.get('HTTP_HOST', environ['SERVER_NAME']) - config.protocol = environ['wsgi.url_scheme'] - config.redirect = None - environ['wsgiorg.routing_args'] = ((), args) - environ['bfg.routes.route'] = route - environ['bfg.routes.matchdict'] = args - adhoc_attrs = environ.setdefault('webob.adhoc_attrs', {}) - adhoc_attrs['matchdict'] = args - factory = route._factory or self.default_root_factory - return factory(environ) + for route in self.routelist: + match = route.match(path) + if match is not None: + environ['wsgiorg.routing_args'] = ((), match) + environ['bfg.routes.route'] = route + environ['bfg.routes.matchdict'] = match + adhoc_attrs = environ.setdefault('webob.adhoc_attrs', {}) + adhoc_attrs['matchdict'] = match + factory = route.factory or self.default_root_factory + return factory(environ) return self.default_root_factory(environ) +# stolen from bobo and modified +route_re = re.compile(r'(/:[a-zA-Z]\w*)') +def _compile_route(route): + if not route.startswith('/'): + route = '/' + route + star = None + if '*' in route: + route, star = route.rsplit('*', 1) + pat = route_re.split(route) + pat.reverse() + rpat = [] + gen = [] + prefix = pat.pop() + if prefix: + rpat.append(re.escape(prefix)) + gen.append(prefix) + while pat: + name = pat.pop() + name = name[2:] + gen.append('/%%(%s)s' % name) + name = '/(?P<%s>[^/]*)' % name + rpat.append(name) + s = pat.pop() + if s: + rpat.append(re.escape(s)) + gen.append(s) + + if star: + rpat.append('(?P<%s>.*?)' % star) + gen.append('%%(%s)s' % star) + + pattern = ''.join(rpat) + '$' + + match = re.compile(pattern).match + def matcher(path): + m = match(path) + if m is None: + return m + return dict(item for item in m.groupdict().iteritems() + if item[1] is not None) + + gen = ''.join(gen) + def generator(dict): + newdict = {} + for k, v in dict.items(): + if isinstance(v, unicode): + v = v.encode('utf-8') + newdict[k] = v + return gen % newdict + + return matcher, generator diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py index 8d842fbfa..786301c45 100644 --- a/repoze/bfg/zcml.py +++ b/repoze/bfg/zcml.py @@ -4,18 +4,16 @@ import types from zope.configuration import xmlconfig from zope.component import getSiteManager +from zope.component import getUtility from zope.component import queryUtility import zope.configuration.config from zope.configuration.exceptions import ConfigurationError from zope.configuration.fields import GlobalObject -from zope.configuration.fields import Tokens from zope.interface import Interface -from zope.interface import implements -from zope.schema import Bool from zope.schema import TextLine from repoze.bfg.interfaces import IRoutesMapper @@ -28,6 +26,7 @@ from repoze.bfg.interfaces import ISecurityPolicy from repoze.bfg.interfaces import IView from repoze.bfg.interfaces import IUnauthorizedAppFactory from repoze.bfg.interfaces import ILogger +from repoze.bfg.interfaces import IRequestFactories from repoze.bfg.request import DEFAULT_REQUEST_FACTORIES from repoze.bfg.request import named_request_factories @@ -60,9 +59,8 @@ def view( if route_name is None: request_factories = DEFAULT_REQUEST_FACTORIES else: - try: - request_factories = _context.request_factories[route_name] - except (KeyError, AttributeError): + request_factories = queryUtility(IRequestFactories, name=route_name) + if request_factories is None: raise ConfigurationError( 'Unknown route_name "%s". <route> definitions must be ordered ' 'before the view definition which mentions the route\'s name ' @@ -94,6 +92,8 @@ def view( derived_view, (for_, request_type), IView, name, _context.info), ) +_view = view # for directives that take a view arg + def view_utility(_context, view, iface): derived_view = derive_view(view) _context.action( @@ -164,145 +164,28 @@ class IRouteDirective(Interface): view_for = GlobalObject(title=u'view_for', required=False) permission = TextLine(title=u'permission', required=False) factory = GlobalObject(title=u'context factory', required=False) - minimize = Bool(title=u'minimize', required=False) - encoding = TextLine(title=u'encoding', required=False) - static = Bool(title=u'static', required=False) - filter = GlobalObject(title=u'filter', required=False) - absolute = Bool(title=u'absolute', required=False) - member_name = TextLine(title=u'member_name', required=False) - collection_name = TextLine(title=u'collection_name', required=False) request_type = TextLine(title=u'request_type', required=False) - condition_method = TextLine(title=u'condition_method', required=False) - condition_subdomain = TextLine(title=u'condition_subdomain', required=False) - condition_function = GlobalObject(title=u'condition_function', - required=False) - parent_member_name = TextLine(title=u'parent member_name', required=False) - parent_collection_name = TextLine(title=u'parent collection_name', - required=False) - explicit = Bool(title=u'explicit', required=False) - subdomains = Tokens(title=u'subdomains', required=False, - value_type=TextLine()) - -class Route(zope.configuration.config.GroupingContextDecorator): + +def route(_context, name, path, view=None, view_for=None, permission=None, + factory=None, request_type=None): """ Handle ``route`` ZCML directives """ - view = None - view_for = None - permission = None - factory = None - minimize = True - encoding = None - static = False - filter = None - absolute = False - member_name = None - collection_name = None - condition_method = None - request_type = None - condition_subdomain = None - condition_function = None - parent_member_name = None - parent_collection_name = None - subdomains = None - explicit = False - - implements(zope.configuration.config.IConfigurationContext, - IRouteDirective) - - def __init__(self, context, path, name, **kw): - self.validate(**kw) - self.requirements = {} # mutated by subdirectives - self.context = context - self.path = path - self.name = name - self.__dict__.update(**kw) - - def validate(self, **kw): - parent_member_name = kw.get('parent_member_name') - parent_collection_name = kw.get('parent_collection_name') - if parent_member_name or parent_collection_name: - if not (parent_member_name and parent_collection_name): - raise ConfigurationError( - 'parent_member_name and parent_collection_name must be ' - 'specified together') - - def after(self): - context = self.context - name = self.name - if not hasattr(context, 'request_factories'): - context.request_factories = {} - context.request_factories[name] = named_request_factories(name) - - if self.view: - view(context, self.permission, self.view_for, self.view, '', - self.request_type, name) - - method = self.condition_method or self.request_type - - self.context.action( - discriminator = ('route', self.name, repr(self.requirements), - method, self.condition_subdomain, - self.condition_function, self.subdomains), - callable = connect_route, - args = (self,), - ) + sm = getSiteManager() + request_factories = named_request_factories(name) + sm.registerUtility(request_factories, IRequestFactories, name=name) + + if view: + _view(_context, permission, view_for, view, '', request_type, name) + + _context.action( + discriminator = ('route', name, view_for, request_type), + callable = connect_route, + args = (name, path, factory), + ) -def route_requirement(context, attr, expr): - route = context.context - if attr in route.requirements: - raise ValueError('Duplicate requirement', attr) - route.requirements[attr] = expr - -def connect_route(directive): - mapper = queryUtility(IRoutesMapper) - if mapper is None: - return - args = [directive.name, directive.path] - kw = dict(requirements=directive.requirements) - if not directive.minimize: - kw['_minimize'] = False - if directive.explicit: - kw['_explicit'] = True - if directive.encoding: - kw['_encoding'] = directive.encoding - if directive.static: - kw['_static'] = True - if directive.filter: - kw['_filter'] = directive.filter - if directive.absolute: - kw['_absolute'] = True - if directive.member_name: - kw['_member_name'] = directive.member_name - if directive.collection_name: - kw['_collection_name'] = directive.collection_name - if directive.parent_member_name and directive.parent_collection_name: - kw['_parent_resource'] = { - 'member_name':directive.parent_member_name, - 'collection_name':directive.parent_collection_name, - } - conditions = {} - - # request_type and condition_method are aliases; condition_method - # "wins" - if directive.request_type: - conditions['method'] = directive.request_type - if directive.condition_method: - conditions['method'] = directive.condition_method - if directive.condition_subdomain: - conditions['sub_domain'] = directive.condition_subdomain - if directive.condition_function: - conditions['function'] = directive.condition_function - if directive.subdomains: - conditions['sub_domain'] = directive.subdomains - if conditions: - kw['conditions'] = conditions - - result = mapper.connect(*args, **kw) - route = mapper.matchlist[-1] - route._factory = directive.factory - context = directive.context - route.request_factories = context.request_factories[directive.name] - return result +def connect_route(name, path, factory): + mapper = getUtility(IRoutesMapper) + mapper.connect(name, path, factory) class IViewDirective(Interface): for_ = GlobalObject( @@ -33,7 +33,6 @@ install_requires=[ 'chameleon.core >= 1.0b32', # non-lxml version 'chameleon.zpt >= 1.0b16', # newest version as of non-xml core release 'PasteScript', - 'Routes', 'WebOb', 'zope.interface >= 3.5.1', # 3.5.0 comment: "allow to bootstrap on jython" 'zope.component >= 3.6.0', # independent of zope.hookable |
