changeset 29:2e3ac3d7b0a4 draft header-includes

A possible workaround for the drainbamage (needs testing)?
author David Barts <n5jrn@me.com>
date Mon, 27 May 2019 14:11:22 -0700
parents cc6ba7834294
children f34d5a90d618
files tincan.py
diffstat 1 files changed, 63 insertions(+), 104 deletions(-) [+]
line wrap: on
line diff
--- a/tincan.py	Sun May 26 13:14:26 2019 -0700
+++ b/tincan.py	Mon May 27 14:11:22 2019 -0700
@@ -326,63 +326,27 @@
 
 # I n c l u s i o n
 #
-# This is where the #include directives get processed
-
-_IEXTEN = ".pt"
-
-class _Includer(object):
-    def __init__(self, base, subdir, name, included=None, encoding="utf-8"):
-        self.base = base
-        self.subdir = subdir
-        self.name = name
-        self.included = set() if included is None else included
-        self.encoding = encoding
-        self._buf = []
-
-    def render(self, includes, body):
-        for i in includes:
-            if i.startswith('<') and i.endswith('>'):
-                self._buf.append(self._render1([_WINF, "tlib"], i[1:-1]))
-            else:
-                self._buf.append(self._render1(self.subdir, i))
-        self._buf.append(body)
-        return ''.join(self._buf)
+# Most processing is in the TinCanRoute class; this just interprets and
+# represents arguments to the #include header directive.
 
-    def _render1(self, subdir, path):
-        # Reject bad file names
-        if not path.endswith(_IEXTEN) or path.endswith(_TEXTEN):
-            raise IncludeError("file names must end with {0} or {1}".format(_IEXTEN, _TEXTEN), self.name)
-        # Normalize
-        rawpath = _normpath(subdir, path)
-        # Only include once
-        relpath = '/' + '/'.join(rawpath)
-        if relpath in self.included:
-            return
-        self.included.add(relpath)
-        # Do actual inclusion of a file
-        npath = os.path.join(self.base, *rawpath)
-        nsubdir = rawpath[:-1]
-        try:
-            tf = TemplateFile(npath)
-        except OSError as e:
-            raise IncludeError(str(e), self.name) from e
-        try:
-            th = TemplateHeader(tf.header)
-        except TemplateHeaderError as e:
-            raise IncludeError(str(e), npath) from e
-        # Reject unsupported crap (included files can only #include)
-        for i in dir(th):
-            if i.startswith('_') or i == 'include':
-                continue
-            v = getattr(th, i)
-            if callable(v):
-                continue
-            if v is not None and v != False:
-                raise IncludeError(npath, "unsupported #{0}".format(i))
-        # Inclusion is recursive...
-        nested = _Includer(self.base, nsubdir, relpath,
-            included=self.included, encoding=self.encoding)
-        return nested.render(th.include, tf.body)
+class _IncludedFile(object):
+    def __init__(self, raw):
+        if raw.startswith('<') and raw.endswith('>'):
+            raw = raw[1:-1]
+            self.in_lib = True
+        else:
+            self.in_lib = False
+        comma = raw.find(',')
+        if comma < 0:
+            raise ValueError("missing comma")
+        self.vname = raw[:comma]
+        if self.vname == "":
+            raise ValueError("empty variable name")
+        self.fname = raw[comma+1:]
+        if self.fname == "":
+            raise ValueError("empty file name")
+        if not self.fname.endswith(_IEXTEN):
+            raise ValueError("file does not end in {0}".format(_IEXTEN))
 
 # R o u t e s
 #
@@ -391,6 +355,7 @@
 
 _ERRMIN = 400
 _ERRMAX = 599
+_IEXTEN = ".pt"
 _PEXTEN = ".py"
 _TEXTEN = ".pspx"
 _FLOOP = "tincan.forwards"
