# HG changeset patch # User David Barts # Date 1557805671 25200 # Node ID 75e375b1976a2cf2b84a3264a3381ca5d66c5288 # Parent 9aaa91247b141103ff2bb617752f86eea599cbf9 Error pages now can have code-behind. diff -r 9aaa91247b14 -r 75e375b1976a pspx.html --- 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 @@ .pspx Template Files

.pspx Template Files

Introduction

- Files ending in a .pspx extension define both - routes and templates. The files have two parts: a header and a body.
+ Files ending in a .pspx extension define both routes and + templates. The files have two parts: a header and a body.

The Header

The header is optional and consists of lines starting with an octothorpe (#) character. With the exception of the #rem header, all @@ -19,20 +19,19 @@

#end
Marks the last line of the headers. Needed only for templating - languages where lines often start with #, such - as Cheetah.
+ languages where lines often start with #, such as Cheetah.
#errors
-
Ignore other headers and make this is an error page which handles the - specified HTTP error codes. See the subsection on error pages below.
+
This is an error page which handles the specified HTTP error codes. + See the subsection on error pages below.
#forward
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 #forward may itself contain a #forward, but attempts to create a #forward loop are not allowed and - will cause a TinCanError to be raised. Note - that unlike calls to app.forward(), the #forward - header is resolved at route-creation time; no extra processing will - happen at request time.
+ will cause a TinCanError to be raised. Note that unlike + calls to app.forward(), the #forward header + is resolved at route-creation time; no extra processing will happen at + request time.
#hidden
This is a hidden page; do not create a route for it. The page can only be displayed by a forward.
@@ -43,13 +42,13 @@
#python
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 .py.
+ the same name but an extension of .py.
#rem
The rest of the line is treated as a remark (comment) and is ignored.
#template
Ignore the body of this file and instead use the template in the body - of the specified file, which must end in .pspx. - Any headers in the referred template file are ignored.
+ of the specified file, which must end in .pspx. Any headers + in the referred template file are ignored.

It is possible to include whitespace and special characters in arguments to the #forward, #python, and #template @@ -57,22 +56,22 @@ example, #python "space case.py".

Header directives that don't take arguments as a rule simply ignore them. For example, #end headers has the same effect as #end. +

Error Pages

Error pages supersede the standard Bottle error handling, and are created - by using the #errors page header. Error pages have no - associated code-behind; they consist of templates only. Error page - templates are provided with two variables when rendering:

-
-
error
-
The bottle.HTTPError object associated with this error.
-
request
-
The bottle.Request object associated with this error.
-
+ by using the #errors page header. The #hidden + and #method header directives are ignored in error pages + (error pages are effectively hidden anyhow, by virtue of never having + normal routes created for them).

The #errors 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.

+

Templates with No Code-Behind

+

Code-behind is optional for both normal and error page templates. If + code-behind is not provided, TinCan will use the Page or ErrorPage + class as appropriate.

The Body

The body begins with the first line that does not start with # 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.

+

Default Template Variables

+

By default, regular pages will have a single template variable available: + page, which refers to the Page class that contains + the code-behind logic for this page. The pertinent bottle.Request + and bottle.Response objects are available to normal, non-error + pages as page.request and page.response, + respectively.

+

Error pages have their code-behind logic in an ErrorPage + class. In addition to the standard page variable, error pages + also have request and error variables available by + default, which contain copies of the pertinent bottle.Request + and bottle.HTTPError objects respectively.

+

The default behavior of the export method (which exports + instance variables to template variables) in both the Page and + ErrorPage 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.

+

Note that if for some reason you create an instance variable named page, + it will overwrite the standard template variable by the same name.

+

diff -r 9aaa91247b14 -r 75e375b1976a tincan.py --- 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: