Mercurial > cgi-bin > hgweb.cgi > tincan
diff tincan.py @ 9:75e375b1976a draft
Error pages now can have code-behind.
author | David Barts <n5jrn@me.com> |
---|---|
date | Mon, 13 May 2019 20:47:51 -0700 |
parents | 9aaa91247b14 |
children | 8037bad7d5a8 |
line wrap: on
line diff
--- a/tincan.py Mon May 13 16:27:05 2019 -0700 +++ b/tincan.py Mon May 13 20:47:51 2019 -0700 @@ -26,7 +26,7 @@ """ pass -class TemplateHeaderException(TinCanException): +class TemplateHeaderError(TinCanException): """ Raised upon encountering a syntax error in the template headers. """ @@ -132,7 +132,7 @@ # Get line count += 1 if not line.startswith("#"): - raise TemplateHeaderException("Does not start with '#'.", count) + raise TemplateHeaderError("Does not start with '#'.", count) try: rna, rpa = line.split(maxsplit=1) except ValueError: @@ -145,9 +145,9 @@ if name == "end": break if name not in nameset: - raise TemplateHeaderException("Invalid directive: {0!r}".format(rna), count) + raise TemplateHeaderError("Invalid directive: {0!r}".format(rna), count) if name in seen: - raise TemplateHeaderException("Duplicate {0!r} directive.".format(rna), count) + raise TemplateHeaderError("Duplicate {0!r} directive.".format(rna), count) seen.add(name) # Flags if name in self._FNAMES: @@ -155,7 +155,7 @@ continue # Get parameter if rpa is None: - raise TemplateHeaderException("Missing parameter.", count) + raise TemplateHeaderError("Missing parameter.", count) param = rpa.strip() for i in [ "'", '"']: if param.startswith(i) and param.endswith(i): @@ -235,6 +235,8 @@ """ source = bottle.request.environ['PATH_INFO'] base = source.strip('/').split('/')[:-1] + if bottle.request.environ.get(_FTYPE, False): + raise TinCanError("{0}: forward from error page".format(source)) try: exc = ForwardException('/' + '/'.join(_normpath(base, target))) except IndexError as e: @@ -246,17 +248,10 @@ # Represents the code-behind of one of our pages. This gets subclassed, of # course. -class Page(object): - # Non-private things we refuse to export anyhow. - __HIDDEN = set([ "request", "response" ]) - - def __init__(self, req, resp): - """ - Constructor. This is a lightweight operation. - """ - self.request = req # app context is request.app in Bottle - self.response = resp - +class BasePage(object): + """ + The parent class of both error and normal pages. + """ def handle(self): """ This is the entry point for the code-behind logic. It is intended @@ -267,14 +262,13 @@ def export(self): """ Export template variables. The default behavior is to export all - non-hidden non-callables that don't start with an underscore, - plus a an export named page that contains this object itself. + non-hidden non-callables that don't start with an underscore. This method can be overridden if a different behavior is desired. It should always return a dict or dict-like object. """ - ret = { "page": self } # feature: will be clobbered if self.page exists + ret = { 'page': self } for name in dir(self): - if name in self.__HIDDEN or name.startswith('_'): + if name in self._HIDDEN or name.startswith('_'): continue value = getattr(self, name) if callable(value): @@ -282,6 +276,30 @@ ret[name] = value return ret +class Page(BasePage): + # Non-private things we refuse to export anyhow. + _HIDDEN = set([ "request", "response" ]) + + def __init__(self, req, resp): + """ + Constructor. This is a lightweight operation. + """ + self.request = req # app context is request.app in Bottle + self.response = resp + +class ErrorPage(BasePage): + """ + The code-behind for an error page. + """ + _HIDDEN = set() + + def __init__(self, req, err): + """ + Constructor. This is a lightweight operation. + """ + self.request = req + self.error = err + # R o u t e s # # Represents a route in TinCan. Our launcher creates these on-the-fly based @@ -293,20 +311,24 @@ _TEXTEN = ".pspx" _FLOOP = "tincan.forwards" _FORIG = "tincan.origin" +_FTYPE = "tincan.iserror" 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 - there. Error templates only have two variables available: e (the - HTTPError object associated with the error) and request. + A route to an error page. These don't get routes created for them, + and are only reached if an error routes them there. Unless you create + custom code-behind, only two variables are available to your template: + request (bottle.Request) and error (bottle.HTTPError). """ - def __init__(self, template): + def __init__(self, template, klass): self._template = template self._template.prepare() + self._class = klass def __call__(self, e): - return self._template.render(error=e, request=bottle.request).lstrip('\n') + obj = self._class(bottle.request, e) + obj.handle() + return self._template.render(obj.export()).lstrip('\n') class _TinCanRoute(object): """ @@ -330,16 +352,17 @@ Launch a single page. """ # Build master and header objects, process #forward directives - hidden = None + hidden = oerrors = None while True: + if oerrors is not None and oerrors != self._header.errors: + raise TinCanError("{0}: invalid redirect") self._template = TemplateFile(self._fspath) try: self._header = TemplateHeader(self._template.header) - except TemplateHeaderException as e: + except TemplateHeaderError as e: raise TinCanError("{0}: {1!s}".format(self._fspath, e)) from e + oerrors = self._header.errors if hidden is None: - if self._header.errors is not None: - break hidden = self._header.hidden elif self._header.errors is not None: raise TinCanError("{0}: #forward to #errors not allowed".format(self._origin)) @@ -348,19 +371,8 @@ self._redirect() # If this is a #hidden page, we ignore it for now, since hidden pages # don't get routes made for them. - if hidden: + if hidden and not self._headers.errors: return - # 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 - 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)) # Get the code-behind #python if self._header.python is not None: if not self._header.python.endswith(_PEXTEN): @@ -378,6 +390,17 @@ else: self._body = self._tclass(source=self._template.body) self._body.prepare() + # 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 + 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)) # Register this thing with Bottle print("adding route:", self._origin, '('+','.join(methods)+')') # debug self._app.route(self._origin, methods, self) @@ -392,7 +415,7 @@ 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), self._class) for error in errors: if error < _ERRMIN or error > _ERRMAX: raise TinCanError("{0}: bad #errors code".format(self._urlpath)) @@ -404,14 +427,15 @@ except FileNotFoundError: return 0 except OSError as e: - raise TinCanError("{0}: {1}".format(path, e.strerror)) from e + raise TinCanError(str(e)) from e def _getclass(self): pypath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._python))) + klass = ErrorPage if self._header.errors else Page # Give 'em a default code-behind if they don't furnish one pytime = self._gettime(pypath) if not pytime: - self._class = Page + self._class = klass return # Else load the code-behind from a .py file pycpath = pypath + 'c' @@ -432,12 +456,12 @@ self._class = None for i in dir(mod): v = getattr(mod, i) - if isclass(v) and issubclass(v, Page): + if isclass(v) and issubclass(v, klass): if self._class is not None: - raise TinCanError("{0}: contains multiple Page classes".format(pypath)) + raise TinCanError("{0}: contains multiple {1} classes".format(pypath, klass.__name__)) self._class = v if self._class is None: - raise TinCanError("{0}: contains no Page classes".format(pypath)) + raise TinCanError("{0}: contains no {1} classes".format(pypath, klass.__name__)) def _redirect(self): try: