diff tincan.py @ 45:969f515b505b draft

Make it easy to leave stdout and stderr alone (untested).
author David Barts <n5jrn@me.com>
date Thu, 30 May 2019 14:44:09 -0700
parents 7261459351fa
children 997d0c8c174f
line wrap: on
line diff
--- a/tincan.py	Thu May 30 11:23:26 2019 -0700
+++ b/tincan.py	Thu May 30 14:44:09 2019 -0700
@@ -13,14 +13,15 @@
 import importlib
 from inspect import isclass
 import io
+import logging
 import mimetypes
 import py_compile
 from stat import S_ISDIR, S_ISREG
 from string import whitespace
 from threading import Lock
 import time
-import traceback
 import urllib
+import uuid
 
 import bottle
 
@@ -398,6 +399,7 @@
         else:
             self.lock = _DummyLock()
             self.get_template = _get_template_cache
+        self.logger = launcher.logger
 
     def urljoin(self, *args):
         """
@@ -437,7 +439,7 @@
             self._encoding = None
 
     def launch(self):
-        # print("adding static route:", self._urlpath)  # debug
+        self.logger.info("adding static route: %s", self._urlpath)
         self._app.route(self._urlpath, 'GET', self)
 
     def _parse_date(self, ims):
@@ -462,6 +464,7 @@
         except PermissionError as e:
             return bottle.HTTPError(status=403, exception=e)
         except OSError as e:
+            self.logger.exception("unexpected exception reading %r", self._fspath)
             return bottle.HTTPError(status=500, exception=e)
         # Establish preliminary standard headers.
         headers = {
@@ -488,12 +491,13 @@
     custom code-behind, only two variables are available to your template:
     request (bottle.Request) and error (bottle.HTTPError).
     """
-    def __init__(self, template, loads, klass, lock):
+    def __init__(self, template, loads, klass, lock, logger):
         self._template = template
         self._template.prepare()
         self._loads = loads
         self._class = klass
         self.lock = lock
+        self.logger = logger
 
     def __call__(self, e):
         bottle.request.environ[_FTYPE] = True
@@ -507,7 +511,7 @@
         except bottle.HTTPResponse as e:
             return e
         except Exception as e:
-            traceback.print_exc()
+            self.logger.exception("unexpected exception in error page")
             # Bottle doesn't allow error handlers to themselves cause
             # errors, most likely as a measure to prevent looping. So
             # this will cause a "Critical error while processing request"
@@ -558,9 +562,9 @@
                 raise TinCanError("{0}: invalid #forward".format(self._origin))
             if self._header.forward is None:
                 break
-            # print("forwarding from:", self._urlpath)  # debug
+            # self.logger.debug("forwarding from: %s", self._urlpath)  # debug
             self._redirect()
-            # print("forwarded to:", self._urlpath)  # debug
+            # self.logger.debug("forwarded to: %s", self._urlpath)  # debug
         # If this is a #hidden page, we ignore it for now, since hidden pages
         # don't get routes made for them.
         if oheader.hidden and not oheader.errors:
@@ -619,7 +623,7 @@
             if not methods:
                 raise TinCanError("{0}: no #methods specified".format(self._urlpath))
         # Register this thing with Bottle
-        # print("adding route:", self._origin, '('+','.join(methods)+')')  # debug
+        self.logger.info("adding route: %s (%s)", self._origin, ','.join(methods))
         self._app.route(self._origin, methods, self)
 
     def _splitpath(self, unsplit):
@@ -634,7 +638,7 @@
             errors = range(_ERRMIN, _ERRMAX+1)
         route = _TinCanErrorRoute(
             ChameleonTemplate(source=self._template.body, encoding=self._encoding),
-            self._loads, self._class, self.lock)
+            self._loads, self._class, self.lock, self.logger)
         for error in errors:
             if error < _ERRMIN or error > _ERRMAX:
                 raise TinCanError("{0}: bad #errors code".format(self._urlpath))
@@ -750,11 +754,10 @@
         except bottle.HTTPResponse as e:
             return e
         except Exception as e:
-            traceback.print_exc()
+            self.logger.exception("%s: unexpected exception", self._urlpath)
             raise bottle.HTTPError(status=500, exception=e)
         if target is None:
-            message = "{0}: unexpected null target".format(self._urlpath)
-            sys.stderr.write(message + '\n')
+            self.logger.error("%s: unexpected null target", self._urlpath)
             raise bottle.HTTPError(status=500, exception=TinCanError(message))
         # We get here if we are doing a server-side programmatic
         # forward.
@@ -764,8 +767,7 @@
         if _FLOOP not in environ:
             environ[_FLOOP] = set([self._urlpath])
         elif target in environ[_FLOOP]:
-            message = "{0}: forward loop detected".format(environ[_FORIG])
-            sys.stderr.write(message + '\n')
+            self.logger.error("%s: forward loop detected", environ[_FORIG])
             raise bottle.HTTPError(status=500, exception=TinCanError(message))
         environ[_FLOOP].add(target)
         environ['bottle.raw_path'] = target
@@ -805,19 +807,19 @@
     """
     Helper class for launching webapps.
     """
-    def __init__(self, fsroot, urlroot, logger, multithread=True):
+    def __init__(self, fsroot, urlroot, multithread):
         """
         Lightweight constructor. The real action happens in .launch() below.
         """
         self.fsroot = fsroot
         self.urlroot = urlroot
-        self.logger = logger
         self.app = None
         self.errors = 0
         self.debug = False
         self.encoding = ENCODING
         self.static = False
         self.multithread = multithread
+        self.logger = None
 
     def launch(self):
         """
@@ -873,29 +875,28 @@
                 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))
+                    if self.logger.getEffectiveLevel() <= logging.DEBUG:
+                        self.logger.exception(str(e))
+                    else:
+                        self.logger.error(str(e))
                     self.errors += 1
             elif S_ISDIR(etype):
                 self._launch(subdir + [entry])
 
-def _logger(message):
-    sys.stderr.write(message)
-    sys.stderr.write('\n')
-
-def launch(fsroot=None, urlroot='/', logger=_logger, debug=False,
+def launch(fsroot=None, urlroot='/', logger=None, debug=False,
     encoding=ENCODING, static=False, multithread=True):
     """
     Launch and return a TinCan webapp. Does not run the app; it is the
     caller's responsibility to call app.run()
     """
+    if logger is None:
+        logger = logging.getLogger("{0!s}-{1!s}".format(__name__, uuid.uuid1()))
+        logger.addHandler(logging.StreamHandler())
+    logger.setLevel(logging.DEBUG if debug else logging.INFO)
     if fsroot is None:
         fsroot = os.getcwd()
-    launcher = _Launcher(fsroot, urlroot, logger, multithread=multithread)
-    launcher.debug = debug
+    launcher = _Launcher(fsroot, urlroot, multithread)
+    launcher.logger = logger
     launcher.encoding = encoding
     launcher.static = static
     launcher.launch()