Mercurial > cgi-bin > hgweb.cgi > tincan
comparison 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 |
comparison
equal
deleted
inserted
replaced
8:9aaa91247b14 | 9:75e375b1976a |
---|---|
24 """ | 24 """ |
25 The parent class of all exceptions we raise. | 25 The parent class of all exceptions we raise. |
26 """ | 26 """ |
27 pass | 27 pass |
28 | 28 |
29 class TemplateHeaderException(TinCanException): | 29 class TemplateHeaderError(TinCanException): |
30 """ | 30 """ |
31 Raised upon encountering a syntax error in the template headers. | 31 Raised upon encountering a syntax error in the template headers. |
32 """ | 32 """ |
33 def __init__(self, message, line): | 33 def __init__(self, message, line): |
34 super().__init__(message, line) | 34 super().__init__(message, line) |
130 del lines[-1] | 130 del lines[-1] |
131 for line in lines: | 131 for line in lines: |
132 # Get line | 132 # Get line |
133 count += 1 | 133 count += 1 |
134 if not line.startswith("#"): | 134 if not line.startswith("#"): |
135 raise TemplateHeaderException("Does not start with '#'.", count) | 135 raise TemplateHeaderError("Does not start with '#'.", count) |
136 try: | 136 try: |
137 rna, rpa = line.split(maxsplit=1) | 137 rna, rpa = line.split(maxsplit=1) |
138 except ValueError: | 138 except ValueError: |
139 rna = line.rstrip() | 139 rna = line.rstrip() |
140 rpa = None | 140 rpa = None |
143 if name == "rem": | 143 if name == "rem": |
144 continue | 144 continue |
145 if name == "end": | 145 if name == "end": |
146 break | 146 break |
147 if name not in nameset: | 147 if name not in nameset: |
148 raise TemplateHeaderException("Invalid directive: {0!r}".format(rna), count) | 148 raise TemplateHeaderError("Invalid directive: {0!r}".format(rna), count) |
149 if name in seen: | 149 if name in seen: |
150 raise TemplateHeaderException("Duplicate {0!r} directive.".format(rna), count) | 150 raise TemplateHeaderError("Duplicate {0!r} directive.".format(rna), count) |
151 seen.add(name) | 151 seen.add(name) |
152 # Flags | 152 # Flags |
153 if name in self._FNAMES: | 153 if name in self._FNAMES: |
154 setattr(self, name, True) | 154 setattr(self, name, True) |
155 continue | 155 continue |
156 # Get parameter | 156 # Get parameter |
157 if rpa is None: | 157 if rpa is None: |
158 raise TemplateHeaderException("Missing parameter.", count) | 158 raise TemplateHeaderError("Missing parameter.", count) |
159 param = rpa.strip() | 159 param = rpa.strip() |
160 for i in [ "'", '"']: | 160 for i in [ "'", '"']: |
161 if param.startswith(i) and param.endswith(i): | 161 if param.startswith(i) and param.endswith(i): |
162 param = ast.literal_eval(param) | 162 param = ast.literal_eval(param) |
163 break | 163 break |
233 """ | 233 """ |
234 Forward this request to the specified target route. | 234 Forward this request to the specified target route. |
235 """ | 235 """ |
236 source = bottle.request.environ['PATH_INFO'] | 236 source = bottle.request.environ['PATH_INFO'] |
237 base = source.strip('/').split('/')[:-1] | 237 base = source.strip('/').split('/')[:-1] |
238 if bottle.request.environ.get(_FTYPE, False): | |
239 raise TinCanError("{0}: forward from error page".format(source)) | |
238 try: | 240 try: |
239 exc = ForwardException('/' + '/'.join(_normpath(base, target))) | 241 exc = ForwardException('/' + '/'.join(_normpath(base, target))) |
240 except IndexError as e: | 242 except IndexError as e: |
241 raise TinCanError("{0}: invalid forward to {1!r}".format(source, target)) from e | 243 raise TinCanError("{0}: invalid forward to {1!r}".format(source, target)) from e |
242 raise exc | 244 raise exc |
244 # C o d e B e h i n d | 246 # C o d e B e h i n d |
245 # | 247 # |
246 # Represents the code-behind of one of our pages. This gets subclassed, of | 248 # Represents the code-behind of one of our pages. This gets subclassed, of |
247 # course. | 249 # course. |
248 | 250 |
249 class Page(object): | 251 class BasePage(object): |
250 # Non-private things we refuse to export anyhow. | 252 """ |
251 __HIDDEN = set([ "request", "response" ]) | 253 The parent class of both error and normal pages. |
252 | 254 """ |
253 def __init__(self, req, resp): | |
254 """ | |
255 Constructor. This is a lightweight operation. | |
256 """ | |
257 self.request = req # app context is request.app in Bottle | |
258 self.response = resp | |
259 | |
260 def handle(self): | 255 def handle(self): |
261 """ | 256 """ |
262 This is the entry point for the code-behind logic. It is intended | 257 This is the entry point for the code-behind logic. It is intended |
263 to be overridden. | 258 to be overridden. |
264 """ | 259 """ |
265 pass | 260 pass |
266 | 261 |
267 def export(self): | 262 def export(self): |
268 """ | 263 """ |
269 Export template variables. The default behavior is to export all | 264 Export template variables. The default behavior is to export all |
270 non-hidden non-callables that don't start with an underscore, | 265 non-hidden non-callables that don't start with an underscore. |
271 plus a an export named page that contains this object itself. | |
272 This method can be overridden if a different behavior is | 266 This method can be overridden if a different behavior is |
273 desired. It should always return a dict or dict-like object. | 267 desired. It should always return a dict or dict-like object. |
274 """ | 268 """ |
275 ret = { "page": self } # feature: will be clobbered if self.page exists | 269 ret = { 'page': self } |
276 for name in dir(self): | 270 for name in dir(self): |
277 if name in self.__HIDDEN or name.startswith('_'): | 271 if name in self._HIDDEN or name.startswith('_'): |
278 continue | 272 continue |
279 value = getattr(self, name) | 273 value = getattr(self, name) |
280 if callable(value): | 274 if callable(value): |
281 continue | 275 continue |
282 ret[name] = value | 276 ret[name] = value |
283 return ret | 277 return ret |
284 | 278 |
279 class Page(BasePage): | |
280 # Non-private things we refuse to export anyhow. | |
281 _HIDDEN = set([ "request", "response" ]) | |
282 | |
283 def __init__(self, req, resp): | |
284 """ | |
285 Constructor. This is a lightweight operation. | |
286 """ | |
287 self.request = req # app context is request.app in Bottle | |
288 self.response = resp | |
289 | |
290 class ErrorPage(BasePage): | |
291 """ | |
292 The code-behind for an error page. | |
293 """ | |
294 _HIDDEN = set() | |
295 | |
296 def __init__(self, req, err): | |
297 """ | |
298 Constructor. This is a lightweight operation. | |
299 """ | |
300 self.request = req | |
301 self.error = err | |
302 | |
285 # R o u t e s | 303 # R o u t e s |
286 # | 304 # |
287 # Represents a route in TinCan. Our launcher creates these on-the-fly based | 305 # Represents a route in TinCan. Our launcher creates these on-the-fly based |
288 # on the files it finds. | 306 # on the files it finds. |
289 | 307 |
291 _ERRMAX = 599 | 309 _ERRMAX = 599 |
292 _PEXTEN = ".py" | 310 _PEXTEN = ".py" |
293 _TEXTEN = ".pspx" | 311 _TEXTEN = ".pspx" |
294 _FLOOP = "tincan.forwards" | 312 _FLOOP = "tincan.forwards" |
295 _FORIG = "tincan.origin" | 313 _FORIG = "tincan.origin" |
314 _FTYPE = "tincan.iserror" | |
296 | 315 |
297 class _TinCanErrorRoute(object): | 316 class _TinCanErrorRoute(object): |
298 """ | 317 """ |
299 A route to an error page. These never have code-behind, don't get | 318 A route to an error page. These don't get routes created for them, |
300 routes created for them, and are only reached if an error routes them | 319 and are only reached if an error routes them there. Unless you create |
301 there. Error templates only have two variables available: e (the | 320 custom code-behind, only two variables are available to your template: |
302 HTTPError object associated with the error) and request. | 321 request (bottle.Request) and error (bottle.HTTPError). |
303 """ | 322 """ |
304 def __init__(self, template): | 323 def __init__(self, template, klass): |
305 self._template = template | 324 self._template = template |
306 self._template.prepare() | 325 self._template.prepare() |
326 self._class = klass | |
307 | 327 |
308 def __call__(self, e): | 328 def __call__(self, e): |
309 return self._template.render(error=e, request=bottle.request).lstrip('\n') | 329 obj = self._class(bottle.request, e) |
330 obj.handle() | |
331 return self._template.render(obj.export()).lstrip('\n') | |
310 | 332 |
311 class _TinCanRoute(object): | 333 class _TinCanRoute(object): |
312 """ | 334 """ |
313 A route created by the TinCan launcher. | 335 A route created by the TinCan launcher. |
314 """ | 336 """ |
328 def launch(self): | 350 def launch(self): |
329 """ | 351 """ |
330 Launch a single page. | 352 Launch a single page. |
331 """ | 353 """ |
332 # Build master and header objects, process #forward directives | 354 # Build master and header objects, process #forward directives |
333 hidden = None | 355 hidden = oerrors = None |
334 while True: | 356 while True: |
357 if oerrors is not None and oerrors != self._header.errors: | |
358 raise TinCanError("{0}: invalid redirect") | |
335 self._template = TemplateFile(self._fspath) | 359 self._template = TemplateFile(self._fspath) |
336 try: | 360 try: |
337 self._header = TemplateHeader(self._template.header) | 361 self._header = TemplateHeader(self._template.header) |
338 except TemplateHeaderException as e: | 362 except TemplateHeaderError as e: |
339 raise TinCanError("{0}: {1!s}".format(self._fspath, e)) from e | 363 raise TinCanError("{0}: {1!s}".format(self._fspath, e)) from e |
364 oerrors = self._header.errors | |
340 if hidden is None: | 365 if hidden is None: |
341 if self._header.errors is not None: | |
342 break | |
343 hidden = self._header.hidden | 366 hidden = self._header.hidden |
344 elif self._header.errors is not None: | 367 elif self._header.errors is not None: |
345 raise TinCanError("{0}: #forward to #errors not allowed".format(self._origin)) | 368 raise TinCanError("{0}: #forward to #errors not allowed".format(self._origin)) |
346 if self._header.forward is None: | 369 if self._header.forward is None: |
347 break | 370 break |
348 self._redirect() | 371 self._redirect() |
349 # If this is a #hidden page, we ignore it for now, since hidden pages | 372 # If this is a #hidden page, we ignore it for now, since hidden pages |
350 # don't get routes made for them. | 373 # don't get routes made for them. |
351 if hidden: | 374 if hidden and not self._headers.errors: |
352 return | 375 return |
353 # If this is an #errors page, register it as such. | |
354 if self._header.errors is not None: | |
355 self._mkerror() | |
356 return # this implies #hidden | |
357 # Get #methods for this route | |
358 if self._header.methods is None: | |
359 methods = [ 'GET' ] | |
360 else: | |
361 methods = [ i.upper() for i in self._header.methods.split() ] | |
362 if not methods: | |
363 raise TinCanError("{0}: no #methods specified".format(self._urlpath)) | |
364 # Get the code-behind #python | 376 # Get the code-behind #python |
365 if self._header.python is not None: | 377 if self._header.python is not None: |
366 if not self._header.python.endswith(_PEXTEN): | 378 if not self._header.python.endswith(_PEXTEN): |
367 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) | 379 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) |
368 self._python = self._header.python | 380 self._python = self._header.python |
376 tfile = TemplateFile(tpath) | 388 tfile = TemplateFile(tpath) |
377 self._body = self._tclass(source=tfile.body) | 389 self._body = self._tclass(source=tfile.body) |
378 else: | 390 else: |
379 self._body = self._tclass(source=self._template.body) | 391 self._body = self._tclass(source=self._template.body) |
380 self._body.prepare() | 392 self._body.prepare() |
393 # If this is an #errors page, register it as such. | |
394 if self._header.errors is not None: | |
395 self._mkerror() | |
396 return # this implies #hidden | |
397 # Get #methods for this route | |
398 if self._header.methods is None: | |
399 methods = [ 'GET' ] | |
400 else: | |
401 methods = [ i.upper() for i in self._header.methods.split() ] | |
402 if not methods: | |
403 raise TinCanError("{0}: no #methods specified".format(self._urlpath)) | |
381 # Register this thing with Bottle | 404 # Register this thing with Bottle |
382 print("adding route:", self._origin, '('+','.join(methods)+')') # debug | 405 print("adding route:", self._origin, '('+','.join(methods)+')') # debug |
383 self._app.route(self._origin, methods, self) | 406 self._app.route(self._origin, methods, self) |
384 | 407 |
385 def _splitpath(self, unsplit): | 408 def _splitpath(self, unsplit): |
390 errors = [ int(i) for i in self._header.errors.split() ] | 413 errors = [ int(i) for i in self._header.errors.split() ] |
391 except ValueError as e: | 414 except ValueError as e: |
392 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e | 415 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e |
393 if not errors: | 416 if not errors: |
394 errors = range(_ERRMIN, _ERRMAX+1) | 417 errors = range(_ERRMIN, _ERRMAX+1) |
395 route = _TinCanErrorRoute(self._tclass(source=self._template.body)) | 418 route = _TinCanErrorRoute(self._tclass(source=self._template.body), self._class) |
396 for error in errors: | 419 for error in errors: |
397 if error < _ERRMIN or error > _ERRMAX: | 420 if error < _ERRMIN or error > _ERRMAX: |
398 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) | 421 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) |
399 self._app.error_handler[error] = route # XXX | 422 self._app.error_handler[error] = route # XXX |
400 | 423 |
402 try: | 425 try: |
403 return os.stat(path).st_mtime | 426 return os.stat(path).st_mtime |
404 except FileNotFoundError: | 427 except FileNotFoundError: |
405 return 0 | 428 return 0 |
406 except OSError as e: | 429 except OSError as e: |
407 raise TinCanError("{0}: {1}".format(path, e.strerror)) from e | 430 raise TinCanError(str(e)) from e |
408 | 431 |
409 def _getclass(self): | 432 def _getclass(self): |
410 pypath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._python))) | 433 pypath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._python))) |
434 klass = ErrorPage if self._header.errors else Page | |
411 # Give 'em a default code-behind if they don't furnish one | 435 # Give 'em a default code-behind if they don't furnish one |
412 pytime = self._gettime(pypath) | 436 pytime = self._gettime(pypath) |
413 if not pytime: | 437 if not pytime: |
414 self._class = Page | 438 self._class = klass |
415 return | 439 return |
416 # Else load the code-behind from a .py file | 440 # Else load the code-behind from a .py file |
417 pycpath = pypath + 'c' | 441 pycpath = pypath + 'c' |
418 pyctime = self._gettime(pycpath) | 442 pyctime = self._gettime(pycpath) |
419 try: | 443 try: |
430 except Exception as e: | 454 except Exception as e: |
431 raise TinCanError("{0}: error importing".format(pycpath)) from e | 455 raise TinCanError("{0}: error importing".format(pycpath)) from e |
432 self._class = None | 456 self._class = None |
433 for i in dir(mod): | 457 for i in dir(mod): |
434 v = getattr(mod, i) | 458 v = getattr(mod, i) |
435 if isclass(v) and issubclass(v, Page): | 459 if isclass(v) and issubclass(v, klass): |
436 if self._class is not None: | 460 if self._class is not None: |
437 raise TinCanError("{0}: contains multiple Page classes".format(pypath)) | 461 raise TinCanError("{0}: contains multiple {1} classes".format(pypath, klass.__name__)) |
438 self._class = v | 462 self._class = v |
439 if self._class is None: | 463 if self._class is None: |
440 raise TinCanError("{0}: contains no Page classes".format(pypath)) | 464 raise TinCanError("{0}: contains no {1} classes".format(pypath, klass.__name__)) |
441 | 465 |
442 def _redirect(self): | 466 def _redirect(self): |
443 try: | 467 try: |
444 rlist = self._splitpath(self._header.forward) | 468 rlist = self._splitpath(self._header.forward) |
445 forw = '/' + '/'.join(rlist) | 469 forw = '/' + '/'.join(rlist) |