changeset 2:ca6f8ca38cf2 draft

Another backup commit.
author David Barts <n5jrn@me.com>
date Sun, 12 May 2019 22:51:34 -0700
parents 94b36e721500
children c6902cded64d
files pspx.html tincan.py
diffstat 2 files changed, 183 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pspx.html	Sun May 12 22:51:34 2019 -0700
@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta http-equiv="content-type" content="text/html; charset=UTF-8">
+    <title>.pspx Template Files</title>
+    <style type="text/css">
+      .kbd { font-family: monospace; }
+    </style>
+  </head>
+  <body>
+    <h1>.pspx Template Files</h1>
+    <h2>Introduction</h2>
+    Files ending in a <span class="kbd">.pspx</span> extension define both
+    routes and templates. The files have two parts: a header and a body.<br>
+    <h2>The Header</h2>
+    <p>The header is optional and consists of lines starting with an octothorpe
+      (#) character. With the exception of the <code>#rem </code>header, all
+      header lines may appear only once in a given file.</p>
+    <dl>
+      <dt><code>#errors</code></dt>
+      <dd>This is an error page which handles the specified HTTP error codes.
+        The codes are specified in numeric form, separated by whitespace. If no
+        error codes are specified, this page handles all possible HTTP error
+        codes.</dd>
+      <dt><code>#forward</code></dt>
+      <dd>Ignore everything else in this template (and any code-behind
+        associated with it), using the specified route to serve it instead. The
+        route specified with <code>#forward</code> may itself contain a <code>#forward</code>,
+        but attempts to create a <code>#forward</code> loop are not allowed and
+        will cause a <span class="kbd">TinCanError</span> to be raised. Note
+        that unlike calls to <code>app.forward()</code>, the <code>#forward</code>
+        header is resolved at route-creation time; no extra processing will
+        happen at request time.</dd>
+      <dt><code>#hidden</code></dt>
+      <dd>This is a hidden page; do not create a route for it. The page can only
+        be displayed by a forward.</dd>
+      <dt><code>#methods</code></dt>
+      <dd>A list of HTTP request methods, separated by whitespace, follows. The route will
+        allow all specified methods. Not specifying this line is equivalent to
+        specifying <code>#methods GET</code>.</dd>
+      <dt><code>#python</code></dt>
+      <dd>What follows is the name of the Python file containing the code-behind
+        for this route. If not specified, the code-behind will be in a file with
+        the same name but an extension of <span class="kbd">.py</span>.</dd>
+      <dt><code>#rem</code></dt>
+      <dd>The rest of the line is treated as a remark (comment) and is ignored.</dd>
+      <dt><code>#template</code></dt>
+      <dd>Ignore the body of this file and instead use the template in the body
+        of the specified file, which must end in <span class="kbd">.pspx</span>.</dd>
+    </dl>
+    <p>It is possible to include whitespace and special characters in arguments
+      to the <code>#forward</code>, <code>#python</code>, and <code>#template</code>
+      headers by using standard Python string quoting and escaping methods. For
+      example, <code>#python "space case.py"</code>.</p>
+    <h3>Error Pages</h3>
+    <p>Error pages supersede the standard Bottle error handling, and are created
+      by using the <code>#error</code> page header. <em>Error pages have no
+        associated code-behind;</em> they consist of templates only. Error page
+      templates are provided with two variables when rendering:</p>
+    <dl>
+      <dt><code>e</code></dt>
+      <dd>The <code>bottle.HTTPError</code> object associated with this error.</dd>
+      <dt><code>request</code></dt>
+      <dd>The <code>bottle.Request</code> object associated with this error.</dd>
+    </dl>
+    <p>The behavior of specifying multiple error pages for the same error code
+    is undefined; doing so is best avoided.</p>
+    <h2>The Body</h2>
+    <p>The body begins with the first line that <em>does not</em> start with <code>#</code>
+      and has the exact same syntax that the templates are in for this webapp.
+      By default, Chameleon templates are used. Cheetah, Jinja2, Mako, and
+      Bottle SimpleTemplate templates are also supported, provided the webapp
+      was launched to use them. (Only one template style per webapp is
+      supported.)</p>
+    <p>In order to make line numbers match file line numbers for reported
+      errors, the template engine will be passed a blank line for each header
+      line encountered. TinCan will strip out all leading blank lines when
+      rendering its responses.</p>
+  </body>
+</html>
--- a/tincan.py	Sun May 12 19:19:40 2019 -0700
+++ b/tincan.py	Sun May 12 22:51:34 2019 -0700
@@ -8,14 +8,14 @@
 import ast
 import binascii
 from base64 import b16encode, b16decode
-import importlib, py_compile
+import importlib
 import io
+import py_compile
+from stat import S_ISDIR, S_ISREG
 
 import bottle
 
-# C l a s s e s
-
-# Exceptions
+# E x c e p t i o n s
 
 class TinCanException(Exception):
     """
@@ -51,6 +51,8 @@
     """
     pass
 
+# T e m p l a t e s
+#
 # Template (.pspx) files. These are standard templates for a supported
 # template engine, but with an optional set of header lines that begin
 # with '#'.
@@ -101,7 +103,7 @@
     """
     Parses and represents a set of header lines.
     """
-    _NAMES = [ "error", "forward", "methods", "python", "template" ]
+    _NAMES = [ "errors", "forward", "methods", "python", "template" ]
     _FNAMES = [ "hidden" ]
 
     def __init__(self, string):
@@ -148,6 +150,8 @@
             # Update this object
             setattr(self, name, param)
 
+# C h a m e l e o n
+#
 # Support for Chameleon templates (the kind TinCan uses by default).
 
 class ChameleonTemplate(bottle.BaseTemplate):
@@ -170,7 +174,7 @@
 chameleon_template = functools.partial(template, template_adapter=ChameleonTemplate)
 chameleon_view = functools.partial(view, template_adapter=ChameleonTemplate)
 
-# Utility functions, used in various places.
+# U t i l i t i e s
 
 def _normpath(base, unsplit):
     """
@@ -223,6 +227,8 @@
             raise TinCanError("{0}: invalid forward to {1!r}".format(source, target)) from e
         raise exc
 
+# C o d e   B e h i n d
+#
 # Represents the code-behind of one of our pages. This gets subclassed, of
 # course.
 
@@ -261,15 +267,15 @@
                 continue
             ret[name] = value
 
+# R o u t e s
+#
 # Represents a route in TinCan. Our launcher creates these on-the-fly based
 # on the files it finds.
 
-EXTENSION = ".pspx"
-CONTENT = "text/html"
-_WINF = "WEB-INF"
-_BANNED = set([_WINF])
-_CODING = "utf-8"
-_CLASS = "Page"
+_ERRMIN = 400
+_ERRMAX = 599
+_PEXTEN = ".py"
+_TEXTEN = ".pspx"
 _FLOOP = "tincan.forwards"
 _FORIG = "tincan.origin"
 
@@ -292,22 +298,20 @@
     A route created by the TinCan launcher.
     """
     def __init__(self, launcher, name, subdir):
-        self._plib = launcher.plib
         self._fsroot = launcher.fsroot
         self._urlroot = launcher.urlroot
         self._name = name
-        self._python = name + ".py"
+        self._python = name + _PEXTEN
         self._content = CONTENT
-        self._fspath = os.path.join(launcher.fsroot, *subdir, name + EXTENSION)
-        self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + EXTENSION)
+        self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN)
+        self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN)
         self._origin = self._urlpath
         self._subdir = subdir
         self._seen = set()
