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