From 750ce41f217cd7b638ad5b69fcb9df1b49841b58 Mon Sep 17 00:00:00 2001 From: Chris McDonough Date: Thu, 17 Sep 2009 19:58:01 +0000 Subject: - Add a ``repoze.bfg.url.static_url`` API which is capable of generating URLs to static resources defined by the ```` ZCML directive. See the "Views" narrative chapter's section titled "Generating Static Resource URLs" for more information. --- CHANGES.txt | 5 +++ docs/api/url.rst | 2 ++ docs/narr/views.rst | 69 ++++++++++++++++++++++++++++++++++++ repoze/bfg/static.py | 7 ++++ repoze/bfg/tests/test_static.py | 7 ++++ repoze/bfg/tests/test_url.py | 65 ++++++++++++++++++++++++++++++++- repoze/bfg/tests/test_urldispatch.py | 9 +++++ repoze/bfg/tests/test_zcml.py | 11 ++---- repoze/bfg/url.py | 56 +++++++++++++++++++++++++++-- repoze/bfg/urldispatch.py | 3 ++ repoze/bfg/zcml.py | 8 ++--- 11 files changed, 226 insertions(+), 16 deletions(-) diff --git a/CHANGES.txt b/CHANGES.txt index eeada7e49..bd1a0a96e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,6 +1,11 @@ Next release ============ +- Add a ``repoze.bfg.url.static_url`` API which is capable of + generating URLs to static resources defined by the ```` ZCML + directive. See the "Views" narrative chapter's section titled + "Generating Static Resource URLs" for more information. + - Add a ``string`` renderer. This renderer converts a non-Response return value of any view callble into a string. It is documented in the "Views" narrative chapter. diff --git a/docs/api/url.rst b/docs/api/url.rst index 36d3c5b65..578a4a2fc 100644 --- a/docs/api/url.rst +++ b/docs/api/url.rst @@ -9,5 +9,7 @@ .. autofunction:: route_url + .. autofunction:: static_url + .. autofunction:: urlencode diff --git a/docs/narr/views.rst b/docs/narr/views.rst index b64fd2a4b..4bb2dade7 100644 --- a/docs/narr/views.rst +++ b/docs/narr/views.rst @@ -1093,6 +1093,75 @@ directive's ``path`` is ``/path/to/static``, subdirectories recursively, and any subdirectories may hold files; these will be resolved by the static view as you would expect. +.. note:: The ```` ZCML directive is new in :mod:`repoze.bfg` + 1.1. + +Generating Static Resource URLs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When a ```` directive is used to register a static resource +directory, a special helper API named ``repoze.bfg.static_url`` can be +used to generate the appropriate URL for a package resource that lives +in one of the directories named by the ```` directive's +``path`` attribute. + +For example, let's assume you create a set of ``static`` declarations +in ZCML like so: + +.. code-block:: xml + :linenos: + + + + + +These declarations create URL-accessible directories which have URLs +which begin, respectively, with ``/static1`` and ``/static2``. The +resources in the ``resources/1`` directory are consulted when a user +visits a URL which begins with ``/static1``, and the resources in the +``resources/2`` directory are consulted when a user visits a URL which +begins with ``/static2``. + +You needn't generate the URLs to static resources "by hand" in such a +configuration. Instead, use the ``repoze.bfg.url.static_url`` API to +generate them for you. For example, let's imagine that the following +code lives in a module that shares the same directory as the above +ZCML file: + +.. code-block:: python + :linenos: + + from repoze.bfg.url import static_url + from repoze.bfg.chameleon_zpt import render_template_to_response + + def my_view(context, request): + css_url = static_url('resources/1/foo.css', request) + js_url = static_url('resources/2/foo.js', request) + return render_template_to_response('templates/my_template.pt', + css_url = css_url, + js_url = js_url) + +If the request "application URL" of the running system is +``http://example.com``, the ``css_url`` generated above would be: +``http://example.com/static1/foo.css``. The ``js_url`` generated +above would be ``'http://example.com/static2/foo.js``. + +One benefit of using the ``static_url`` function rather than +constructing static URLs "by hand" is that if you need to change the +``name`` of a static URL declaration in ZCML, the generated URLs will +continue to resolve properly after the rename. + +See :ref:`url_module` for detailed information about inputs and +outputs of the ``static_url`` function. + +.. note:: The ``static_url`` API is new in :mod:`repoze.bfg` 1.1. + Serving Static Resources Using a View ------------------------------------- diff --git a/repoze/bfg/static.py b/repoze/bfg/static.py index 01ce98a30..ef893875f 100644 --- a/repoze/bfg/static.py +++ b/repoze/bfg/static.py @@ -76,3 +76,10 @@ class PackageURLParser(StaticURLParser): return '<%s %s:%s at %s>' % (self.__class__.__name__, self.package_name, self.root_resource, id(self)) +class StaticRootFactory: + def __init__(self, spec): + self.spec = spec + + def __call__(self, environ): + return self + diff --git a/repoze/bfg/tests/test_static.py b/repoze/bfg/tests/test_static.py index 7c7b5627c..355afac2a 100644 --- a/repoze/bfg/tests/test_static.py +++ b/repoze/bfg/tests/test_static.py @@ -148,6 +148,13 @@ class TestPackageURLParser(unittest.TestCase): self.failUnless('404 Not Found' in body) self.assertEqual(sr.status, '404 Not Found') +class TestStaticRootFactory(unittest.TestCase): + def test_it(self): + from repoze.bfg.static import StaticRootFactory + factory = StaticRootFactory('abc') + self.assertEqual(factory.spec, 'abc') + self.assertEqual(factory({}), factory) + class DummyStartResponse: def __call__(self, status, headerlist, exc_info=None): self.status = status diff --git a/repoze/bfg/tests/test_url.py b/repoze/bfg/tests/test_url.py index 5f0fde872..5833b8880 100644 --- a/repoze/bfg/tests/test_url.py +++ b/repoze/bfg/tests/test_url.py @@ -202,7 +202,62 @@ class TestRouteUrl(unittest.TestCase): mapper.raise_exc = KeyError request = DummyRequest() self.assertRaises(KeyError, self._callFUT, 'flub', request, a=1) + +class TestStaticUrl(unittest.TestCase): + def setUp(self): + cleanUp() + + def tearDown(self): + cleanUp() + def _callFUT(self, *arg, **kw): + from repoze.bfg.url import static_url + return static_url(*arg, **kw) + + def test_notfound(self): + from repoze.bfg.interfaces import IRoutesMapper + from zope.component import getSiteManager + mapper = DummyRoutesMapper(result='/1/2/3') + sm = getSiteManager() + sm.registerUtility(mapper, IRoutesMapper) + request = DummyRequest() + self.assertRaises(ValueError, self._callFUT, 'static/foo.css', request) + + def test_abspath(self): + from repoze.bfg.interfaces import IRoutesMapper + from zope.component import getSiteManager + mapper = DummyRoutesMapper(result='/1/2/3') + sm = getSiteManager() + sm.registerUtility(mapper, IRoutesMapper) + request = DummyRequest() + self.assertRaises(ValueError, self._callFUT, '/static/foo.css', request) + + def test_found_rel(self): + from repoze.bfg.interfaces import IRoutesMapper + from repoze.bfg.static import StaticRootFactory + from zope.component import getSiteManager + factory = StaticRootFactory('repoze.bfg.tests:fixtures') + routes = [DummyRoute('name', factory=factory)] + mapper = DummyRoutesMapper(result='/1/2/3', routes = routes) + sm = getSiteManager() + sm.registerUtility(mapper, IRoutesMapper) + request = DummyRequest() + url = self._callFUT('fixtures/minimal.pt', request) + self.assertEqual(url, 'http://example.com:5432/1/2/3') + + def test_found_abs(self): + from repoze.bfg.interfaces import IRoutesMapper + from repoze.bfg.static import StaticRootFactory + from zope.component import getSiteManager + factory = StaticRootFactory('repoze.bfg.tests:fixtures') + routes = [DummyRoute('name', factory=factory)] + mapper = DummyRoutesMapper(result='/1/2/3', routes = routes) + sm = getSiteManager() + sm.registerUtility(mapper, IRoutesMapper) + request = DummyRequest() + url = self._callFUT('repoze.bfg.tests:fixtures/minimal.pt', request) + self.assertEqual(url, 'http://example.com:5432/1/2/3') + class DummyContext(object): def __init__(self, next=None): self.next = next @@ -216,11 +271,19 @@ class DummyRequest: class DummyRoutesMapper: raise_exc = None - def __init__(self, result='/1/2/3', raise_exc=False): + def __init__(self, result='/1/2/3', raise_exc=False, routes=()): self.result = result + self.routes = routes + + def get_routes(self): + return self.routes def generate(self, *route_args, **newargs): if self.raise_exc: raise self.raise_exc return self.result +class DummyRoute: + def __init__(self, name, factory=None): + self.name = name + self.factory = factory diff --git a/repoze/bfg/tests/test_urldispatch.py b/repoze/bfg/tests/test_urldispatch.py index 1de3d6f3b..faa375dbd 100644 --- a/repoze/bfg/tests/test_urldispatch.py +++ b/repoze/bfg/tests/test_urldispatch.py @@ -120,6 +120,15 @@ class RoutesRootFactoryTests(unittest.TestCase): mapper.connect('whatever', 'archives/:action/:article') self.assertEqual(mapper.has_routes(), True) + def test_get_routes(self): + from repoze.bfg.urldispatch import Route + mapper = self._makeOne(None) + self.assertEqual(mapper.get_routes(), []) + mapper.connect('whatever', 'archives/:action/:article') + routes = mapper.get_routes() + self.assertEqual(len(routes), 1) + self.assertEqual(routes[0].__class__, Route) + def test_generate(self): mapper = self._makeOne(None) def generator(kw): diff --git a/repoze/bfg/tests/test_zcml.py b/repoze/bfg/tests/test_zcml.py index 7af9ab193..6161f6567 100644 --- a/repoze/bfg/tests/test_zcml.py +++ b/repoze/bfg/tests/test_zcml.py @@ -1848,7 +1848,7 @@ class TestStaticDirective(unittest.TestCase): from zope.interface import implementedBy from zope.component import getSiteManager from repoze.bfg.zcml import connect_route - from repoze.bfg.zcml import StaticRootFactory + from repoze.bfg.static import StaticRootFactory from repoze.bfg.interfaces import IView from repoze.bfg.interfaces import IRouteRequest import os @@ -1887,7 +1887,7 @@ class TestStaticDirective(unittest.TestCase): from zope.component import getSiteManager from zope.interface import implementedBy from repoze.bfg.zcml import connect_route - from repoze.bfg.zcml import StaticRootFactory + from repoze.bfg.static import StaticRootFactory from repoze.bfg.interfaces import IView from repoze.bfg.interfaces import IRouteRequest context = DummyContext() @@ -1922,7 +1922,7 @@ class TestStaticDirective(unittest.TestCase): from zope.component import getSiteManager from zope.interface import implementedBy from repoze.bfg.zcml import connect_route - from repoze.bfg.zcml import StaticRootFactory + from repoze.bfg.static import StaticRootFactory from repoze.bfg.interfaces import IView from repoze.bfg.interfaces import IRouteRequest import repoze.bfg.tests @@ -2226,11 +2226,6 @@ class TestAll(unittest.TestCase): self.assertEqual(all([False, False]), False) self.assertEqual(all([False, True]), False) -class TestStaticRootFactory(unittest.TestCase): - def test_it(self): - from repoze.bfg.zcml import StaticRootFactory - StaticRootFactory({}) # it just needs construction - class DummyModule: __path__ = "foo" __name__ = "dummy" diff --git a/repoze/bfg/url.py b/repoze/bfg/url.py index dcaea67ff..f21334191 100644 --- a/repoze/bfg/url.py +++ b/repoze/bfg/url.py @@ -1,5 +1,6 @@ """ Utility functions for dealing with URLs in repoze.bfg """ +import os import urllib from zope.component import queryMultiAdapter @@ -7,6 +8,8 @@ from zope.component import getUtility from repoze.bfg.interfaces import IContextURL from repoze.bfg.interfaces import IRoutesMapper +from repoze.bfg.path import caller_package +from repoze.bfg.static import StaticRootFactory from repoze.bfg.traversal import TraversalContextURL from repoze.bfg.traversal import quote_path_segment @@ -29,9 +32,9 @@ def route_url(route_name, request, *elements, **kw): route_url('foobar', request, foo='1') => route_url('foobar', request, foo='1', bar='2') => route_url('foobar', request, foo='1', bar='2', - 'traverse=('a','b') => http://e.com/1/2/a/b + 'traverse=('a','b')) => http://e.com/1/2/a/b route_url('foobar', request, foo='1', bar='2', - 'traverse=('/a/b') => http://e.com/1/2/a/b + 'traverse=('/a/b')) => http://e.com/1/2/a/b Values replacing ``:segment`` arguments can be passed as strings or Unicode objects. They will be encoded to UTF-8 and URL-quoted @@ -197,6 +200,55 @@ def model_url(model, request, *elements, **kw): return model_url + suffix + qs + anchor +def static_url(path, request, **kw): + """ + Generates a fully qualified URL for a static resource. The + resource must live within a location defined via the ```` + ZCML directive. + + The ``path`` argument points at a file or directory on disk which + a URL should be generated for. The ``path`` may be either a + relative path (e.g. ``static/foo.css``) or a :term:`resource + specification` (e.g. ``mypackage:static/foo.css``). A ``path`` + may not be an absolute filesystem path (a ValueError will be + raised if this function is supplied with an absolute path). + + The ``request`` argument should be a WebOb request. + + The purpose of the ``**kw`` argument is the same as the purpose of + the ``route_url`` ``*kw`` argument. See the documentation for + that function to understand the arguments which you can provide to + it. However, typically, you don't need to pass anything as + ``*kw`` when generating a static resource URL. + + This function raises a ValueError if a ```` ZCML + definition cannot be found which matches the path specification. + + .. note:: This feature is new in :mod:`repoze.bfg` 1.1. + """ + mapper = getUtility(IRoutesMapper) + routes = mapper.get_routes() + if os.path.isabs(path): + raise ValueError('Absolute paths cannot be used to generate static ' + 'urls (use a package-relative path or a resource ' + 'specification).') + if not ':' in path: + # if it's not a package:relative/name and it's not an + # /absolute/path it's a relative/path; this means its relative + # to the package in which the caller's module is defined. + package = caller_package(level=2) + path = '%s:%s' % (package.__name__, path) + + for route in routes: + factory = route.factory + if factory.__class__ is StaticRootFactory: + if path.startswith(factory.spec): + subpath = path[len(factory.spec):] + kw['subpath'] = subpath + return route_url(route.name, request, **kw) + + raise ValueError('No static URL definition matching %s' % path) + def urlencode(query, doseq=False): """ A wrapper around Python's stdlib `urllib.urlencode function diff --git a/repoze/bfg/urldispatch.py b/repoze/bfg/urldispatch.py index 747faace7..dfb7c9979 100644 --- a/repoze/bfg/urldispatch.py +++ b/repoze/bfg/urldispatch.py @@ -29,6 +29,9 @@ class RoutesRootFactory(object): def has_routes(self): return bool(self.routelist) + def get_routes(self): + return self.routelist + def connect(self, path, name, factory=None): route = Route(path, name, factory) self.routelist.append(route) diff --git a/repoze/bfg/zcml.py b/repoze/bfg/zcml.py index fd8e086a4..8ad0c3ec9 100644 --- a/repoze/bfg/zcml.py +++ b/repoze/bfg/zcml.py @@ -51,6 +51,8 @@ from repoze.bfg.security import Unauthorized from repoze.bfg.settings import get_settings +from repoze.bfg.static import StaticRootFactory + from repoze.bfg.traversal import find_interface from repoze.bfg.view import static as static_view @@ -611,10 +613,6 @@ class IStaticDirective(Interface): required=False, default=None) -class StaticRootFactory: - def __init__(self, environ): - pass - def static(_context, name, path, cache_max_age=3600): """ Handle ``static`` ZCML directives """ @@ -626,7 +624,7 @@ def static(_context, name, path, cache_max_age=3600): view = static_view(path, cache_max_age=cache_max_age) route(_context, name, "%s*subpath" % name, view=view, - view_for=StaticRootFactory, factory=StaticRootFactory) + view_for=StaticRootFactory, factory=StaticRootFactory(path)) class IViewDirective(Interface): for_ = GlobalObject( -- cgit v1.2.3