From 92e3a98d3ea6facfc96d583467805dcfe795ca1e Mon Sep 17 00:00:00 2001 From: Malthe Borch Date: Sat, 26 Sep 2009 15:27:31 +0000 Subject: [PATCH] Fixed issue with multiple layers; refactor, added tests. --- CHANGES.txt | 20 +++ README.txt | 13 +- setup.py | 4 +- z3c/jbot/README.txt | 167 ++++++++++-------- z3c/jbot/__init__.py | 60 ++++--- z3c/jbot/manager.py | 45 +++-- z3c/jbot/metaconfigure.py | 19 +- .../http/z3c.jbot.tests.templates.example.pt | 1 + .../https/z3c.jbot.tests.templates.example.pt | 1 + .../z3c.jbot.tests.templates.example.pt | 1 + .../z3c.jbot.tests.templates.example.pt | 1 + .../z3c.jbot.tests.templates.example.pt | 1 - z3c/jbot/utility.py | 4 +- 13 files changed, 202 insertions(+), 135 deletions(-) create mode 100644 CHANGES.txt create mode 100644 z3c/jbot/tests/overrides/http/z3c.jbot.tests.templates.example.pt create mode 100644 z3c/jbot/tests/overrides/https/z3c.jbot.tests.templates.example.pt create mode 100644 z3c/jbot/tests/overrides/interface/z3c.jbot.tests.templates.example.pt create mode 100644 z3c/jbot/tests/overrides/request/z3c.jbot.tests.templates.example.pt delete mode 100644 z3c/jbot/tests/templates/z3c.jbot.tests.templates.example.pt diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..94bcf94 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,20 @@ +Changes +======= + +In next release... + +- Improved test coverage. + +- Refactored code, improving performance. + +- Fixed issue with multiple layers. + +0.2 (2008-07-14) +---------------- + +- Added layer support. + +0.1 (2007-11-27) +---------------- + +- Initial public release. diff --git a/README.txt b/README.txt index ad99f2a..31a8d7a 100644 --- a/README.txt +++ b/README.txt @@ -1,5 +1,5 @@ Overview --------- +======== The z3c.jbot (or "Just a bunch of templates") package allows drop-in page template overrides. @@ -11,7 +11,6 @@ filename. Overrides may be registered for a specific layer or any layer. - Canonical filename ------------------ @@ -24,28 +23,24 @@ Example: Suppose you want to override: /plone/app/layout/viewlets/logo.pt You would use the filename: plone.app.layout.viewlets.logo.pt - Registering a on overrides directory ------------------------------------ A Zope component configuration directive is available to configure -overrides. +overrides:: - + - Performance considerations -------------------------- The use of jbot adds to the general page load time. On a site with -many templates this may be as much as 20 ms per request (a 7% increase -on my machine). +many templates this may be as much as 10 ms per request. - Author ------ diff --git a/setup.py b/setup.py index c784ca6..5625f1e 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,12 @@ from setuptools import setup, find_packages import sys, os -version = '0.2dev' +version = '0.3dev' setup(name='z3c.jbot', version=version, description="Drop-in template overrides.", - long_description=open("README.txt").read(), + long_description=open("README.txt").read() + open("CHANGES.txt").read(), classifiers=[ "Framework :: Zope2", "Framework :: Zope3", diff --git a/z3c/jbot/README.txt b/z3c/jbot/README.txt index 9c955ec..8b7ca40 100644 --- a/z3c/jbot/README.txt +++ b/z3c/jbot/README.txt @@ -11,7 +11,7 @@ Let's instantiate a page template >>> from zope.pagetemplate.pagetemplatefile import PageTemplateFile >>> template = PageTemplateFile("tests/templates/example.pt") - + A call to the template will render it. >>> template() @@ -30,41 +30,23 @@ of the original filename. >>> import z3c.jbot.tests >>> directory = z3c.jbot.tests.__path__[0] -Register template manager factory. We'll register it for -``zope.interface.Interface`` which makes it available on all layers. - - >>> import z3c.jbot.manager - >>> import z3c.jbot.interfaces - >>> factory = z3c.jbot.manager.TemplateManagerFactory() - >>> component.provideAdapter( - ... factory, (interface.Interface,), - ... z3c.jbot.interfaces.ITemplateManager) - -Register overrides directory. - - >>> manager = factory.manager - >>> manager.registerDirectory("%s/templates" % directory) - -Verify that we've registered the contents of the directory: - - >>> manager.paths - {'z3c.jbot.tests.templates.example.pt': '.../z3c.jbot.tests.templates.example.pt', - 'example.pt': '.../example.pt'} - -Notice that the file "z3c.jbot.tests.templates.example.pt" is the -dotted name for the original example page template file. +Register overrides directory (by default for any request); we confirm +that it's registered for the same template manager. + + >>> from z3c.jbot.metaconfigure import handler + >>> manager = handler("%s/overrides/interface" % directory, interface.Interface) We should now see that the new filename will be used for rendering: >>> template() - u'This template will override the example template.\n' + u'Override from ./interface.\n' Before we proceed we'll clean up. - >>> manager.unregisterDirectory("%s/templates" % directory) + >>> manager.unregisterAllDirectories() The template does indeed render the original template. - + >>> template() u'This is an example page template.\n' @@ -74,73 +56,112 @@ template filename to the original. >>> template.filename '.../z3c.jbot/z3c/jbot/tests/templates/example.pt' -Overrides can be registered for a specific layer. Let's re-register an -override template factory for the HTTP-request layer. +Overrides can be registered for a specific layer. Let's register three +more overrides, one for the general-purpose ``IRequest`` layer, one +for the ``IHTTPRequest`` layer and one for a made-up ``IHTTPSRequest`` +layer. - >>> from zope.publisher.interfaces.browser import IHTTPRequest - >>> factory = z3c.jbot.manager.TemplateManagerFactory() - >>> component.provideAdapter( - ... factory, (IHTTPRequest,), - ... z3c.jbot.interfaces.ITemplateManager, name='http') + >>> from zope.publisher.interfaces import IRequest + >>> from zope.publisher.interfaces.http import IHTTPRequest + >>> class IHTTPSRequest(IRequest): + ... """An HTTPS request.""" -Register overrides directory. - - >>> manager = factory.manager - >>> manager.registerDirectory("%s/templates" % directory) +Next we register an overrides directory for the ``IRequest`` layer. -Let's set up an interaction with a base request. + >>> general = handler("%s/overrides/request" % directory, IRequest) +Let's set up an interaction with a trivial participation. + + >>> class Participation: + ... interaction = None + + >>> participation = Participation() >>> import zope.security.management - >>> import zope.publisher.base - >>> request = zope.publisher.base.BaseRequest("", {}) - >>> IHTTPRequest.providedBy(request) + >>> zope.security.management.newInteraction(participation) + +This participation does not provide even the basic request interface. + + >>> IRequest.providedBy(participation) False - >>> zope.security.management.newInteraction(request) -Since this request is not an HTTP-request, we don't expect the -override to be enabled. +We don't expect the template to be overriden for this interaction. >>> template() u'This is an example page template.\n' -Let's now engage in an interaction with an HTTP-request. - - >>> interface.alsoProvides(request, IHTTPRequest) +Let's upgrade it. + + >>> request = participation + >>> interface.alsoProvides(request, IRequest) + >>> template() - u'This template will override the example template.\n' + u'Override from ./request.\n' >>> template._v_cooked 1 - + Going back to a basic request. - >>> interface.noLongerProvides(request, IHTTPRequest) - >>> IHTTPRequest.providedBy(request) - False - + >>> interface.noLongerProvides(request, IRequest) >>> template() u'This is an example page template.\n' Let's verify that we only cook once per template source. - >>> import z3c.jbot.utility >>> output = template() >>> template._v_last_read and template._v_cooked 1 - >>> interface.alsoProvides(request, IHTTPRequest) + >>> interface.alsoProvides(request, IRequest) >>> output = template() >>> template._v_last_read and template._v_cooked 1 >>> template() - u'This template will override the example template.\n' + u'Override from ./request.\n' + +Now, if we switch to the HTTP-layer. + + >>> interface.noLongerProvides(request, IRequest) + >>> interface.alsoProvides(request, IHTTPRequest) + + >>> template() + u'Override from ./request.\n' + + >>> general.unregisterAllDirectories() + + >>> template() + u'This is an example page template.\n' + + >>> http = handler("%s/overrides/http" % directory, IHTTPRequest) + >>> https = handler("%s/overrides/https" % directory, IHTTPSRequest) + + >>> template() + u'Override from ./http.\n' + +Switching to HTTPS. - >>> for manager in z3c.jbot.utility.getManagers(): - ... manager.unregisterDirectory("%s/templates" % directory) - >>> interface.noLongerProvides(request, IHTTPRequest) - + >>> interface.alsoProvides(request, IHTTPSRequest) + + >>> template() + u'Override from ./https.\n' + + >>> interface.noLongerProvides(request, IHTTPSRequest) + +Unregister all directories (cleanup). + + >>> for manager, layer in ((http, IHTTPRequest), (https, IHTTPSRequest)): + ... interface.alsoProvides(request, layer) + ... _ = template() + ... manager.unregisterAllDirectories() + ... interface.noLongerProvides(request, layer) + +The override is no longer in effect. + + >>> template() + u'This is an example page template.\n' + Configuring template override directories in ZCML ------------------------------------------------- @@ -155,42 +176,42 @@ Let's try registering the directory again. >>> xmlconfig.xmlconfig(StringIO(""" ... - ... + ... ... ... """ % directory)) Once again, the override will be in effect. - + >>> template() - u'This template will override the example template.\n' + u'Override from ./interface.\n' Providing the HTTP-request layer does not change this. >>> interface.alsoProvides(request, IHTTPRequest) >>> template() - u'This template will override the example template.\n' + u'Override from ./interface.\n' Unregister overrides. - - >>> manager = tuple(z3c.jbot.utility.getManagers())[0] - >>> manager.unregisterDirectory("%s/templates" % directory) - + + >>> for manager in z3c.jbot.utility.getManagers(): + ... manager.unregisterAllDirectories() + >>> template() u'This is an example page template.\n' - + Let's register overrides for the HTTP-request layer. >>> xmlconfig.xmlconfig(StringIO(""" ... ... ... ... """ % directory)) -If we now provide the HTTP-request layer, the override becomes active. +Since we now provide the HTTP-request layer, the override is used. >>> template() - u'This template will override the example template.\n' + u'Override from ./http.\n' diff --git a/z3c/jbot/__init__.py b/z3c/jbot/__init__.py index 8c70950..249ce42 100644 --- a/z3c/jbot/__init__.py +++ b/z3c/jbot/__init__.py @@ -1,4 +1,3 @@ -from zope import interface from zope.pagetemplate.pagetemplatefile import PageTemplateFile import utility @@ -15,6 +14,8 @@ except: pass +_v_cache = threading.local() + class LayerProperty(property): """Layer-specific property class. @@ -30,46 +31,59 @@ class LayerProperty(property): This pattern takes into account that attributes may be set before the property is defined on the class. """ - + def __init__(self, cls, name): self.name = name self.default = getattr(cls, name, None) property.__init__(self, self._get, self._set) - + def _get(self, template): key = self.name - layer = getattr(template._v_cache, 'layer', None) - attrs = getattr(template, '_v_attrs', template.__dict__) - if (layer, key) in attrs: + __dict__ = template.__dict__ + + # note: exceptions should not arise during normal service + try: + layer = _v_cache.layer + except AttributeError: + layer = None + + try: + # any of these could raise a key-error + attrs = __dict__['_v_attrs'] return attrs[layer, key] - return attrs.get(key) or template.__dict__.get(key) or self.default - + except KeyError: + return __dict__.get(key) or self.default + def _set(self, template, value): key = self.name - layer = getattr(template._v_cache, 'layer', None) - attrs = template.__dict__.get('_v_attrs') - if attrs is None: - attrs = template._v_attrs = {} + __dict__ = template.__dict__ + + # note: exceptions should not arise during normal service + try: + layer = _v_cache.layer + except AttributeError: + layer = None + + try: + attrs = __dict__['_v_attrs'] + except KeyError: + attrs = __dict__['_v_attrs'] = {} - # set value attrs[layer, key] = value - - # set default value - attrs.setdefault(key, value) - + __dict__.setdefault(key, value) + # registration hook to template manager def jbot(func): def patch(self, *args, **kwargs): - # set layer - self._v_cache.layer = utility.getLayer() + _v_cache.layer = utility.getLayer() for manager in utility.getManagers(): # register template; this call returns ``True`` if # template was invalidated if manager.registerTemplate(self): break - - return func(self, *args, **kwargs) + + return func(self, *args, **kwargs) return patch logger.info("Patching page template classes for use with z3c.jbot...") @@ -82,7 +96,5 @@ def patch(self, *args, **kwargs): for pt_class in PT_CLASSES: for name in ('_v_macros', '_v_program', '_v_cooked', '_v_errors', '_v_last_read', '_v_warning', '_text_', - 'filename', 'content_type', 'is_html'): + 'filename', '_filename', 'content_type', 'is_html'): setattr(pt_class, name, LayerProperty(pt_class, name)) - - setattr(pt_class, '_v_cache', threading.local()) diff --git a/z3c/jbot/manager.py b/z3c/jbot/manager.py index 2880873..3e2556e 100644 --- a/z3c/jbot/manager.py +++ b/z3c/jbot/manager.py @@ -7,6 +7,7 @@ import interfaces IGNORE = object() +DELETE = object() def root_length(a, b): if b.startswith(a): @@ -21,7 +22,7 @@ def sort_by_path(path, paths): def find_zope2_product(path): """Check the Zope2 magic Products semi-namespace to see if the path is part of a Product.""" - + _syspaths = sort_by_path(path, sys.modules["Products"].__path__) syspath = _syspaths[0] @@ -39,37 +40,41 @@ def find_package(syspaths, path): _syspaths = sort_by_path(path, syspaths) syspath = _syspaths[0] - + path = os.path.normpath(path) if not path.startswith(syspath): if utility.ZOPE_2: return find_zope2_product(path) return None - + path = path[len(syspath):] - + # convert path to dotted filename if path.startswith(os.path.sep): path = path[1:] - + return path class TemplateManagerFactory(object): - def __init__(self): - self.manager = TemplateManager() + def __init__(self, name): + self.manager = TemplateManager(name) def __call__(self, layer): return self.manager - + class TemplateManager(object): interface.implements(interfaces.ITemplateManager) - - def __init__(self): + + def __init__(self, name): self.syspaths = tuple(sys.path) self.templates = {} self.paths = {} + self.directories = set() + self.name = name def registerDirectory(self, directory): + self.directories.add(directory) + for filename in os.listdir(directory): if filename.endswith('.pt'): self.paths[filename] = "%s/%s" % (directory, filename) @@ -79,8 +84,10 @@ def registerDirectory(self, directory): del self.templates[template] def unregisterDirectory(self, directory): + self.directories.remove(directory) + templates = [] - + for template, filename in self.templates.items(): if filename in self.paths: templates.append(template) @@ -92,12 +99,18 @@ def unregisterDirectory(self, directory): for template in templates: self.registerTemplate(template) del self.templates[template] - + template.filename = template._filename + template._v_last_read = False + + def unregisterAllDirectories(self): + for directory in tuple(self.directories): + self.unregisterDirectory(directory) + def registerTemplate(self, template): # only register templates that have a filename attribute if not hasattr(template, 'filename'): return - + # assert that the template is not already registered filename = self.templates.get(template) if filename is IGNORE: @@ -110,18 +123,16 @@ def registerTemplate(self, template): # verify that override has not been unregistered if filename is not None and filename not in paths: - # restore original template template.filename = template._filename - delattr(template, '_filename') del self.templates[template] - + # check if an override exists path = find_package(self.syspaths, template.filename) if path is None: # permanently ignore template self.templates[template] = IGNORE return - + filename = path.replace(os.path.sep, '.') if filename in paths: path = paths[filename] diff --git a/z3c/jbot/metaconfigure.py b/z3c/jbot/metaconfigure.py index 1059749..50bee71 100644 --- a/z3c/jbot/metaconfigure.py +++ b/z3c/jbot/metaconfigure.py @@ -5,10 +5,10 @@ import interfaces def handler(directory, layer): - gsm = component.getGlobalSiteManager() + lookup_all = component.getGlobalSiteManager().adapters.lookupAll # check if a template manager already exists - factories = set(factory for name, factory in gsm.adapters.lookupAll( + factories = set(factory for name, factory in lookup_all( (layer,), interfaces.ITemplateManager)) # if factory is available on the interface bases of the layer we @@ -16,18 +16,23 @@ def handler(directory, layer): if layer is interface.Interface: base_factories = set() else: - base_factories = set(factory for name, factory in gsm.adapters.lookupAll( - (interface.implementedBy(layer.__bases__),), interfaces.ITemplateManager)) + base_factories = set() + for base in layer.__bases__: + for name, factory in lookup_all((base,), interfaces.ITemplateManager): + base_factories.add(factory) try: factory = factories.difference(base_factories).pop() except KeyError: - factory = manager.TemplateManagerFactory() + name = directory + factory = manager.TemplateManagerFactory(name) component.provideAdapter( - factory, (layer,), interfaces.ITemplateManager, name=directory) + factory, (layer,), interfaces.ITemplateManager, name=name) factory(layer).registerDirectory(directory) - + + return factory(layer) + def templateOverridesDirective(_context, directory, layer=interface.Interface): _context.action( discriminator = ('override', directory, layer), diff --git a/z3c/jbot/tests/overrides/http/z3c.jbot.tests.templates.example.pt b/z3c/jbot/tests/overrides/http/z3c.jbot.tests.templates.example.pt new file mode 100644 index 0000000..e83b05e --- /dev/null +++ b/z3c/jbot/tests/overrides/http/z3c.jbot.tests.templates.example.pt @@ -0,0 +1 @@ +Override from ./http. diff --git a/z3c/jbot/tests/overrides/https/z3c.jbot.tests.templates.example.pt b/z3c/jbot/tests/overrides/https/z3c.jbot.tests.templates.example.pt new file mode 100644 index 0000000..b339dc0 --- /dev/null +++ b/z3c/jbot/tests/overrides/https/z3c.jbot.tests.templates.example.pt @@ -0,0 +1 @@ +Override from ./https. diff --git a/z3c/jbot/tests/overrides/interface/z3c.jbot.tests.templates.example.pt b/z3c/jbot/tests/overrides/interface/z3c.jbot.tests.templates.example.pt new file mode 100644 index 0000000..ffd48fe --- /dev/null +++ b/z3c/jbot/tests/overrides/interface/z3c.jbot.tests.templates.example.pt @@ -0,0 +1 @@ +Override from ./interface. diff --git a/z3c/jbot/tests/overrides/request/z3c.jbot.tests.templates.example.pt b/z3c/jbot/tests/overrides/request/z3c.jbot.tests.templates.example.pt new file mode 100644 index 0000000..fc04691 --- /dev/null +++ b/z3c/jbot/tests/overrides/request/z3c.jbot.tests.templates.example.pt @@ -0,0 +1 @@ +Override from ./request. diff --git a/z3c/jbot/tests/templates/z3c.jbot.tests.templates.example.pt b/z3c/jbot/tests/templates/z3c.jbot.tests.templates.example.pt deleted file mode 100644 index caac740..0000000 --- a/z3c/jbot/tests/templates/z3c.jbot.tests.templates.example.pt +++ /dev/null @@ -1 +0,0 @@ -This template will override the example template. diff --git a/z3c/jbot/utility.py b/z3c/jbot/utility.py index 710559f..40c782d 100644 --- a/z3c/jbot/utility.py +++ b/z3c/jbot/utility.py @@ -24,7 +24,7 @@ def getRequest(): return site.request except AttributeError: return site.REQUEST - + try: i = zope.security.management.getInteraction() except zope.security.interfaces.NoInteraction: @@ -45,6 +45,6 @@ def getLayer(): def getManagers(): layer = getLayer() gsm = component.getGlobalSiteManager() - + for name, factory in gsm.adapters.lookupAll((layer,), interfaces.ITemplateManager): yield factory(layer)