changeset 25:e93e5e746cc5 draft header-includes

Preliminary debugging, still not fully tested.
author David Barts <n5jrn@me.com>
date Sun, 26 May 2019 11:43:48 -0700
parents 34d3cfcd37ef
children cc6ba7834294
files launch tincan.py
diffstat 2 files changed, 53 insertions(+), 37 deletions(-) [+]
line wrap: on
line diff
--- a/launch	Wed May 22 07:47:16 2019 -0700
+++ b/launch	Sun May 26 11:43:48 2019 -0700
@@ -18,13 +18,16 @@
 parser = ArgumentParser(prog=sys.argv[0], usage="%(prog)s [options] [directory [path]]")
 opt = parser.add_argument
 opt("-b", "--bind", default="localhost", help="address to bind to")
+opt("-d", "--debug", action="store_true", help="enable debug mode")
+opt("-f", "--force", action="store_true", help="do not abort on errors")
 opt("-p", "--port", default=8080, help="port to listen on")
 opt("directory", default=".", help="directory to serve", nargs='?')
 opt("path", default="/", help="URL path to serve", nargs='?')
 args = parser.parse_args(sys.argv[1:])
-app, errors = launch(fsroot=args.directory, urlroot=args.path)
+app, errors = launch(fsroot=args.directory, urlroot=args.path, debug=args.debug)
 if errors:
-    sys.stderr.write("{0}: {1} error{2} detected, aborting\n".format(
-        MYNAME, errors, "" if errors == 1 else "s"))
-    sys.exit(1)
+    action = "continuing" if args.force else "aborting"
+    sys.stderr.write("{0}: {1} error{2} detected, {3}\n".format(
+        MYNAME, errors, "" if errors == 1 else "s", action))
+    if not args.force: sys.exit(1)
 app.run(host=args.bind, port=args.port)
--- a/tincan.py	Wed May 22 07:47:16 2019 -0700
+++ b/tincan.py	Sun May 26 11:43:48 2019 -0700
@@ -28,9 +28,6 @@
     """
     pass
 
-class BaseSyntaxError(TinCanException):
-
-
 class TemplateHeaderError(TinCanException):
     """
     Raised upon encountering a syntax error in the template headers.
@@ -54,7 +51,7 @@
         self.source = source
 
     def __str__(self):
-        return "{0}: {1}".format(self.source, self.message)
+        return "{0}: #include error: {1}".format(self.source, self.message)
 
 class ForwardException(TinCanException):
     """
@@ -121,7 +118,6 @@
         if line.startswith(self._END) and (len(line) == self._LEND or line[self._LEND] in self._WS):
             self._state = self._body
         self._hbuf.append(line)
-        self._bbuf.append("\n")
 
     def _body(self, line):
         self._bbuf.append(line)
@@ -140,9 +136,11 @@
             setattr(self, i, None)
         for i in self._FNAMES:
             setattr(self, i, False)
+        for i in self._ANAMES:
+            setattr(self, i, [])
         # Parse the string
         count = 0
-        nameset = set(self._NAMES + self._FNAMES)
+        nameset = set(self._NAMES + self._FNAMES + self._ANAMES)
         seen = set()
         lines = string.split("\n")
         if lines and lines[-1] == "":
@@ -340,12 +338,11 @@
         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]))
+                self._buf.append(self._render1([_WINF, "tlib"], i[1:-1]))
             else:
                 self._buf.append(self._render1(self.subdir, i))
         self._buf.append(body)
@@ -353,8 +350,8 @@
 
     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))
+        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
@@ -368,11 +365,11 @@
         try:
             tf = TemplateFile(npath)
         except OSError as e:
-            raise IncludeError(self.name, str(e)) from e
+            raise IncludeError(str(e), self.name) from e
         try:
-            th = TemplateHeader(tf.headers)
+            th = TemplateHeader(tf.header)
         except TemplateHeaderError as e:
-            raise IncludeError(npath, str(e)) from 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':
@@ -380,12 +377,12 @@
             v = getattr(th, i)
             if callable(v):
                 continue
-            if v is not None and v is not False:
+            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.includes, tf.body)
+        return nested.render(th.include, tf.body)
 
 # R o u t e s
 #
@@ -417,7 +414,7 @@
         try:
             obj = self._class(bottle.request, e)
             obj.handle()
-            return self._template.render(obj.export()).lstrip('\n')
+            return self._template.render(obj.export())
         except bottle.HTTPResponse as e:
             return e
         except Exception as e:
@@ -445,6 +442,7 @@
         self._seen = set()
         self._tclass = launcher.tclass
         self._app = launcher.app
+        self._save_includes = launcher.debug
 
     def launch(self):
         """
@@ -488,34 +486,28 @@
             self._python_specified = True
         # Obtain a class object by importing and introspecting a module.
         self._getclass()
-        # Build body object (#template)
+        # Build body object (#template) and process #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)
                 tfile = TemplateFile(tpath)
-                thead = TemplateHeader(tpath.header)
+                thead = TemplateHeader(tfile.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
-            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
+            r = self._getbody(tsubdir, tname, thead.include, tfile.body)
+            self._dumpbody(r, tpath, turlpath)
         else:
-            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()
+            r = self._getbody(self._subdir, self._name+_TEXTEN,
+                self._header.include, self._template.body)
+            self._dumpbody(r, self._fspath, self._urlpath)
         # If this is an #errors page, register it as such.
         if oheader.errors is not None:
             self._mkerror(oheader.errors)
@@ -531,6 +523,27 @@
         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(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)
 
@@ -654,7 +667,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())
         except ForwardException as fwd:
             target = fwd.target
         except bottle.HTTPResponse as e:
@@ -775,7 +788,7 @@
     sys.stderr.write(message)
     sys.stderr.write('\n')
 
-def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger):
+def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger, debug=False):
     """
     Launch and return a TinCan webapp. Does not run the app; it is the
     caller's responsibility to call app.run()
@@ -783,7 +796,7 @@
     if fsroot is None:
         fsroot = os.getcwd()
     launcher = _Launcher(fsroot, urlroot, tclass, logger)
-    # launcher.debug = True
+    launcher.debug = debug
     launcher.launch()
     return launcher.app, launcher.errors