def __init__(self, config): self.__load_templates = util.listify( config.get("LOAD_TEMPLATES") or Provider(config)) self.__load_plugins = util.listify( config.get("LOAD_PLUGINS") or Plugins(config)) self.__load_filters = util.listify( config.get("LOAD_FILTERS") or Filters(config)) prefix_map = config.get("PREFIX_MAP") or {} self.__filter_cache = {} self.__prefix_map = {} for key, value in prefix_map.items(): if isinstance(value, str): self.__prefix_map[key] = util.slice( self.__load_templates, [int(x) for x in re.split(r"\D+", value)]) else: self.__prefix_map[key] = value if "STASH" in config: self.__stash = config["STASH"] else: predefs = config.get("VARIABLES") or config.get("PRE_DEFINE") or {} predefs.setdefault("_DEBUG", int(bool(config.get("DEBUG", 0) & DEBUG_UNDEF))) self.__stash = Stash(predefs) # compile any template BLOCKS specified as text blocks = config.get("BLOCKS") or {} b = {} for key, block in blocks.items(): if isinstance(block, str): block = self.template(util.Literal(block)) b[key] = block self.__init_blocks = self.__blocks = b self.__recursion = config.get("RECURSION", False) self.__eval_python = config.get("EVAL_PYTHON", False) self.__trim = config.get("TRIM", False) self.__blkstack = [] self.__config = config if config.get("EXPOSE_BLOCKS") is not None: self.__expose_blocks = config.get("EXPOSE_BLOCKS") else: self.__expose_blocks = False self.__debug_format = config.get("DEBUG_FORMAT") self.__debug_dirs = config.get("DEBUG", 0) & DEBUG_DIRS if config.get("DEBUG") is not None: self.__debug = config["DEBUG"] & (DEBUG_CONTEXT | DEBUG_FLAGS) else: self.__debug = self.DEBUG
def testStash(self): count = [20] def boz(): count[0] += 10 return count[0] def biz(*args): return args and args[0] or "<undef>" data = { "foo": 10, "bar": { "baz": 20 }, "baz": lambda: { "boz": boz, "biz": biz }, "obj": AnObject(name="an object"), "hashobj": HashObject(planet="World") } stash = Stash(data) self.assertEqual(10, stash.get("foo").value()) self.assertEqual(20, stash.get(["bar", 0, "baz", 0]).value()) self.assertEqual(20, stash.get("bar.baz").value()) self.assertEqual(20, stash.get("bar(10).baz").value()) self.assertEqual(30, stash.get("baz.boz").value()) self.assertEqual(40, stash.get("baz.boz").value()) self.assertEqual("<undef>", stash.get("baz.biz").value()) self.assertEqual("<undef>", stash.get("baz(50).biz").value()) # args are ignored stash.set("bar.buz", 100) self.assertEqual(100, stash.get("bar.buz").value()) ttlist = (("default", Template()), ("warn", Template({"DEBUG": constants.DEBUG_UNDEF, "DEBUG_FORMAT": ""}))) self.Expect(DATA, ttlist, data)
class Context: """Class defining a context in which a template document is processed. This is the runtime processing interface through which templates can access the functionality of the Template Toolkit. """ DEBUG = None def __init__(self, config): self.__load_templates = util.listify( config.get("LOAD_TEMPLATES") or Provider(config)) self.__load_plugins = util.listify( config.get("LOAD_PLUGINS") or Plugins(config)) self.__load_filters = util.listify( config.get("LOAD_FILTERS") or Filters(config)) prefix_map = config.get("PREFIX_MAP") or {} self.__filter_cache = {} self.__prefix_map = {} for key, value in prefix_map.items(): if isinstance(value, str): self.__prefix_map[key] = util.slice( self.__load_templates, [int(x) for x in re.split(r"\D+", value)]) else: self.__prefix_map[key] = value if "STASH" in config: self.__stash = config["STASH"] else: predefs = config.get("VARIABLES") or config.get("PRE_DEFINE") or {} predefs.setdefault("_DEBUG", int(bool(config.get("DEBUG", 0) & DEBUG_UNDEF))) self.__stash = Stash(predefs) # compile any template BLOCKS specified as text blocks = config.get("BLOCKS") or {} b = {} for key, block in blocks.items(): if isinstance(block, str): block = self.template(util.Literal(block)) b[key] = block self.__init_blocks = self.__blocks = b self.__recursion = config.get("RECURSION", False) self.__eval_python = config.get("EVAL_PYTHON", False) self.__trim = config.get("TRIM", False) self.__blkstack = [] self.__config = config if config.get("EXPOSE_BLOCKS") is not None: self.__expose_blocks = config.get("EXPOSE_BLOCKS") else: self.__expose_blocks = False self.__debug_format = config.get("DEBUG_FORMAT") self.__debug_dirs = config.get("DEBUG", 0) & DEBUG_DIRS if config.get("DEBUG") is not None: self.__debug = config["DEBUG"] & (DEBUG_CONTEXT | DEBUG_FLAGS) else: self.__debug = self.DEBUG def config(self): return self.__config def insert(self, files): """Insert the contents of a file without parsing.""" # TODO: Clean this up; unify the way "files" is passed to this routine. files = unscalar(files) if is_seq(files): files = unscalar_list(files) else: files = [unscalar(files)] prefix = providers = text = None output = StringIO() for file in files: prefix, name = split_prefix(file) if prefix: providers = self.__prefix_map.get(prefix) if not providers: self.throw(ERROR_FILE, "no providers for file prefix '%s'" % prefix) else: providers = self.__prefix_map.get( "default") or self.__load_templates for provider in providers: try: text = provider.load(name, prefix) except Exception as e: self.throw(ERROR_FILE, str(e)) if text is not None: output.write(text) break else: self.throw(ERROR_FILE, "%s: not found" % file) return output.getvalue() def throw(self, error, info=None, output=None): """Raises a TemplateException. This method may be passed an existing TemplateException object; a single value containing an error message which is used to instantiate a TemplateException of type 'None'; or a pair of values representing the exception type and info from which a TemplateException object is instantiated. e.g. context.throw(exception) context.throw("I'm sorry Dave, I can't do that") context.throw('denied', "I'm sorry Dave, I can't do that") An optional third parameter can be supplied in the last case which is a reference to the current output buffer containing the results of processing the template up to the point at which the exception was thrown. The RETURN and STOP directives, for example, use this to propagate output back to the user, but it can safely be ignored in most cases. """ error = unscalar(error) info = unscalar(info) if isinstance(error, TemplateException): raise error elif info is not None: raise TemplateException(error, info, output) else: raise TemplateException("None", error or "", output) def catch(self, error, output=None): """Called by various directives after catching an exception. The first parameter contains the errror which may be a sanitized reference to a TemplateException object (such as that raised by the throw() method above, a plugin object, and so on) or an error message raised from somewhere in user code. The latter are coerced into 'None' TemplateException objects. Like throw() above, the current output buffer may be passed as an additional parameter. As exceptions are thrown upwards and outwards from nested blocks, the catch() method reconstructs the correct output buffer from these fragments, storing it in the exception object for passing further onwards and upwards. # Returns a TemplateException object. """ if isinstance(error, TemplateException): if output: error.text(output) return error else: return TemplateException("None", error, output) def view(self, params=None): """Create a new View object bound to this context.""" from template.view import View return View(self, unscalar(params)) def process(self, template, params=None, localize=False): """Processes the template named or referenced by the first parameter. The optional second parameter may reference a dictionary of variable definitions. These are set before the template is processed by calling update() on the stash. Note that, unless the third parameter is true, the context is not localised and these, and any other variables set in the template will retain their new values after this method returns. The third parameter is in place so that this method can handle INCLUDE calls: the stash will be localized. # Returns the output of processing the template. Errors are raised as TemplateException objects. """ template = util.listify(unscalar(template)) params = unscalar(params) compileds = [] for name in template: compileds.append(self.template(name)) if localize: self.__stash = self.__stash.clone(params) else: self.__stash.update(params) output = StringIO() try: # save current component try: component = self.__stash.get("component") except: component = None for name, compiled in list(zip(template, compileds)): if not callable(compiled): element = compiled else: element = { "name": isinstance(name, str) and name or "", "modtime": time.time() } if isinstance(component, Document): # FIXME: This block is not exercised by any test. elt = Accessor(element) elt["caller"] = component.name elt["callers"] = getattr(component, "callers", []) elt["callers"].append(component.name) self.__stash.set("component", element) if not localize: # merge any local blocks defined in the Template::Document # info our local BLOCKS cache if isinstance(compiled, Document): tblocks = compiled.blocks() if tblocks: self.__blocks.update(tblocks) if callable(compiled): tmpout = compiled(self) elif util.can(compiled, "process"): tmpout = compiled.process(self) else: self.throw("file", "invalid template reference: %s" % compiled) if self.__trim: tmpout = tmpout.strip() output.write(tmpout) # pop last item from callers if isinstance(component, Document): elt["callers"].pop() self.__stash.set("component", component) finally: if localize: # ensure stash is delocalised before dying self.__stash = self.__stash.declone() return output.getvalue() def include(self, template, params=None): """Similar to process() above but processing the template in a local context. Any variables passed by dictionary as the second parameter will be set before the template is processed and then revert to their original values before the method returns. Similarly, any changes made to non-global variables within the template will persist only until the template is processed. Returns the output of processing the template. Errors are raised as TemplateException objects. """ return self.process(template, params, True) def localise(self, *args): """The localise() method creates a local copy of the current stash, allowing the existing state of variables to be saved and later restored via delocalise(). A dictionary may be passed containing local variable definitions which should be added to the cloned namespace. These values persist until delocalisation. """ self.__stash = self.__stash.clone(*args) return self.__stash def delocalise(self): self.__stash = self.__stash.declone() def plugin(self, name, args=None): """Calls on each of the LOAD_PLUGINS providers in turn to fetch() (i.e. load and instantiate) a plugin of the specified name. Additional parameters passed are propagated to the plugin's constructor. Returns a reference to a new plugin object or other object. On error, a TemplateException is raiased. """ args = unscalar_list(args) for provider in self.__load_plugins: plugin = provider.fetch(name, args, self) if plugin: return plugin self.throw(ERROR_PLUGIN, "%s: plugin not found" % name) def filter(self, name, args=None, alias=None): """Similar to plugin() above, but querying the LOAD_FILTERS providers to return filter instances. An alias may be provided which is used to save the returned filter in a local cache. """ name = unscalar(name) args = unscalar_list(args or []) filter = None if not args and isinstance(name, str): filter = self.__filter_cache.get(name) if filter: return filter for provider in self.__load_filters: filter = provider.fetch(name, args, self) if filter: if alias: self.__filter_cache[alias] = filter return filter self.throw("%s: filter not found" % name) def reset(self, blocks=None): """Reset the state of the internal BLOCKS hash to clear any BLOCK definitions imported via the PROCESS directive. Any original BLOCKS definitions passed to the constructor will be restored. """ self.__blkstack = [] self.__blocks = self.__init_blocks.copy() def template(self, name): """General purpose method to fetch a template and return it in compiled form. In the usual case, the name parameter will be a simple string containing the name of a template (e.g. 'header'). It may also be a template.document.Document object (or sub-class) or a callable object. These are considered to be compiled templates and are returned intact. Finally, it may be a file-like object with a read() method. Templates may be cached at one of 3 different levels. The internal BLOCKS member is a local cache which holds references to all template blocks used or imported via PROCESS since the context's reset() method was last called. This is checked first and if the template is not found, the method then walks down the BLOCKSTACK list. This contains references to the block definition tables in any enclosing Documents that we're visiting (e.g. we've been called via an INCLUDE and we want to access a BLOCK defined in the template that INCLUDE'd us). If nothing is defined, then we iterate through the LOAD_TEMPLATES providers list as a 'chain of responsibility' (see Design Patterns) asking each object to fetch() the template if it can. Returns the compiled template, or raises a TemplateException on error. """ if isinstance(name, Document) or callable(name): return name shortname = name prefix = providers = None if isinstance(name, str): for block in [self.__blocks] + self.__blkstack: template = block.get(name) if template: return template prefix, shortname = split_prefix(shortname) if prefix: providers = self.__prefix_map.get(prefix) if not providers: self.throw( ERROR_FILE, "no providers for template prefix '%s'" % prefix) providers = (providers or self.__prefix_map.get("default") or self.__load_templates) blockname = "" while shortname: for provider in providers: try: template = provider.fetch(shortname, prefix) except Exception as e: if isinstance( e, TemplateException) and e.type() == ERROR_FILE: self.throw(e) else: self.throw(ERROR_FILE, str(e)) if template is None: continue if blockname: template = template.blocks().get(blockname) if template: return template else: return template if not isinstance(shortname, str) or not self.__expose_blocks: break match = re.search(r"/([^/]+)$", shortname) if not match: break shortname = shortname[:match.start()] + shortname[match.end():] if blockname: blockname = "%s/%s" % (match.group(1), blockname) else: blockname = match.group(1) # TODO: This is the error thrown when a template has syntax # errors. Confusing! Is this what the Perl version does? self.throw(ERROR_FILE, "%s: not found" % name) def stash(self): """Simple accessor for the local stash object.""" return self.__stash def define_vmethod(self, *args): """Passes all args on to stash.define_vmethod.""" self.__stash.define_vmethod(*args) def visit(self, document, blocks): """Each template.document.Document calls the visit() method on the context before processing itself. It passes the dictionary of named BLOCKs defined within the document, allowing them to be added to the internal BLKSTACK list which is subsequently used by template() to resolve templates. from a provider. """ self.__blkstack.insert(0, blocks) def leave(self): """The leave() method is called when the document has finished processing itself. This removes the entry from the BLKSTACK list that was added visit() above. For persistence of BLOCK definitions, the process() method (i.e. the PROCESS directive) does some extra magic to copy BLOCKs into a shared hash. """ self.__blkstack.pop(0) def define_block(self, name, block): """Adds a new BLOCK definition to the local BLOCKS cache. block may be specified as a callable or template.document.Document object or as text which is compiled into a template. Returns a true value ('block' or the compiled block reference) if successful, or raises a TemplateException on failure. """ # NOTE: This function is entirely untested by the test suite. if isinstance(block, str): block = self.template(util.Literal(block)) self.__blocks[name] = block def define_filter(self, name, filter, dynamic=False): """Adds a new FILTER definition to the local FILTER_CACHE.""" if dynamic: filter = util.dynamic_filter(filter) for provider in self.__load_filters: try: provider.store(name, filter) return 1 except Exception as e: self.throw(ERROR_FILTER, e) self.throw(ERROR_FILTER, "FILTER providers declined to store filter %s" % name) def eval_python(self): return self.__eval_python def trim(self): return self.__trim def load_templates(self): return self.__load_templates def load_plugins(self): return self.__load_plugins def load_filters(self): return self.__load_filters def recursion(self): return self.__recursion
def testStash(self): count = [20] def boz(): count[0] += 10 return count[0] def biz(*args): return args and args[0] or "<undef>" data = { "foo": 10, "bar": { "baz": 20 }, "baz": lambda: { "boz": boz, "biz": biz }, "obj": AnObject(name="an object"), "hashobj": HashObject(planet="World") } stash = Stash(data) self.assertEquals(10, stash.get("foo").value()) self.assertEquals(20, stash.get(["bar", 0, "baz", 0]).value()) self.assertEquals(20, stash.get("bar.baz").value()) self.assertEquals(20, stash.get("bar(10).baz").value()) self.assertEquals(30, stash.get("baz.boz").value()) self.assertEquals(40, stash.get("baz.boz").value()) self.assertEquals("<undef>", stash.get("baz.biz").value()) self.assertEquals("<undef>", stash.get("baz(50).biz").value()) # args are ignored stash.set("bar.buz", 100) self.assertEquals(100, stash.get("bar.buz").value()) ttlist = (("default", Template()), ("warn", Template({"DEBUG": constants.DEBUG_UNDEF, "DEBUG_FORMAT": ""}))) self.Expect(DATA, ttlist, data)