-        self._class = None
         self._tclass = launcher.tclass
         self._app = launcher.app
 
-    def launch(self, config):
+    def launch(self):
         """
         Launch a single page.
         """
@@ -326,15 +330,20 @@
         if hidden:
             return
         # If this is an error page, register it as such.
-        if self._header.error is not None:
+        if self._header.errors is not None:
+            if self._header.template is not None:
+                tpath = os.path.join(self._fsroot, *self._splitpath(self._header.template))
+                self._template = 
             try:
-                errors = [ int(i) for i in self._header.error.split() ]
+                errors = [ int(i) for i in self._header.errors.split() ]
             except ValueError as e:
-                raise TinCanError("{0}: bad #error line".format(self._urlpath)) from e
+                raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e
             if not errors:
-                errors = range(400, 600)
+                errors = range(_ERRMIN, _ERRMAX+1)
             route = TinCanErrorRoute(self._tclass(source=self._template.body))
             for error in errors:
+                if error < _ERRMIN or error > _ERRMAX:
+                    raise TinCanError("{0}: bad #errors code".format(self._urlpath))
                 self._app.error(code=error, callback=route)
             return  # this implies #hidden
         # Get methods for this route
@@ -344,8 +353,8 @@
             methods = [ i.upper() for i in self._header.methods.split() ]
         # Process other header entries
         if self._header.python is not None:
-            if not self._header.python.endswith('.py'):
-                raise TinCanError("{0}: #python files must end in .py", self._urlpath)
+            if not self._header.python.endswith(_PEXTEN):
+                raise TinCanError("{0}: #python files must end in {1}", self._urlpath, _PEXTEN)
             self._python = self._header.python
         # Obtain a class object by importing and introspecting a module.
         pypath = os.path.normpath(os.path.join(self._fsroot, *self._subdir, self._python))
@@ -360,7 +369,6 @@
             except Exception as e:
                 raise TinCanError("{0}: error compiling".format(pypath)) from e
         try:
-            self._mangled = self._manage_module()
             spec = importlib.util.spec_from_file_location(_mangle(self._name), cpath)
             mod =  importlib.util.module_from_spec(spec)
             spec.loader.exec_module(mod)
@@ -398,10 +406,10 @@
         except IndexError as e:
             raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e
         name, ext = os.path.splitext(rname)[1]
-        if ext != EXTENSION:
+        if ext != _TEXTEN:
             raise TinCanError("{0}: invalid #forward".format(self._urlpath))
         self._subdir = rlist
-        self._python = name + ".py"
+        self._python = name + _PEXTEN
         self._fspath = os.path.join(self._fsroot, *self._subdir, rname)
         self._urlpath = self._urljoin(*self._subdir, rname)
 
@@ -416,10 +424,10 @@
         This gets called by the framework AFTER the page is launched.
         """
         target = None
+        obj = self._class(bottle.request, bottle.response)
         try:
-            obj = self._class(bottle.request, bottle.response)
             obj.handle()
-            return self._body.render(obj.export())
+            return self._body.render(obj.export()).lstrip('\n')
         except ForwardException as fwd:
             target = fwd.target
         if target is None:
@@ -427,10 +435,10 @@
         # We get here if we are doing a server-side programmatic
         # forward.
         environ = bottle.request.environ
-        if _FLOOP not in environ:
-            environ[_FLOOP] = set()
         if _FORIG not in environ:
             environ[_FORIG] = self._urlpath
+        if _FLOOP not in environ:
+            environ[_FLOOP] = set([self._urlpath])
         elif target in environ[_FLOOP]:
             TinCanError("{0}: forward loop detected".format(environ[_FORIG]))
         environ[_FLOOP].add(target)
@@ -450,3 +458,67 @@
             if not callable(value):
                 ret[name] = value
         return ret
+
+# L a u n c h e r
+
+_WINF = "WEB-INF"
+_BANNED = set([_WINF])
+
+class _Launcher(object):
+    """
+    Helper class for launching webapps.
+    """
+    def __init__(self, fsroot, urlroot, tclass):
+        """
+        Lightweight constructor. The real action happens in .launch() below.
+        """
+        self.fsroot = fsroot
+        self.urlroot = urlroot
+        self.tclass = tclass
+        self.app = None
+
+    def launch(self):
+        """
+        Does the actual work of launching something. XXX - modifies sys.path
+        and never un-modifies it.
+        """
+        # Sanity checks
+        if not self.urlroot.startswith("/"):
+            raise TinCanError("urlroot must be absolute")
+        if not os.path.isdir(self.fsroot):
+            raise TinCanError("no such directory: {0!r}".format(self.fsroot))
+        # Make WEB-INF, if needed
+        winf = os.path.join(self.fsroot, _WINF)
+        lib = os.path.join(winf, "lib")
+        for i in [ winf, lib ]:
+            if not os.path.isdir(i):
+                os.mkdir(i)
+        # Add our private lib directory to sys.path
+        sys.path.insert(1, os.path.abspath(lib))
+        # Do what we gotta do
+        self.app = TinCan()
+        self._launch([])
+        return self.app
+
+    def _launch(self, subdir):
+        for entry in os.listdir(os.path.join(self.fsroot, *subdir)):
+            if not subdir and entry in _BANNED:
+                continue
+            etype = os.stat(os.path.join(self.fsroot, *subdir, entry)).st_mode
+            if S_ISREG(etype):
+                ename, eext = os.path.splitext(entry)
+                if eext != _TEXTEN:
+                    continue  # only look at interesting files
+                route = TinCanRoute(self, ename, subdir)
+                page.launch()
+            elif S_ISDIR(etype):
+                self._launch(subdir + [entry])
+
+def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate):
+    """
+    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()
+    return _Launcher(fsroot, urlroot, tclass).launch()