@@ -404,9 +369,10 @@
     custom code-behind, only two variables are available to your template:
     request (bottle.Request) and error (bottle.HTTPError).
     """
-    def __init__(self, template, klass):
+    def __init__(self, template, includes, klass):
         self._template = template
         self._template.prepare()
+        self._includes = includes
         self._class = klass
 
     def __call__(self, e):
@@ -414,7 +380,9 @@
         try:
             obj = self._class(bottle.request, e)
             obj.handle()
-            return self._template.render(obj.export())
+            tvars = self._includes.copy()
+            tvars.update(obj.export())
+            return self._template.render(tvars)
         except bottle.HTTPResponse as e:
             return e
         except Exception as e:
@@ -486,28 +454,47 @@
             self._python_specified = True
         # Obtain a class object by importing and introspecting a module.
         self._getclass()
-        # Build body object (#template) and process #includes.
+        # Build body object (#template) and obtain #includes.
         if self._header.template is not None:
             if not self._header.template.endswith(_TEXTEN):
                 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN))
             try:
-                rawpath = self._splitpath(self._header.template)
-                tsubdir = rawpath[:-1]
-                tname = rawpath[-1]
-                tpath = os.path.normpath(os.path.join(self._fsroot, *rawpath))
-                turlpath = '/' + self._urljoin(rawpath)
+                rtpath = self._splitpath(self._header.template)
+                tpath = os.path.normpath(os.path.join(self._fsroot, *rtpath))
                 tfile = TemplateFile(tpath)
-                thead = TemplateHeader(tfile.header)
-            except (OSError, TemplateHeaderError) as e:
+            except OSError as e:
                 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
-            r = self._getbody(tsubdir, tname, thead.include, tfile.body)
-            self._dumpbody(r, tpath, turlpath)
+            self._body = self._tclass(source=tfile.body)
+            try:
+                includes = TemplateHeader(tfile.header).include
+            except TemplateHeaderError as e:
+                raise TinCanError("{0}: {1!s}".format(self._fspath, e)) from e
+            ibase = rtpath[:-1]
         else:
-            r = self._getbody(self._subdir, self._name+_TEXTEN,
-                self._header.include, self._template.body)
-            self._dumpbody(r, self._fspath, self._urlpath)
+            self._body = self._tclass(source=self._template.body)
+            includes = self._header.include
+            ibase = self._subdir
+        self._body.prepare()
+        # Process includes
+        self._includes = {}
+        for include in includes:
+            try:
+                include = _IncludedFile(include)
+            except ValueError as e:
+                raise TinCanError("{0}: bad #include: {1!s}", self._urlpath, e) from e
+            if include.in_lib:
+                fdir = os.path.join(self._fsroot, _WINF, "tlib")
+            else:
+                fdir = os.path.join(self._fsroot, *ibase)
+            try:
+                tmpl = self._tclass(name=include.fname, lookup=[fdir])
+                tmpl.prepare()
+                # tmpl.render()  # is this needed?
+            except Exception as e:
+                raise TinCanError("{0}: bad #include: {1!s}".format(self._urlpath, e)) from e
+            self._includes[include.vname] = tmpl.tpl
         # If this is an #errors page, register it as such.
         if oheader.errors is not None:
             self._mkerror(oheader.errors)
@@ -523,27 +510,6 @@
         print("adding route:", self._origin, '('+','.join(methods)+')')  # debug
         self._app.route(self._origin, methods, self)
 
-    def _getbody(self, subdir, name, include, body):
-        includer = _Includer(self._fsroot, subdir, name)
-        try:
-            rendered = includer.render(include, body)
-        except IncludeError as e:
-            raise TinCanError(str(e)) from e
-        try:
-            self._body = self._tclass(source=rendered)
-            self._body.prepare()
-        except Exception as e:
-            raise TinCanError("{0}: template error: {1!s}".format(self._urlpath, e)) from e
-        return rendered
-
-    def _dumpbody(self, rendered, based_on, logname):
-        if self._save_includes:
-            try:
-                with open(based_on + "i", "w") as fp:
-                    fp.write(rendered)
-            except OSError as e:
-                raise TinCanError("{0}: {1!s}".format(logname, e)) from e
-
     def _splitpath(self, unsplit):
         return _normpath(self._subdir, unsplit)
 
@@ -554,7 +520,8 @@
             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)
+        route = _TinCanErrorRoute(self._tclass(source=self._template.body),
+            self._includes, self._class)
         for error in errors:
             if error < _ERRMIN or error > _ERRMAX:
                 raise TinCanError("{0}: bad #errors code".format(self._urlpath))
@@ -667,7 +634,9 @@
         try:
             obj = self._class(bottle.request, bottle.response)
             obj.handle()
-            return self._body.render(obj.export())
+            tvars = self._includes.copy()
+            tvars.update(obj.export())
+            return self._body.render(tvars)
         except ForwardException as fwd:
             target = fwd.target
         except bottle.HTTPResponse as e:
@@ -698,16 +667,6 @@
         environ['route.url_args'] = args
         return route.call(**args)
 
-    def _mkdict(self, obj):
-        ret = {}
-        for name in dir(obj):
-            if name.startswith('_'):
-                continue
-            value = getattr(obj, name)
-            if not callable(value):
-                ret[name] = value
-        return ret
-
 # L a u n c h e r
 
 _WINF = "WEB-INF"