Mercurial > cgi-bin > hgweb.cgi > tincan
changeset 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 | 84998cd4e123 |
files | pspx.html tincan.py |
diffstat | 2 files changed, 115 insertions(+), 71 deletions(-) [+] |
line wrap: on
line diff
--- a/pspx.html Mon May 13 16:27:05 2019 -0700 +++ b/pspx.html Mon May 13 20:47:51 2019 -0700 @@ -4,14 +4,14 @@ <meta http-equiv="content-type" content="text/html; charset=UTF-8"> <title>.pspx Template Files</title> <style type="text/css"> - .kbd { font-family: monospace; } + var { font-family: monospace; font-style: normal; } </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> + Files ending in a <var>.pspx</var> 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 @@ -19,20 +19,19 @@ <dl> <dt><code>#end</code></dt> <dd>Marks the last line of the headers. Needed only for templating - languages where lines often start with <span class="kbd">#</span>, such - as Cheetah.</dd> + languages where lines often start with <var>#</var>, such as Cheetah.</dd> <dt><code>#errors</code></dt> - <dd>Ignore other headers and make this is an error page which handles the - specified HTTP error codes. See the subsection on error pages below. </dd> + <dd>This is an error page which handles the specified HTTP error codes. + See the subsection on error pages below. </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> + will cause a <var>TinCanError</var> 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> @@ -43,13 +42,13 @@ <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> + the same name but an extension of <var>.py</var>.</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>. - Any headers in the referred template file are ignored.</dd> + of the specified file, which must end in <var>.pspx</var>. Any headers + in the referred template file are ignored.</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> @@ -57,22 +56,22 @@ example, <code>#python "space case.py"</code>.</p> <p>Header directives that don't take arguments as a rule simply ignore them. For example, <code>#end headers</code> has the same effect as <code>#end</code>. + </p> <h3>Error Pages</h3> <p>Error pages supersede the standard Bottle error handling, and are created - by using the <code>#errors</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>error</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> + by using the <code>#errors</code> page header. The <code>#hidden</code> + and <code>#method</code> header directives are ignored in error pages + (error pages are effectively hidden anyhow, by virtue of never having + normal routes created for them).</p> <p>The <code>#errors</code> directive takes a list of numeric error codes (values from 400 to 599 are allowed); the page is created to handle the specified errors. If no error codes are specified, the page will handle all errors. The behavior of specifying multiple error pages for the same error code is undefined; doing so is best avoided.</p> + <h3>Templates with No Code-Behind</h3> + <p>Code-behind is optional for both normal and error page templates. If + code-behind is not provided, TinCan will use the <var>Page</var> or <var>ErrorPage</var> + class as appropriate. </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. @@ -84,5 +83,26 @@ 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> + <h3>Default Template Variables</h3> + <p>By default, regular pages will have a single template variable available: + <var>page</var>, which refers to the <var>Page</var> class that contains + the code-behind logic for this page. The pertinent <var>bottle.Request</var> + and <var>bottle.Response</var> objects are available to normal, non-error + pages as <var>page.request</var> and <var>page.response</var>, + respectively.</p> + <p>Error pages have their code-behind logic in an <var>ErrorPage</var> + class. In addition to the standard <var>page</var> variable, error pages + also have <var>request</var> and <var>error</var> variables available by + default, which contain copies of the pertinent <var>bottle.Request</var> + and <var>bottle.HTTPError</var> objects respectively.</p> + <p>The default behavior of the <var>export</var> method (which exports + instance variables to template variables) in both the <var>Page</var> and + <var>ErrorPage</var> classes is to export, in addition to the default + variables, every user-created instance attribute that is not callable and + whose name does not start with an underscore. This behavior can be changed + if desired by overriding that method.</p> + <p>Note that if for some reason you create an instance variable named <var>page</var>, + it will overwrite the standard template variable by the same name.</p> + <p></p> </body> </html>
--- 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: