diff tincan.py @ 4:0d47859f792a draft

Finally got "hello, world" working. Still likely many bugs.
author David Barts <n5jrn@me.com>
date Mon, 13 May 2019 12:38:26 -0700
parents c6902cded64d
children 31bb8400e6e3
line wrap: on
line diff
--- a/tincan.py	Mon May 13 06:53:08 2019 -0700
+++ b/tincan.py	Mon May 13 12:38:26 2019 -0700
@@ -8,7 +8,9 @@
 import ast
 import binascii
 from base64 import b16encode, b16decode
+import functools
 import importlib
+from inspect import isclass
 import io
 import py_compile
 from stat import S_ISDIR, S_ISREG
@@ -33,7 +35,7 @@
         self.line = line
 
     def __str__(self):
-        return "Line {0}: {1}".format(self.line, self.message)
+        return "line {0}: {1}".format(self.line, self.message)
 
 class ForwardException(TinCanException):
     """
@@ -158,11 +160,11 @@
     def prepare(self, **options):
         from chameleon import PageTemplate, PageTemplateFile
         if self.source:
-            self.tpl = chameleon.PageTemplate(self.source,
-                encoding=self.encoding, **options)
+            self.tpl = PageTemplate(self.source, encoding=self.encoding,
+                **options)
         else:
-            self.tpl = chameleon.PageTemplateFile(self.filename,
-                encoding=self.encoding, search_path=self.lookup, **options)
+            self.tpl = PageTemplateFile(self.filename, encoding=self.encoding,
+                search_path=self.lookup, **options)
 
     def render(self, *args, **kwargs):
         for dictarg in args:
@@ -171,8 +173,8 @@
         _defaults.update(kwargs)
         return self.tpl.render(**_defaults)
 
-chameleon_template = functools.partial(template, template_adapter=ChameleonTemplate)
-chameleon_view = functools.partial(view, template_adapter=ChameleonTemplate)
+chameleon_template = functools.partial(bottle.template, template_adapter=ChameleonTemplate)
+chameleon_view = functools.partial(bottle.view, template_adapter=ChameleonTemplate)
 
 # U t i l i t i e s
 
@@ -266,6 +268,7 @@
             if callable(value):
                 continue
             ret[name] = value
+        return ret
 
 # R o u t e s
 #
@@ -279,7 +282,7 @@
 _FLOOP = "tincan.forwards"
 _FORIG = "tincan.origin"
 
-class TinCanErrorRoute(object):
+class _TinCanErrorRoute(object):
     """
     A route to an error page. These never have code-behind, don't get
     routes created for them, and are only reached if an error routes them
@@ -293,7 +296,7 @@
     def __call__(self, e):
         return self._template.render(e=e, request=bottle.request).lstrip('\n')
 
-class TinCanRoute(object):
+class _TinCanRoute(object):
     """
     A route created by the TinCan launcher.
     """
@@ -302,7 +305,6 @@
         self._urlroot = launcher.urlroot
         self._name = name
         self._python = name + _PEXTEN
-        self._content = CONTENT
         self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN)
         self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN)
         self._origin = self._urlpath
@@ -319,7 +321,10 @@
         hidden = None
         while True:
             self._template = TemplateFile(self._fspath)
-            self._header = TemplateHeader(self._template.header)
+            try:
+                self._header = TemplateHeader(self._template.header)
+            except TemplateHeaderException as e:
+                raise TinCanError("{0}: {1!s}".format(self._fspath, e)) from e
             if hidden is None:
                 if self._header.errors is not None:
                     break
@@ -329,33 +334,33 @@
             if self._header.forward is None:
                 break
             self._redirect()
-        # If this is a hidden page, we ignore it for now, since hidden pages
+        # If this is a #hidden page, we ignore it for now, since hidden pages
         # don't get routes made for them.
         if hidden:
             return
-        # If this is an error page, register it as such.
+        # If this is an #errors page, register it as such.
         if self._header.errors is not None:
             self._mkerror()
             return  # this implies #hidden
-        # Get methods for this route
+        # Get #methods for this route
         if self._header.methods is None:
             methods = [ 'GET' ]
         else:
             methods = [ i.upper() for i in self._header.methods.split() ]
             if not methods:
                 raise TinCanError("{0}: no #methods specified".format(self._urlpath))
-        # Process other header entries
+        # Get the code-behind #python
         if self._header.python is not None:
             if not self._header.python.endswith(_PEXTEN):
-                raise TinCanError("{0}: #python files must end in {1}", self._urlpath, _PEXTEN)
+                raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN))
             self._python = self._header.python
         # Obtain a class object by importing and introspecting a module.
         self._getclass()
-        # Build body object (Chameleon template)
+        # Build body object (#template)
         if self._header.template is not None:
             if not self._header.template.endswith(_TEXTEN):
-                raise TinCanError("{0}: #template files must end in {1}", self._urlpath, _TEXTEN)
-            tpath = os.path.join(self._fsroot, *self._splitpath(self._header.template))
+                raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN))
+            tpath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._header.template)))
             tfile = TemplateFile(tpath)
             self._body = self._tclass(source=tfile.body)
         else:
