diff tincan.py @ 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
line wrap: on
line diff
--- 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()