Mercurial > cgi-bin > hgweb.cgi > tincan
comparison tincan.py @ 29:2e3ac3d7b0a4 draft header-includes
A possible workaround for the drainbamage (needs testing)?
author | David Barts <n5jrn@me.com> |
---|---|
date | Mon, 27 May 2019 14:11:22 -0700 |
parents | cc6ba7834294 |
children | f34d5a90d618 |
comparison
equal
deleted
inserted
replaced
26:cc6ba7834294 | 29:2e3ac3d7b0a4 |
---|---|
324 self.request = req | 324 self.request = req |
325 self.error = err | 325 self.error = err |
326 | 326 |
327 # I n c l u s i o n | 327 # I n c l u s i o n |
328 # | 328 # |
329 # This is where the #include directives get processed | 329 # Most processing is in the TinCanRoute class; this just interprets and |
330 | 330 # represents arguments to the #include header directive. |
331 _IEXTEN = ".pt" | 331 |
332 | 332 class _IncludedFile(object): |
333 class _Includer(object): | 333 def __init__(self, raw): |
334 def __init__(self, base, subdir, name, included=None, encoding="utf-8"): | 334 if raw.startswith('<') and raw.endswith('>'): |
335 self.base = base | 335 raw = raw[1:-1] |
336 self.subdir = subdir | 336 self.in_lib = True |
337 self.name = name | 337 else: |
338 self.included = set() if included is None else included | 338 self.in_lib = False |
339 self.encoding = encoding | 339 comma = raw.find(',') |
340 self._buf = [] | 340 if comma < 0: |
341 | 341 raise ValueError("missing comma") |
342 def render(self, includes, body): | 342 self.vname = raw[:comma] |
343 for i in includes: | 343 if self.vname == "": |
344 if i.startswith('<') and i.endswith('>'): | 344 raise ValueError("empty variable name") |
345 self._buf.append(self._render1([_WINF, "tlib"], i[1:-1])) | 345 self.fname = raw[comma+1:] |
346 else: | 346 if self.fname == "": |
347 self._buf.append(self._render1(self.subdir, i)) | 347 raise ValueError("empty file name") |
348 self._buf.append(body) | 348 if not self.fname.endswith(_IEXTEN): |
349 return ''.join(self._buf) | 349 raise ValueError("file does not end in {0}".format(_IEXTEN)) |
350 | |
351 def _render1(self, subdir, path): | |
352 # Reject bad file names | |
353 if not path.endswith(_IEXTEN) or path.endswith(_TEXTEN): | |
354 raise IncludeError("file names must end with {0} or {1}".format(_IEXTEN, _TEXTEN), self.name) | |
355 # Normalize | |
356 rawpath = _normpath(subdir, path) | |
357 # Only include once | |
358 relpath = '/' + '/'.join(rawpath) | |
359 if relpath in self.included: | |
360 return | |
361 self.included.add(relpath) | |
362 # Do actual inclusion of a file | |
363 npath = os.path.join(self.base, *rawpath) | |
364 nsubdir = rawpath[:-1] | |
365 try: | |
366 tf = TemplateFile(npath) | |
367 except OSError as e: | |
368 raise IncludeError(str(e), self.name) from e | |
369 try: | |
370 th = TemplateHeader(tf.header) | |
371 except TemplateHeaderError as e: | |
372 raise IncludeError(str(e), npath) from e | |
373 # Reject unsupported crap (included files can only #include) | |
374 for i in dir(th): | |
375 if i.startswith('_') or i == 'include': | |
376 continue | |
377 v = getattr(th, i) | |
378 if callable(v): | |
379 continue | |
380 if v is not None and v != False: | |
381 raise IncludeError(npath, "unsupported #{0}".format(i)) | |
382 # Inclusion is recursive... | |
383 nested = _Includer(self.base, nsubdir, relpath, | |
384 included=self.included, encoding=self.encoding) | |
385 return nested.render(th.include, tf.body) | |
386 | 350 |
387 # R o u t e s | 351 # R o u t e s |
388 # | 352 # |
389 # Represents a route in TinCan. Our launcher creates these on-the-fly based | 353 # Represents a route in TinCan. Our launcher creates these on-the-fly based |
390 # on the files it finds. | 354 # on the files it finds. |
391 | 355 |
392 _ERRMIN = 400 | 356 _ERRMIN = 400 |
393 _ERRMAX = 599 | 357 _ERRMAX = 599 |
358 _IEXTEN = ".pt" | |
394 _PEXTEN = ".py" | 359 _PEXTEN = ".py" |
395 _TEXTEN = ".pspx" | 360 _TEXTEN = ".pspx" |
396 _FLOOP = "tincan.forwards" | 361 _FLOOP = "tincan.forwards" |
397 _FORIG = "tincan.origin" | 362 _FORIG = "tincan.origin" |
398 _FTYPE = "tincan.iserror" | 363 _FTYPE = "tincan.iserror" |
402 A route to an error page. These don't get routes created for them, | 367 A route to an error page. These don't get routes created for them, |
403 and are only reached if an error routes them there. Unless you create | 368 and are only reached if an error routes them there. Unless you create |
404 custom code-behind, only two variables are available to your template: | 369 custom code-behind, only two variables are available to your template: |
405 request (bottle.Request) and error (bottle.HTTPError). | 370 request (bottle.Request) and error (bottle.HTTPError). |
406 """ | 371 """ |
407 def __init__(self, template, klass): | 372 def __init__(self, template, includes, klass): |
408 self._template = template | 373 self._template = template |
409 self._template.prepare() | 374 self._template.prepare() |
375 self._includes = includes | |
410 self._class = klass | 376 self._class = klass |
411 | 377 |
412 def __call__(self, e): | 378 def __call__(self, e): |
413 bottle.request.environ[_FTYPE] = True | 379 bottle.request.environ[_FTYPE] = True |
414 try: | 380 try: |
415 obj = self._class(bottle.request, e) | 381 obj = self._class(bottle.request, e) |
416 obj.handle() | 382 obj.handle() |
417 return self._template.render(obj.export()) | 383 tvars = self._includes.copy() |
384 tvars.update(obj.export()) | |
385 return self._template.render(tvars) | |
418 except bottle.HTTPResponse as e: | 386 except bottle.HTTPResponse as e: |
419 return e | 387 return e |
420 except Exception as e: | 388 except Exception as e: |
421 traceback.print_exc() | 389 traceback.print_exc() |
422 # Bottle doesn't allow error handlers to themselves cause | 390 # Bottle doesn't allow error handlers to themselves cause |
484 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) | 452 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) |
485 self._python = self._header.python | 453 self._python = self._header.python |
486 self._python_specified = True | 454 self._python_specified = True |
487 # Obtain a class object by importing and introspecting a module. | 455 # Obtain a class object by importing and introspecting a module. |
488 self._getclass() | 456 self._getclass() |
489 # Build body object (#template) and process #includes. | 457 # Build body object (#template) and obtain #includes. |
490 if self._header.template is not None: | 458 if self._header.template is not None: |
491 if not self._header.template.endswith(_TEXTEN): | 459 if not self._header.template.endswith(_TEXTEN): |
492 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) | 460 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) |
493 try: | 461 try: |
494 rawpath = self._splitpath(self._header.template) | 462 rtpath = self._splitpath(self._header.template) |
495 tsubdir = rawpath[:-1] | 463 tpath = os.path.normpath(os.path.join(self._fsroot, *rtpath)) |
496 tname = rawpath[-1] | |
497 tpath = os.path.normpath(os.path.join(self._fsroot, *rawpath)) | |
498 turlpath = '/' + self._urljoin(rawpath) | |
499 tfile = TemplateFile(tpath) | 464 tfile = TemplateFile(tpath) |
500 thead = TemplateHeader(tfile.header) | 465 except OSError as e: |
501 except (OSError, TemplateHeaderError) as e: | |
502 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e | 466 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e |
503 except IndexError as e: | 467 except IndexError as e: |
504 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e | 468 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e |
505 r = self._getbody(tsubdir, tname, thead.include, tfile.body) | 469 self._body = self._tclass(source=tfile.body) |
506 self._dumpbody(r, tpath, turlpath) | 470 try: |
471 includes = TemplateHeader(tfile.header).include | |
472 except TemplateHeaderError as e: | |
473 raise TinCanError("{0}: {1!s}".format(self._fspath, e)) from e | |
474 ibase = rtpath[:-1] | |
507 else: | 475 else: |
508 r = self._getbody(self._subdir, self._name+_TEXTEN, | 476 self._body = self._tclass(source=self._template.body) |
509 self._header.include, self._template.body) | 477 includes = self._header.include |
510 self._dumpbody(r, self._fspath, self._urlpath) | 478 ibase = self._subdir |
479 self._body.prepare() | |
480 # Process includes | |
481 self._includes = {} | |
482 for include in includes: | |
483 try: | |
484 include = _IncludedFile(include) | |
485 except ValueError as e: | |
486 raise TinCanError("{0}: bad #include: {1!s}", self._urlpath, e) from e | |
487 if include.in_lib: | |
488 fdir = os.path.join(self._fsroot, _WINF, "tlib") | |
489 else: | |
490 fdir = os.path.join(self._fsroot, *ibase) | |
491 try: | |
492 tmpl = self._tclass(name=include.fname, lookup=[fdir]) | |
493 tmpl.prepare() | |
494 # tmpl.render() # is this needed? | |
495 except Exception as e: | |
496 raise TinCanError("{0}: bad #include: {1!s}".format(self._urlpath, e)) from e | |
497 self._includes[include.vname] = tmpl.tpl | |
511 # If this is an #errors page, register it as such. | 498 # If this is an #errors page, register it as such. |
512 if oheader.errors is not None: | 499 if oheader.errors is not None: |
513 self._mkerror(oheader.errors) | 500 self._mkerror(oheader.errors) |
514 return # this implies #hidden | 501 return # this implies #hidden |
515 # Get #methods for this route | 502 # Get #methods for this route |
521 raise TinCanError("{0}: no #methods specified".format(self._urlpath)) | 508 raise TinCanError("{0}: no #methods specified".format(self._urlpath)) |
522 # Register this thing with Bottle | 509 # Register this thing with Bottle |
523 print("adding route:", self._origin, '('+','.join(methods)+')') # debug | 510 print("adding route:", self._origin, '('+','.join(methods)+')') # debug |
524 self._app.route(self._origin, methods, self) | 511 self._app.route(self._origin, methods, self) |
525 | 512 |
526 def _getbody(self, subdir, name, include, body): | |
527 includer = _Includer(self._fsroot, subdir, name) | |
528 try: | |
529 rendered = includer.render(include, body) | |
530 except IncludeError as e: | |
531 raise TinCanError(str(e)) from e | |
532 try: | |
533 self._body = self._tclass(source=rendered) | |
534 self._body.prepare() | |
535 except Exception as e: | |
536 raise TinCanError("{0}: template error: {1!s}".format(self._urlpath, e)) from e | |
537 return rendered | |
538 | |
539 def _dumpbody(self, rendered, based_on, logname): | |
540 if self._save_includes: | |
541 try: | |
542 with open(based_on + "i", "w") as fp: | |
543 fp.write(rendered) | |
544 except OSError as e: | |
545 raise TinCanError("{0}: {1!s}".format(logname, e)) from e | |
546 | |
547 def _splitpath(self, unsplit): | 513 def _splitpath(self, unsplit): |
548 return _normpath(self._subdir, unsplit) | 514 return _normpath(self._subdir, unsplit) |
549 | 515 |
550 def _mkerror(self, rerrors): | 516 def _mkerror(self, rerrors): |
551 try: | 517 try: |
552 errors = [ int(i) for i in rerrors.split() ] | 518 errors = [ int(i) for i in rerrors.split() ] |
553 except ValueError as e: | 519 except ValueError as e: |
554 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e | 520 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e |
555 if not errors: | 521 if not errors: |
556 errors = range(_ERRMIN, _ERRMAX+1) | 522 errors = range(_ERRMIN, _ERRMAX+1) |
557 route = _TinCanErrorRoute(self._tclass(source=self._template.body), self._class) | 523 route = _TinCanErrorRoute(self._tclass(source=self._template.body), |
524 self._includes, self._class) | |
558 for error in errors: | 525 for error in errors: |
559 if error < _ERRMIN or error > _ERRMAX: | 526 if error < _ERRMIN or error > _ERRMAX: |
560 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) | 527 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) |
561 self._app.error_handler[error] = route # XXX | 528 self._app.error_handler[error] = route # XXX |
562 | 529 |
665 """ | 632 """ |
666 target = None | 633 target = None |
667 try: | 634 try: |
668 obj = self._class(bottle.request, bottle.response) | 635 obj = self._class(bottle.request, bottle.response) |
669 obj.handle() | 636 obj.handle() |
670 return self._body.render(obj.export()) | 637 tvars = self._includes.copy() |
638 tvars.update(obj.export()) | |
639 return self._body.render(tvars) | |
671 except ForwardException as fwd: | 640 except ForwardException as fwd: |
672 target = fwd.target | 641 target = fwd.target |
673 except bottle.HTTPResponse as e: | 642 except bottle.HTTPResponse as e: |
674 return e | 643 return e |
675 except Exception as e: | 644 except Exception as e: |
696 route, args = self._app.router.match(environ) | 665 route, args = self._app.router.match(environ) |
697 environ['route.handle'] = environ['bottle.route'] = route | 666 environ['route.handle'] = environ['bottle.route'] = route |
698 environ['route.url_args'] = args | 667 environ['route.url_args'] = args |
699 return route.call(**args) | 668 return route.call(**args) |
700 | 669 |
701 def _mkdict(self, obj): | |
702 ret = {} | |
703 for name in dir(obj): | |
704 if name.startswith('_'): | |
705 continue | |
706 value = getattr(obj, name) | |
707 if not callable(value): | |
708 ret[name] = value | |
709 return ret | |
710 | |
711 # L a u n c h e r | 670 # L a u n c h e r |
712 | 671 |
713 _WINF = "WEB-INF" | 672 _WINF = "WEB-INF" |
714 _BANNED = set([_WINF]) | 673 _BANNED = set([_WINF]) |
715 | 674 |