# HG changeset patch # User David Barts # Date 1558479435 25200 # Node ID ca2029ce95c7af78543a60a1553cc302d864e739 # Parent 6bf9a41a09f22b2c6158b7418408e0b3dfe8f9f9 Mostly done with new template logic, working through testing. diff -r 6bf9a41a09f2 -r ca2029ce95c7 tincan.py --- a/tincan.py Tue May 21 14:50:20 2019 -0700 +++ b/tincan.py Tue May 21 15:57:15 2019 -0700 @@ -18,6 +18,7 @@ import traceback import urllib +from chameleon import PageTemplate, PageTemplateFile import bottle # E x c e p t i o n s @@ -166,13 +167,15 @@ # Update this object setattr(self, name, param) -# C h a m e l e o n +# C h a m e l e o n ( B o t t l e ) # -# Support for Chameleon templates (the kind TinCan uses by default). +# Support for Chameleon templates (the kind TinCan uses by default). This +# allows the usage of Chameleon with Bottle itself. It is here as a +# convenience for those using Bottle routes as well as TinCan webapps; +# this is NOT how Tincan uses Chameleon class ChameleonTemplate(bottle.BaseTemplate): def prepare(self, **options): - from chameleon import PageTemplate, PageTemplateFile if self.source: self.tpl = PageTemplate(self.source, encoding=self.encoding, **options) @@ -190,6 +193,61 @@ chameleon_template = functools.partial(bottle.template, template_adapter=ChameleonTemplate) chameleon_view = functools.partial(bottle.view, template_adapter=ChameleonTemplate) +# C h a m e l e o n ( T i n c a n ) +# +# How we use Chameleon ourselves. Everything is loaded as a string, and we +# provide convenience routines to load other templates. + +class TinCanChameleon(PageTemplate): + """ + Basically, a standard PageTemplate with load and lload functionality. + """ + def __init__(self, body, base=None, subdir=None, **config): + super(TinCanChameleon, self).__init__(body, **config) + if base is not None: + encoding = config.get("encoding", "utf-8") + if subdir is not None: + self.expression_types['load'] = \ + _LoaderFactory(base, subdir, encoding) + self.expression_types['lload'] = \ + _LoaderFactory(os.path.join(base,_WINF,"tlib"), None, encoding) + +class _LoaderFactory(object): + """ + One of two helper classes for the above. + """ + def __init__(self, base, subdir, encoding): + self.base = base + self.subdir = subdir + self.encoding = encoding + + def __call__(self, string): + return _Loader(self, string) + +class _Loader(object): + """ + Two of two helper classes for the above. + """ + def __init__(self, based_on, string): + if not (string.endswith(".pspx") or string.endswith(".pt")): + raise ValueError("loaded templates must end in .pspx or .pt") + self.path = string + self.params = based_on + + def __call__(self, target, engine): + if self.params.subdir is None: + npath = os.path.join(self.params.base, self.path.lstrip('/')) + else: + try: + normalized = _normpath(self.params.subdir, self.path) + except IndexError: + raise ValueError("invalid path: {0!s}".format(self.path)) + npath = os.path.join(self.params.base, *normalized) + with open(npath, "r", encoding=self.params.encoding) as fp: + contents = fp.read() + value = ast.Str(contents) + return [ast.Assign(targets=[target], value=value)] + # U t i l i t i e s def _normpath(base, unsplit): @@ -327,7 +385,6 @@ """ def __init__(self, template, klass): self._template = template - self._template.prepare() self._class = klass def __call__(self, e): @@ -335,7 +392,7 @@ try: obj = self._class(bottle.request, e) obj.handle() - return self._template.render(obj.export()).lstrip('\n') + return self._template.render(**obj.export()).lstrip('\n') except bottle.HTTPResponse as e: return e except Exception as e: @@ -361,7 +418,6 @@ self._origin = self._urlpath self._subdir = subdir self._seen = set() - self._tclass = launcher.tclass self._app = launcher.app def launch(self): @@ -417,10 +473,15 @@ raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e except IndexError as e: raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e - self._body = self._tclass(source=tfile.body) + try: + self._body = TinCanChameleon(tfile.body, base=self._fsroot, subdir=self._subdir) + except Exception as e: + raise TinCanError("{0}: template error: {1!s}".format(self._urlpath, e)) from e else: - self._body = self._tclass(source=self._template.body) - self._body.prepare() + try: + self._body = TinCanChameleon(self._template.body, self._fsroot, subdir=self._subdir) + except Exception as e: + raise TinCanError("{0}: template error: {1!s}".format(self._urlpath, e)) from e # If this is an #errors page, register it as such. if oheader.errors is not None: self._mkerror(oheader.errors) @@ -446,7 +507,11 @@ raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e if not errors: errors = range(_ERRMIN, _ERRMAX+1) - route = _TinCanErrorRoute(self._tclass(source=self._template.body), self._class) + try: + template = TinCanChameleon(self._template.body, base=self._fsroot, subdir=self._subdir) + except Exception as e: + raise TinCanError("{0}: template error: {1!s}".format(self._urlpath, e)) from e + route = _TinCanErrorRoute(template, self._class) for error in errors: if error < _ERRMIN or error > _ERRMAX: raise TinCanError("{0}: bad #errors code".format(self._urlpath)) @@ -559,7 +624,7 @@ try: obj = self._class(bottle.request, bottle.response) obj.handle() - return self._body.render(obj.export()).lstrip('\n') + return self._body.render(**obj.export()).lstrip('\n') except ForwardException as fwd: target = fwd.target except bottle.HTTPResponse as e: @@ -609,13 +674,12 @@ """ Helper class for launching webapps. """ - def __init__(self, fsroot, urlroot, tclass, logger): + def __init__(self, fsroot, urlroot, logger): """ Lightweight constructor. The real action happens in .launch() below. """ self.fsroot = fsroot self.urlroot = urlroot - self.tclass = tclass self.logger = logger self.app = None self.errors = 0 @@ -680,14 +744,14 @@ sys.stderr.write(message) sys.stderr.write('\n') -def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger): +def launch(fsroot=None, urlroot='/', logger=_logger): """ Launch and return a TinCan webapp. Does not run the app; it is the caller's responsibility to call app.run() """ if fsroot is None: fsroot = os.getcwd() - launcher = _Launcher(fsroot, urlroot, tclass, logger) + launcher = _Launcher(fsroot, urlroot, logger) # launcher.debug = True launcher.launch() return launcher.app, launcher.errors