@@ -375,26 +380,28 @@
             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))
+        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)
 
     def _getclass(self):
-        pypath = os.path.normpath(os.path.join(self._fsroot, *self._subdir, self._python))
+        pypath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._python)))
         pycpath = pypath + 'c'
         try:
             pyctime = os.stat(pycpath).st_mtime
         except OSError:
             pyctime = 0
-        if pyctime < os.stat(pypath).st_mtime:
-            try:
-                py_compile.compile(pypath, cfile=pycpath)
-            except Exception as e:
-                raise TinCanError("{0}: error compiling".format(pypath)) from e
         try:
-            spec = importlib.util.spec_from_file_location(_mangle(self._name), cpath)
+            if pyctime < os.stat(pypath).st_mtime:
+                py_compile.compile(pypath, cfile=pycpath, doraise=True)
+        except py_compile.PyCompileError as e:
+            raise TinCanError(str(e)) from e
+        except Exception as e:
+            raise TinCanError("{0}: {1!s}".format(pypath, e)) from e
+        try:
+            spec = importlib.util.spec_from_file_location(_mangle(self._name), pycpath)
             mod =  importlib.util.module_from_spec(spec)
             spec.loader.exec_module(mod)
         except Exception as e:
@@ -402,19 +409,20 @@
         self._class = None
         for i in dir(mod):
             v = getattr(mod, i)
-            if issubclass(v, Page):
+            if isclass(v) and issubclass(v, Page):
                 if self._class is not None:
-                    raise TinCanError("{0}: contains multiple Page classes", pypath)
+                    raise TinCanError("{0}: contains multiple Page classes".format(pypath))
                 self._class = v
         if self._class is None:
-            raise TinCanError("{0}: contains no Page classes", pypath)
+            raise TinCanError("{0}: contains no Page classes".format(pypath))
 
     def _redirect(self):
-        if self._header.forward in self._seen:
-            raise TinCanError("{0}: #forward loop".format(self._origin))
-        self._seen.add(self._header.forward)
         try:
             rlist = self._splitpath(self._header.forward)
+            forw = '/' + '/'.join(rlist)
+            if forw in self.seen:
+                raise TinCanError("{0}: #forward loop".format(self._origin))
+            self._seen.add(forw)
             rname = rlist.pop()
         except IndexError as e:
             raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e
@@ -444,7 +452,7 @@
         except ForwardException as fwd:
             target = fwd.target
         if target is None:
-            raise TinCanError("Unexpected null target!")
+            raise TinCanError("{0}: unexpected null target".format(self._urlpath))
         # We get here if we are doing a server-side programmatic
         # forward.
         environ = bottle.request.environ
@@ -453,7 +461,7 @@
         if _FLOOP not in environ:
             environ[_FLOOP] = set([self._urlpath])
         elif target in environ[_FLOOP]:
-            TinCanError("{0}: forward loop detected".format(environ[_FORIG]))
+            raise TinCanError("{0}: forward loop detected".format(environ[_FORIG]))
         environ[_FLOOP].add(target)
         environ['bottle.raw_path'] = target
         environ['PATH_INFO'] = urllib.parse.quote(target)
@@ -481,14 +489,17 @@
     """
     Helper class for launching webapps.
     """
-    def __init__(self, fsroot, urlroot, tclass):
+    def __init__(self, fsroot, urlroot, tclass, logger):
         """
         Lightweight constructor. The real action happens in .launch() below.
         """
         self.fsroot = fsroot
         self.urlroot = urlroot
         self.tclass = tclass
+        self.logger = logger
         self.app = None
+        self.errors = 0
+        self.debug = False
 
     def launch(self):
         """
@@ -511,7 +522,7 @@
         # Do what we gotta do
         self.app = TinCan()
         self._launch([])
-        return self.app
+        return self
 
     def _launch(self, subdir):
         for entry in os.listdir(os.path.join(self.fsroot, *subdir)):
@@ -522,16 +533,34 @@
                 ename, eext = os.path.splitext(entry)
                 if eext != _TEXTEN:
                     continue  # only look at interesting files
-                route = TinCanRoute(self, ename, subdir)
-                page.launch()
+                route = _TinCanRoute(self, ename, subdir)
+                try:
+                    route.launch()
+                except TinCanError as e:
+                    self.logger(str(e))
+                    if self.debug:
+                        while e.__cause__ != None:
+                            e = e.__cause__
+                            self.logger("\t{0}: {1!s}".format(e.__class__.__name__, e))
+                    self.errors += 1
             elif S_ISDIR(etype):
                 self._launch(subdir + [entry])
 
-def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate):
+def _logger(message):
+    sys.stderr.write(message)
+    sys.stderr.write('\n')
+
+def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger):
     """
     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()
+    launcher = _Launcher(fsroot, urlroot, tclass, logger)
+    # launcher.debug = True
+    launcher.launch()
+    return launcher.app, launcher.errors
+
+# XXX - We cannot implement a command-line launcher here; see the
+# launcher script for why.