diff tincan.py @ 21:ca2029ce95c7 draft

Mostly done with new template logic, working through testing.
author David Barts <n5jrn@me.com>
date Tue, 21 May 2019 15:57:15 -0700
parents 5d9a1b82251a
children f6a1492fe56e
line wrap: on
line diff
--- 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