diff tincan.py @ 24:34d3cfcd37ef draft header-includes

First batch of work on getting a #include header. Unfinished.
author David Barts <n5jrn@me.com>
date Wed, 22 May 2019 07:47:16 -0700
parents e8b6ee7e5b6b
children e93e5e746cc5
line wrap: on
line diff
--- a/tincan.py	Tue May 21 18:01:44 2019 -0700
+++ b/tincan.py	Wed May 22 07:47:16 2019 -0700
@@ -28,6 +28,9 @@
     """
     pass
 
+class BaseSyntaxError(TinCanException):
+
+
 class TemplateHeaderError(TinCanException):
     """
     Raised upon encountering a syntax error in the template headers.
@@ -40,6 +43,19 @@
     def __str__(self):
         return "line {0}: {1}".format(self.line, self.message)
 
+class IncludeError(TinCanException):
+    """
+    Raised when we run into problems #include'ing something, usually
+    because it doesn't exist.
+    """
+    def __init__(self, message, source):
+        super().__init__(message, source)
+        self.message = message
+        self.source = source
+
+    def __str__(self):
+        return "{0}: {1}".format(self.source, self.message)
+
 class ForwardException(TinCanException):
     """
     Raised to effect the flow control needed to do a forward (server-side
@@ -116,6 +132,7 @@
     """
     _NAMES = [ "errors", "forward", "methods", "python", "template" ]
     _FNAMES = [ "hidden" ]
+    _ANAMES = [ "include" ]
 
     def __init__(self, string):
         # Initialize our state
@@ -150,7 +167,8 @@
                 raise TemplateHeaderError("Invalid directive: {0!r}".format(rna), count)
             if name in seen:
                 raise TemplateHeaderError("Duplicate {0!r} directive.".format(rna), count)
-            seen.add(name)
+            if name not in self._ANAMES:
+                seen.add(name)
             # Flags
             if name in self._FNAMES:
                 setattr(self, name, True)
@@ -164,7 +182,10 @@
                     param = ast.literal_eval(param)
                     break
             # Update this object
-            setattr(self, name, param)
+            if name in self._ANAMES:
+                getattr(self, name).append(param)
+            else:
+                setattr(self, name, param)
 
 # C h a m e l e o n
 #
@@ -305,6 +326,67 @@
         self.request = req
         self.error = err
 
+# 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 = []
+        self._tlib = os.path.join(base, _WINF, "tlib")
+
+    def render(self, includes, body):
+        for i in includes:
+            if i.startswith('<') and i.endswith('>'):
+                self._buf.append(self._render1(self._tlib, i[1:-1]))
+            else:
+                self._buf.append(self._render1(self.subdir, i))
+        self._buf.append(body)
+        return ''.join(self._buf)
+
+    def _render1(self, subdir, path):
+        # Reject bad file names
+        if not path.endswith(_IEXTEN) or path.endswith(_PEXTEN):
+            raise IncludeError(self.name, "#include files must end with {0} or {1}".format(_IEXTEN, _PEXTEN))
+        # 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(self.name, str(e)) from e
+        try:
+            th = TemplateHeader(tf.headers)
+        except TemplateHeaderError as e:
+            raise IncludeError(npath, str(e)) 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 is not 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.includes, tf.body)
+
 # R o u t e s
 #
 # Represents a route in TinCan. Our launcher creates these on-the-fly based
@@ -411,15 +493,28 @@
             if not self._header.template.endswith(_TEXTEN):
                 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN))
             try:
-                tpath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._header.template)))
+                rawpath = self._splitpath(self._header.template)
+                tsubdir = rawpath[:-1]
+                tpath = os.path.normpath(os.path.join(self._fsroot, *rawpath))
+                turlpath = '/' + self._urljoin(rawpath)
                 tfile = TemplateFile(tpath)
-            except OSError as e:
+                thead = TemplateHeader(tpath.header)
+            except (OSError, TemplateHeaderError) 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
-            self._body = self._tclass(source=tfile.body)
+            includer = _Includer(self._fspath, tsubdir, turlpath)
+            try:
+                self._body = self._tclass(source=includer.render(thead.include, tfile.body))
+            except IncludeError as e:
+                raise TinCanError("{0}: #include error: {1!s}".format(turlpath, e)) from e
         else:
-            self._body = self._tclass(source=self._template.body)
+            includer = _Includer(self._fspath, self._subdir, self._urlpath)
+            try:
+                self._body = self._tclass(
+                    source=includer.render(self._header.include, self._template.body))
+            except IncludeError as e:
+                raise TinCanError("{0}: #include error: {1!s}".format(self._urlpath, e)) from e
         self._body.prepare()
         # If this is an #errors page, register it as such.
         if oheader.errors is not None: