summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChris McDonough <chrism@agendaless.com>2009-06-30 21:02:00 +0000
committerChris McDonough <chrism@agendaless.com>2009-06-30 21:02:00 +0000
commitd809ac74d19342bcc84e4fe043697709b2001cc0 (patch)
tree9973b531b90336bf6ecf3811aa3b9ba753f4e6d8
parentf9e73ea5a9d5a4210e3a346aa2f9483d70d22ab9 (diff)
downloadpyramid-d809ac74d19342bcc84e4fe043697709b2001cc0.tar.gz
pyramid-d809ac74d19342bcc84e4fe043697709b2001cc0.tar.bz2
pyramid-d809ac74d19342bcc84e4fe043697709b2001cc0.zip
- Add a ``reload_resources`` configuration file setting (aka the
``BFG_RELOAD_RESOURCES`` environment variable). When this is set to true, the server never needs to be restarted when moving files between directory resource overrides (esp. for templates currently). - Add a ``reload_all`` configuration file setting (aka the ``BFG_RELOAD_ALL`` environment variable) that implies both ``reload_resources`` and ``reload_templates``. - The ``static`` helper view class now uses a ``PackageURLParser`` in order to allow for the overriding of static resources (CSS / logo files, etc) using the ``resource`` ZCML directive. The ``PackageURLParser`` class was added to a (new) ``static`` module in BFG; it is a subclass of the ``StaticURLParser`` class in ``paste.urlparser``. - The ``repoze.bfg.templating.renderer_from_cache`` function now checks for the ``reload_resources`` setting; if it's true, it does not register a template renderer (it won't use the registry as a template renderer cache). - Add ``pkg_resources`` to the glossary. - Update the "Environment" docs to note the existence of ``reload_resources`` and ``reload_all``. - Use a colon instead of a tab as the separator between package name and relpath to form the "spec" when register a ITemplateRenderer.
-rw-r--r--CHANGES.txt29
-rw-r--r--docs/glossary.rst7
-rw-r--r--docs/narr/environment.rst54
-rw-r--r--docs/narr/hooks.rst2
-rw-r--r--repoze/bfg/settings.py9
-rw-r--r--repoze/bfg/static.py78
-rw-r--r--repoze/bfg/templating.py19
-rw-r--r--repoze/bfg/tests/fixtures/static/index.html1
-rw-r--r--repoze/bfg/tests/fixtures/static/subdir/index.html1
-rw-r--r--repoze/bfg/tests/test_settings.py36
-rw-r--r--repoze/bfg/tests/test_static.py157
-rw-r--r--repoze/bfg/tests/test_templating.py31
-rw-r--r--repoze/bfg/tests/test_view.py26
-rw-r--r--repoze/bfg/view.py51
14 files changed, 474 insertions, 27 deletions
diff --git a/CHANGES.txt b/CHANGES.txt
index d8a72991b..11a3407e2 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,35 @@
Next release
============
+- Add a ``reload_resources`` configuration file setting (aka the
+ ``BFG_RELOAD_RESOURCES`` environment variable). When this is set to
+ true, the server never needs to be restarted when moving files
+ between directory resource overrides (esp. for templates currently).
+
+- Add a ``reload_all`` configuration file setting (aka the
+ ``BFG_RELOAD_ALL`` environment variable) that implies both
+ ``reload_resources`` and ``reload_templates``.
+
+- The ``static`` helper view class now uses a ``PackageURLParser`` in
+ order to allow for the overriding of static resources (CSS / logo
+ files, etc) using the ``resource`` ZCML directive. The
+ ``PackageURLParser`` class was added to a (new) ``static`` module in
+ BFG; it is a subclass of the ``StaticURLParser`` class in
+ ``paste.urlparser``.
+
+- The ``repoze.bfg.templating.renderer_from_cache`` function now
+ checks for the ``reload_resources`` setting; if it's true, it does
+ not register a template renderer (it won't use the registry as a
+ template renderer cache).
+
+- Add ``pkg_resources`` to the glossary.
+
+- Update the "Environment" docs to note the existence of
+ ``reload_resources`` and ``reload_all``.
+
+- Use a colon instead of a tab as the separator between package name
+ and relpath to form the "spec" when register a ITemplateRenderer.
+
- Updated the ``bfg_alchemy`` paster template to include two views:
the view on the root shows a list of links to records; the view on
a record shows the details for that object.
diff --git a/docs/glossary.rst b/docs/glossary.rst
index 303c20703..3abe31ea6 100644
--- a/docs/glossary.rst
+++ b/docs/glossary.rst
@@ -17,6 +17,13 @@ Glossary
`Setuptools <http://peak.telecommunity.com/DevCenter/setuptools>`_
builds on Python's ``distutils`` to provide easier building,
distribution, and installation of libraries and applications.
+ pkg_resources
+ A module which ships with :term:`setuptools` that provides an API
+ for addressing "resource files" within Python packages. Resource
+ files are static files, template files, etc; basically anything
+ non-Python-source that lives in a Python package can be considered
+ a resource file. See also `PkgResources
+ <http://peak.telecommunity.com/DevCenter/PkgResources>`_
Package
A directory on disk which contains an ``__init__.py`` file, making
it recognizable to Python as a location which can be ``import`` -ed.
diff --git a/docs/narr/environment.rst b/docs/narr/environment.rst
index 6df4157e2..45b544df5 100644
--- a/docs/narr/environment.rst
+++ b/docs/narr/environment.rst
@@ -27,6 +27,11 @@ application-specific configuration settings.
| | | See also: |
| | | :ref:`reload_templates_section` |
+---------------------------------+-----------------------------+----------------------------------------+
+| ``BFG_RELOAD_RESOURCES`` | ``reload_resources`` | Don't cache any resource file data |
+| | | when true |
+| | | See also: |
+| | | :ref:`overriding_resources_section` |
++---------------------------------+-----------------------------+----------------------------------------+
| ``BFG_DEBUG_AUTHORIZATION`` | ``debug_authorization`` | Print view authorization failure & |
| | | success info to stderr when true |
| | | See also: |
@@ -39,6 +44,8 @@ application-specific configuration settings.
+---------------------------------+-----------------------------+----------------------------------------+
| ``BFG_DEBUG_ALL`` | ``debug_all`` | Turns all debug_* settings on. |
+---------------------------------+-----------------------------+----------------------------------------+
+| ``BFG_RELOAD_ALL`` | ``reload_all`` | Turns all reload_* settings on. |
++---------------------------------+-----------------------------+----------------------------------------+
Examples
--------
@@ -66,9 +73,52 @@ application would behave in the same manner as if you had placed the
respective settings in the ``[app:main]`` section of your
application's ``.ini`` file.
-If you want to turn all ``debug`` settings (every debug setting that
-starts with ``debug_``). on in one fell swoop, you can use
+If you want to turn all ``debug`` settings (every setting that starts
+with ``debug_``). on in one fell swoop, you can use
``BFG_DEBUG_ALL=1`` as an environment variable setting or you may use
``debug_all=true`` in the config file. Note that this does not effect
settings that do not start with ``debug_*`` such as
``reload_templates``.
+
+If you want to turn all ``reload`` settings (everysetting that starts
+with ``reload_``). on in one fell swoop, you can use
+``BFG_RELOAD_ALL=1`` as an environment variable setting or you may use
+``reload_all=true`` in the config file. Note that this does not
+effect settings that do not start with ``reload_*`` such as
+``debug_notfound``.
+
+Understanding the Distinction Between ``reload_templates`` and ``reload_resources``
+-----------------------------------------------------------------------------------
+
+The difference between ``reload_resources`` and ``reload_templates``
+is a bit subtle. Templates are themselves also treated by
+:mod:`repoze.bfg` as :term:`pkg_resources` resource files (along with
+static files and other resources), so the distinction can be
+confusing. It's helpful to read :ref:`overriding_resources_section`
+for some context about resources in general.
+
+When ``reload_templates`` is true, :mod:`repoze.bfg`` takes advantage
+of the underlying templating systems' ability to check for file
+modifications to an individual template file. When
+``reload_templates`` is true but ``reload_resources`` is *not* true,
+the template filename returned by pkg_resources is cached by
+:mod:`repoze.bfg` on the first request. Subsequent requests for the
+same template file will return a cached template filename. The
+underlying templating system checks for modifications to this
+particular file for every request. Setting ``reload_templates`` to
+``True`` doesn't effect performance dramatically (although it should
+still not be used in production because it has some effect).
+
+However, when ``reload_resources`` is true, :mod:`repoze.bfg` will not
+cache the template filename, meaning you can see the effect of
+changing the content of an overridden resource directory for templates
+without restarting the server after every change. Subsequent requests
+for the same template file may return different filenames based on the
+current state of overridden resource directories. Setting
+``reload_resources`` to ``True`` effects performance *dramatically*
+(slowing things down by an order of magnitude for each template
+rendering) but it's convenient when moving files around in overridden
+resource directories. ``reload_resources`` makes the system *very
+slow* when templates are in use. Never set ``reload_resources`` to
+``True`` on a production system.
+
diff --git a/docs/narr/hooks.rst b/docs/narr/hooks.rst
index 85b1131bb..9eba8a92c 100644
--- a/docs/narr/hooks.rst
+++ b/docs/narr/hooks.rst
@@ -118,6 +118,8 @@ an object that implements any particular interface; it simply needs
have a ``status`` attribute, a ``headerlist`` attribute, and and
``app_iter`` attribute.
+.. _overriding_resources_section:
+
Overriding Resources
--------------------
diff --git a/repoze/bfg/settings.py b/repoze/bfg/settings.py
index df2a5e99e..08607f756 100644
--- a/repoze/bfg/settings.py
+++ b/repoze/bfg/settings.py
@@ -40,6 +40,9 @@ def get_options(kw, environ=os.environ):
config_debug_all = kw.get('debug_all', '')
effective_debug_all = asbool(eget('BFG_DEBUG_ALL',
config_debug_all))
+ config_reload_all = kw.get('reload_all', '')
+ effective_reload_all = asbool(eget('BFG_RELOAD_ALL',
+ config_reload_all))
config_debug_auth = kw.get('debug_authorization', '')
effective_debug_auth = asbool(eget('BFG_DEBUG_AUTHORIZATION',
config_debug_auth))
@@ -49,10 +52,14 @@ def get_options(kw, environ=os.environ):
config_reload_templates = kw.get('reload_templates', '')
effective_reload_templates = asbool(eget('BFG_RELOAD_TEMPLATES',
config_reload_templates))
+ config_reload_resources = kw.get('reload_resources', '')
+ effective_reload_resources = asbool(eget('BFG_RELOAD_RESOURCES',
+ config_reload_resources))
update = {
'debug_authorization': effective_debug_all or effective_debug_auth,
'debug_notfound': effective_debug_all or effective_debug_notfound,
- 'reload_templates': effective_reload_templates,
+ 'reload_templates': effective_reload_all or effective_reload_templates,
+ 'reload_resources':effective_reload_all or effective_reload_resources,
}
kw.update(update)
diff --git a/repoze/bfg/static.py b/repoze/bfg/static.py
new file mode 100644
index 000000000..01ce98a30
--- /dev/null
+++ b/repoze/bfg/static.py
@@ -0,0 +1,78 @@
+import os
+import pkg_resources
+
+from paste.httpheaders import ETAG
+from paste.urlparser import StaticURLParser
+from paste import httpexceptions
+from paste import request
+
+class PackageURLParser(StaticURLParser):
+ """ This probably won't work with zipimported resources """
+ def __init__(self, package_name, resource_name, root_resource=None,
+ cache_max_age=None):
+ self.package_name = package_name
+ self.resource_name = os.path.normpath(resource_name)
+ if root_resource is None:
+ root_resource = self.resource_name
+ self.root_resource = root_resource
+ self.cache_max_age = cache_max_age
+
+ def __call__(self, environ, start_response):
+ path_info = environ.get('PATH_INFO', '')
+ if not path_info:
+ return self.add_slash(environ, start_response)
+ if path_info == '/':
+ # @@: This should obviously be configurable
+ filename = 'index.html'
+ else:
+ filename = request.path_info_pop(environ)
+ resource = os.path.normcase(os.path.normpath(
+ self.resource_name + '/' + filename))
+ if ( (self.root_resource is not None) and
+ (not resource.startswith(self.root_resource)) ):
+ # Out of bounds
+ return self.not_found(environ, start_response)
+ if not pkg_resources.resource_exists(self.package_name, resource):
+ return self.not_found(environ, start_response)
+ if pkg_resources.resource_isdir(self.package_name, resource):
+ # @@: Cache?
+ child_root = (self.root_resource is not None and
+ self.root_resource or self.resource_name)
+ return self.__class__(
+ self.package_name, resource, root_resource=child_root,
+ cache_max_age=self.cache_max_age)(environ, start_response)
+ if (environ.get('PATH_INFO')
+ and environ.get('PATH_INFO') != '/'): # pragma: no cover
+ return self.error_extra_path(environ, start_response)
+ full = pkg_resources.resource_filename(self.package_name, resource)
+ if_none_match = environ.get('HTTP_IF_NONE_MATCH')
+ if if_none_match:
+ mytime = os.stat(full).st_mtime
+ if str(mytime) == if_none_match:
+ headers = []
+ ETAG.update(headers, mytime)
+ start_response('304 Not Modified', headers)
+ return [''] # empty body
+
+ fa = self.make_app(full)
+ if self.cache_max_age:
+ fa.cache_control(max_age=self.cache_max_age)
+ return fa(environ, start_response)
+
+ def not_found(self, environ, start_response, debug_message=None):
+ comment=('SCRIPT_NAME=%r; PATH_INFO=%r; looking in package %s; '
+ 'subdir %s ;debug: %s' % (environ.get('SCRIPT_NAME'),
+ environ.get('PATH_INFO'),
+ self.package_name,
+ self.resource_name,
+ debug_message or '(none)'))
+ exc = httpexceptions.HTTPNotFound(
+ 'The resource at %s could not be found'
+ % request.construct_url(environ),
+ comment=comment)
+ return exc.wsgi_application(environ, start_response)
+
+ def __repr__(self):
+ return '<%s %s:%s at %s>' % (self.__class__.__name__, self.package_name,
+ self.root_resource, id(self))
+
diff --git a/repoze/bfg/templating.py b/repoze/bfg/templating.py
index a81726bb9..3287f0808 100644
--- a/repoze/bfg/templating.py
+++ b/repoze/bfg/templating.py
@@ -1,9 +1,12 @@
+import os
import pkg_resources
+
from zope.component import queryUtility
-from repoze.bfg.interfaces import ITemplateRenderer
from zope.component import getSiteManager
+
+from repoze.bfg.interfaces import ITemplateRenderer
from repoze.bfg.path import caller_package
-import os
+from repoze.bfg.settings import get_settings
def renderer_from_cache(path, factory, level=3, **kw):
if os.path.isabs(path):
@@ -21,7 +24,7 @@ def renderer_from_cache(path, factory, level=3, **kw):
# 'path' is a relative filename
package = caller_package(level=level)
spec = (package.__name__, path)
- utility_name = '%s\t%s' % spec # utility name must be a string :-(
+ utility_name = '%s:%s' % spec # utility name must be a string
renderer = queryUtility(ITemplateRenderer, name=utility_name)
if renderer is None:
# service unit tests here by trying the relative path
@@ -29,11 +32,15 @@ def renderer_from_cache(path, factory, level=3, **kw):
renderer = queryUtility(ITemplateRenderer, name=path)
if renderer is None:
if not pkg_resources.resource_exists(*spec):
- raise ValueError('Missing template resource: %s:%s' % spec)
+ raise ValueError('Missing template resource: %s' % utility_name)
abspath = pkg_resources.resource_filename(*spec)
renderer = factory(abspath, **kw)
- sm = getSiteManager()
- sm.registerUtility(renderer, ITemplateRenderer, name=utility_name)
+ settings = get_settings()
+ if (not settings) or (not settings.get('reload_resources')):
+ # cache the template
+ sm = getSiteManager()
+ sm.registerUtility(renderer, ITemplateRenderer,
+ name=utility_name)
return renderer
diff --git a/repoze/bfg/tests/fixtures/static/index.html b/repoze/bfg/tests/fixtures/static/index.html
new file mode 100644
index 000000000..6498787a5
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/static/index.html
@@ -0,0 +1 @@
+<html>static</html>
diff --git a/repoze/bfg/tests/fixtures/static/subdir/index.html b/repoze/bfg/tests/fixtures/static/subdir/index.html
new file mode 100644
index 000000000..bb84fad04
--- /dev/null
+++ b/repoze/bfg/tests/fixtures/static/subdir/index.html
@@ -0,0 +1 @@
+<html>subdir</html>
diff --git a/repoze/bfg/tests/test_settings.py b/repoze/bfg/tests/test_settings.py
index 903a23f5d..8319a302d 100644
--- a/repoze/bfg/tests/test_settings.py
+++ b/repoze/bfg/tests/test_settings.py
@@ -61,6 +61,42 @@ class TestGetOptions(unittest.TestCase):
{'BFG_RELOAD_TEMPLATES':'1'})
self.assertEqual(result['reload_templates'], True)
+ def test_reload_resources(self):
+ result = self._callFUT({})
+ self.assertEqual(result['reload_resources'], False)
+ result = self._callFUT({'reload_resources':'false'})
+ self.assertEqual(result['reload_resources'], False)
+ result = self._callFUT({'reload_resources':'t'})
+ self.assertEqual(result['reload_resources'], True)
+ result = self._callFUT({'reload_resources':'1'})
+ self.assertEqual(result['reload_resources'], True)
+ result = self._callFUT({}, {'BFG_RELOAD_RESOURCES':'1'})
+ self.assertEqual(result['reload_resources'], True)
+ result = self._callFUT({'reload_resources':'false'},
+ {'BFG_RELOAD_RESOURCES':'1'})
+ self.assertEqual(result['reload_resources'], True)
+
+ def test_reload_all(self):
+ result = self._callFUT({})
+ self.assertEqual(result['reload_templates'], False)
+ self.assertEqual(result['reload_resources'], False)
+ result = self._callFUT({'reload_all':'false'})
+ self.assertEqual(result['reload_templates'], False)
+ self.assertEqual(result['reload_resources'], False)
+ result = self._callFUT({'reload_all':'t'})
+ self.assertEqual(result['reload_templates'], True)
+ self.assertEqual(result['reload_resources'], True)
+ result = self._callFUT({'reload_all':'1'})
+ self.assertEqual(result['reload_templates'], True)
+ self.assertEqual(result['reload_resources'], True)
+ result = self._callFUT({}, {'BFG_RELOAD_ALL':'1'})
+ self.assertEqual(result['reload_templates'], True)
+ self.assertEqual(result['reload_resources'], True)
+ result = self._callFUT({'reload_all':'false'},
+ {'BFG_RELOAD_ALL':'1'})
+ self.assertEqual(result['reload_templates'], True)
+ self.assertEqual(result['reload_resources'], True)
+
def test_debug_authorization(self):
result = self._callFUT({})
self.assertEqual(result['debug_authorization'], False)
diff --git a/repoze/bfg/tests/test_static.py b/repoze/bfg/tests/test_static.py
new file mode 100644
index 000000000..7c7b5627c
--- /dev/null
+++ b/repoze/bfg/tests/test_static.py
@@ -0,0 +1,157 @@
+import unittest
+
+class TestPackageURLParser(unittest.TestCase):
+ def _getTargetClass(self):
+ from repoze.bfg.static import PackageURLParser
+ return PackageURLParser
+
+ def _makeOne(self, *arg, **kw):
+ return self._getTargetClass()(*arg, **kw)
+
+
+ def _makeEnviron(self, **kw):
+ environ = {
+ 'wsgi.url_scheme':'http',
+ 'wsgi.version':(1,0),
+ 'SERVER_NAME':'example.com',
+ 'SERVER_PORT':'6543',
+ 'PATH_INFO':'/',
+ 'SCRIPT_NAME':'',
+ 'REQUEST_METHOD':'GET',
+ }
+ environ.update(kw)
+ return environ
+
+ def test_ctor_allargs(self):
+ inst = self._makeOne('package', 'resource/name', root_resource='root',
+ cache_max_age=100)
+ self.assertEqual(inst.package_name, 'package')
+ self.assertEqual(inst.resource_name, 'resource/name')
+ self.assertEqual(inst.root_resource, 'root')
+ self.assertEqual(inst.cache_max_age, 100)
+
+ def test_ctor_defaultargs(self):
+ inst = self._makeOne('package', 'resource/name')
+ self.assertEqual(inst.package_name, 'package')
+ self.assertEqual(inst.resource_name, 'resource/name')
+ self.assertEqual(inst.root_resource, 'resource/name')
+ self.assertEqual(inst.cache_max_age, None)
+
+ def test_call_adds_slash_path_info_empty(self):
+ environ = self._makeEnviron(PATH_INFO='')
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless('301 Moved Permanently' in body)
+ self.failUnless('http://example.com:6543/' in body)
+
+ def test_path_info_slash_means_index_html(self):
+ environ = self._makeEnviron()
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless('<html>static</html>' in body)
+
+ def test_resource_out_of_bounds(self):
+ environ = self._makeEnviron()
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ inst.root_resource = 'abcdef'
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless('404 Not Found' in body)
+ self.failUnless('http://example.com:6543/' in body)
+
+ def test_resource_doesnt_exist(self):
+ environ = self._makeEnviron(PATH_INFO='/notthere')
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless('404 Not Found' in body)
+ self.failUnless('http://example.com:6543/' in body)
+
+ def test_resource_isdir(self):
+ environ = self._makeEnviron(PATH_INFO='/subdir/')
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless('<html>subdir</html>' in body)
+
+ def test_resource_is_file(self):
+ environ = self._makeEnviron(PATH_INFO='/index.html')
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless('<html>static</html>' in body)
+
+ def test_resource_is_file_with_cache_max_age(self):
+ environ = self._makeEnviron(PATH_INFO='/index.html')
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static',
+ cache_max_age=600)
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless('<html>static</html>' in body)
+ self.assertEqual(len(sr.headerlist), 8)
+ header_names = [ x[0] for x in sr.headerlist ]
+ header_names.sort()
+ self.assertEqual(header_names,
+ ['Accept-Ranges', 'Cache-Control',
+ 'Content-Length', 'Content-Range',
+ 'Content-Type', 'ETag', 'Expires', 'Last-Modified'])
+
+ def test_resource_is_file_with_no_cache_max_age(self):
+ environ = self._makeEnviron(PATH_INFO='/index.html')
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ body = response[0]
+ self.failUnless('<html>static</html>' in body)
+ self.assertEqual(len(sr.headerlist), 6)
+ header_names = [ x[0] for x in sr.headerlist ]
+ header_names.sort()
+ self.assertEqual(header_names,
+ ['Accept-Ranges', 'Content-Length', 'Content-Range',
+ 'Content-Type', 'ETag', 'Last-Modified'])
+
+ def test_if_none_match(self):
+ class DummyEq(object):
+ def __eq__(self, other):
+ return True
+ dummy_eq = DummyEq()
+ environ = self._makeEnviron(HTTP_IF_NONE_MATCH=dummy_eq)
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ sr = DummyStartResponse()
+ response = inst(environ, sr)
+ self.assertEqual(len(sr.headerlist), 1)
+ self.assertEqual(sr.status, '304 Not Modified')
+ self.assertEqual(sr.headerlist[0][0], 'ETag')
+ self.assertEqual(response[0], '')
+
+ def test_repr(self):
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ self.failUnless(
+ repr(inst).startswith(
+ '<PackageURLParser repoze.bfg.tests:fixtures/static at'))
+
+ def test_not_found(self):
+ inst = self._makeOne('repoze.bfg.tests', 'fixtures/static')
+ environ = self._makeEnviron()
+ sr = DummyStartResponse()
+ response = inst.not_found(environ, sr, 'debug_message')
+ body = response[0]
+ self.failUnless('404 Not Found' in body)
+ self.assertEqual(sr.status, '404 Not Found')
+
+class DummyStartResponse:
+ def __call__(self, status, headerlist, exc_info=None):
+ self.status = status
+ self.headerlist = headerlist
+ self.exc_info = exc_info
+
+
diff --git a/repoze/bfg/tests/test_templating.py b/repoze/bfg/tests/test_templating.py
index d413acc2e..361cf6f7e 100644
--- a/repoze/bfg/tests/test_templating.py
+++ b/repoze/bfg/tests/test_templating.py
@@ -57,7 +57,7 @@ class TestRendererFromCache(unittest.TestCase):
from repoze.bfg import tests
module_name = tests.__name__
relpath = 'test_templating.py'
- spec = '%s\t%s' % (module_name, relpath)
+ spec = '%s:%s' % (module_name, relpath)
renderer = {}
testing.registerUtility(renderer, ITemplateRenderer, name=spec)
result = self._callFUT('test_templating.py', None)
@@ -68,7 +68,6 @@ class TestRendererFromCache(unittest.TestCase):
from repoze.bfg.tests import test_templating
module_name = test_templating.__name__
relpath = 'test_templating.py'
- spec = '%s\t%s' % (module_name, relpath)
renderer = {}
factory = DummyFactory(renderer)
result = self._callFUT('test_templating.py', factory)
@@ -79,6 +78,34 @@ class TestRendererFromCache(unittest.TestCase):
self.assertEqual(factory.path, path)
self.assertEqual(factory.kw, {})
+ def test_relpath_notyetregistered_reload_resources_true(self):
+ from zope.component import queryUtility
+ from repoze.bfg.interfaces import ISettings
+ from repoze.bfg.interfaces import ITemplateRenderer
+ settings = {'reload_resources':True}
+ testing.registerUtility(settings, ISettings)
+ renderer = {}
+ factory = DummyFactory(renderer)
+ result = self._callFUT('test_templating.py', factory)
+ self.failUnless(result is renderer)
+ spec = '%s:%s' % ('repoze.bfg.tests', 'test_templating.py')
+ self.assertEqual(queryUtility(ITemplateRenderer, name=spec),
+ None)
+
+ def test_relpath_notyetregistered_reload_resources_false(self):
+ from zope.component import queryUtility
+ from repoze.bfg.interfaces import ISettings
+ from repoze.bfg.interfaces import ITemplateRenderer
+ settings = {'reload_resources':False}
+ testing.registerUtility(settings, ISettings)
+ renderer = {}
+ factory = DummyFactory(renderer)
+ result = self._callFUT('test_templating.py', factory)
+ self.failUnless(result is renderer)
+ spec = '%s:%s' % ('repoze.bfg.tests', 'test_templating.py')
+ self.assertNotEqual(queryUtility(ITemplateRenderer, name=spec),
+ None)
+
class DummyFactory:
def __init__(self, renderer):
self.renderer = renderer
diff --git a/repoze/bfg/tests/test_view.py b/repoze/bfg/tests/test_view.py
index 55e9ecffa..5b870fa7d 100644
--- a/repoze/bfg/tests/test_view.py
+++ b/repoze/bfg/tests/test_view.py
@@ -317,8 +317,8 @@ class TestStaticView(unittest.TestCase, BaseTest):
from repoze.bfg.view import static
return static
- def _makeOne(self, path):
- return self._getTargetClass()(path)
+ def _makeOne(self, path, package_name=None):
+ return self._getTargetClass()(path, package_name=package_name)
def test_abspath(self):
import os
@@ -343,8 +343,26 @@ class TestStaticView(unittest.TestCase, BaseTest):
response = view(context, request)
self.assertEqual(request.copied, True)
here = os.path.abspath(os.path.dirname(__file__))
- abspath = os.path.join(here, 'fixtures')
- self.assertEqual(response.directory, abspath)
+ self.assertEqual(response.root_resource, 'fixtures')
+ self.assertEqual(response.resource_name, 'fixtures')
+ self.assertEqual(response.package_name, 'repoze.bfg.tests')
+ self.assertEqual(response.cache_max_age, 3600)
+
+ def test_relpath_withpackage(self):
+ import os
+ path = 'fixtures'
+ view = self._makeOne(path, package_name='another')
+ context = DummyContext()
+ request = DummyRequest()
+ request.subpath = ['__init__.py']
+ request.environ = self._makeEnviron()
+ response = view(context, request)
+ self.assertEqual(request.copied, True)
+ here = os.path.abspath(os.path.dirname(__file__))
+ self.assertEqual(response.root_resource, 'fixtures')
+ self.assertEqual(response.resource_name, 'fixtures')
+ self.assertEqual(response.package_name, 'another')
+ self.assertEqual(response.cache_max_age, 3600)
class TestBFGViewDecorator(unittest.TestCase):
def setUp(self):
diff --git a/repoze/bfg/view.py b/repoze/bfg/view.py
index 0b1f09837..2c0e3efdc 100644
--- a/repoze/bfg/view.py
+++ b/repoze/bfg/view.py
@@ -1,13 +1,17 @@
+import os
import inspect
from paste.urlparser import StaticURLParser
+
from zope.component import queryMultiAdapter
from zope.deprecation import deprecated
from repoze.bfg.interfaces import IView
from repoze.bfg.path import caller_path
+from repoze.bfg.path import caller_package
from repoze.bfg.security import view_execution_permitted
from repoze.bfg.security import Unauthorized
+from repoze.bfg.static import PackageURLParser
deprecated('view_execution_permitted',
"('from repoze.bfg.view import view_execution_permitted' is now "
@@ -106,25 +110,48 @@ def is_response(ob):
class static(object):
""" An instance of this class is a callable which can act as a BFG
view; this view will serve static files from a directory on disk
- based on the ``root_dir`` you provide to its constructor. The
- directory may contain subdirectories (recursively); the static
+ based on the ``root_dir`` you provide to its constructor.
+
+ The directory may contain subdirectories (recursively); the static
view implementation will descend into these directories as
necessary based on the components of the URL in order to resolve a
path into a response.
You may pass an absolute or relative filesystem path to the
directory containing static files directory to the constructor as
- the ``root_dir`` argument. If the path is relative, it will be
- considered relative to the directory in which the Python file
- which calls ``static`` resides. ``cache_max_age`` influences the
- Expires and Max-Age response headers returned by the view (default
- is 3600 seconds or five minutes). ``level`` influences how
- relative directories are resolved (the number of hops in the call
- stack), not used very often.
+ the ``root_dir`` argument.
+
+ If the path is relative, and the ``package`` argument is ``None``,
+ it will be considered relative to the directory in which the
+ Python file which calls ``static`` resides. If the ``package``
+ name argument is provided, and a relative ``root_dir`` is
+ provided, the ``root_dir`` will be considered relative to the
+ Python package specified by ``package_name`` (a dotted path to a
+ Python package).
+
+ ``cache_max_age`` influences the Expires and Max-Age response
+ headers returned by the view (default is 3600 seconds or five
+ minutes). ``level`` influences how relative directories are
+ resolved (the number of hops in the call stack), not used very
+ often.
+
+ .. note:: If the ``root_dir`` is relative to a package, the BFG
+ ``resource`` ZCML directive can be used to override resources
+ within the named ``root_dir`` package-relative directory.
+ However, if the ``root_dir`` is absolute, the ``resource``
+ directive will not be able to override the resources it
+ contains.
"""
- def __init__(self, root_dir, cache_max_age=3600, level=2):
- root_dir = caller_path(root_dir, level=level)
- self.app = StaticURLParser(root_dir, cache_max_age=cache_max_age)
+ def __init__(self, root_dir, cache_max_age=3600, level=2,
+ package_name=None):
+ if os.path.isabs(root_dir):
+ root_dir = caller_path(root_dir, level=level)
+ self.app = StaticURLParser(root_dir, cache_max_age=cache_max_age)
+ else:
+ if package_name is None:
+ package_name = caller_package().__name__
+ self.app = PackageURLParser(package_name, root_dir,
+ cache_max_age=cache_max_age)
def __call__(self, context, request):
subpath = '/'.join(request.subpath)