comparison tincan.py @ 25:e93e5e746cc5 draft header-includes

Preliminary debugging, still not fully tested.
author David Barts <n5jrn@me.com>
date Sun, 26 May 2019 11:43:48 -0700
parents 34d3cfcd37ef
children cc6ba7834294
comparison
equal deleted inserted replaced
24:34d3cfcd37ef 25:e93e5e746cc5
26 """ 26 """
27 The parent class of all exceptions we raise. 27 The parent class of all exceptions we raise.
28 """ 28 """
29 pass 29 pass
30 30
31 class BaseSyntaxError(TinCanException):
32
33
34 class TemplateHeaderError(TinCanException): 31 class TemplateHeaderError(TinCanException):
35 """ 32 """
36 Raised upon encountering a syntax error in the template headers. 33 Raised upon encountering a syntax error in the template headers.
37 """ 34 """
38 def __init__(self, message, line): 35 def __init__(self, message, line):
52 super().__init__(message, source) 49 super().__init__(message, source)
53 self.message = message 50 self.message = message
54 self.source = source 51 self.source = source
55 52
56 def __str__(self): 53 def __str__(self):
57 return "{0}: {1}".format(self.source, self.message) 54 return "{0}: #include error: {1}".format(self.source, self.message)
58 55
59 class ForwardException(TinCanException): 56 class ForwardException(TinCanException):
60 """ 57 """
61 Raised to effect the flow control needed to do a forward (server-side 58 Raised to effect the flow control needed to do a forward (server-side
62 redirect). It is ugly to do this, but other Python frameworks do and 59 redirect). It is ugly to do this, but other Python frameworks do and
119 self._state(line) 116 self._state(line)
120 return 117 return
121 if line.startswith(self._END) and (len(line) == self._LEND or line[self._LEND] in self._WS): 118 if line.startswith(self._END) and (len(line) == self._LEND or line[self._LEND] in self._WS):
122 self._state = self._body 119 self._state = self._body
123 self._hbuf.append(line) 120 self._hbuf.append(line)
124 self._bbuf.append("\n")
125 121
126 def _body(self, line): 122 def _body(self, line):
127 self._bbuf.append(line) 123 self._bbuf.append(line)
128 124
129 class TemplateHeader(object): 125 class TemplateHeader(object):
138 # Initialize our state 134 # Initialize our state
139 for i in self._NAMES: 135 for i in self._NAMES:
140 setattr(self, i, None) 136 setattr(self, i, None)
141 for i in self._FNAMES: 137 for i in self._FNAMES:
142 setattr(self, i, False) 138 setattr(self, i, False)
139 for i in self._ANAMES:
140 setattr(self, i, [])
143 # Parse the string 141 # Parse the string
144 count = 0 142 count = 0
145 nameset = set(self._NAMES + self._FNAMES) 143 nameset = set(self._NAMES + self._FNAMES + self._ANAMES)
146 seen = set() 144 seen = set()
147 lines = string.split("\n") 145 lines = string.split("\n")
148 if lines and lines[-1] == "": 146 if lines and lines[-1] == "":
149 del lines[-1] 147 del lines[-1]
150 for line in lines: 148 for line in lines:
338 self.subdir = subdir 336 self.subdir = subdir
339 self.name = name 337 self.name = name
340 self.included = set() if included is None else included 338 self.included = set() if included is None else included
341 self.encoding = encoding 339 self.encoding = encoding
342 self._buf = [] 340 self._buf = []
343 self._tlib = os.path.join(base, _WINF, "tlib")
344 341
345 def render(self, includes, body): 342 def render(self, includes, body):
346 for i in includes: 343 for i in includes:
347 if i.startswith('<') and i.endswith('>'): 344 if i.startswith('<') and i.endswith('>'):
348 self._buf.append(self._render1(self._tlib, i[1:-1])) 345 self._buf.append(self._render1([_WINF, "tlib"], i[1:-1]))
349 else: 346 else:
350 self._buf.append(self._render1(self.subdir, i)) 347 self._buf.append(self._render1(self.subdir, i))
351 self._buf.append(body) 348 self._buf.append(body)
352 return ''.join(self._buf) 349 return ''.join(self._buf)
353 350
354 def _render1(self, subdir, path): 351 def _render1(self, subdir, path):
355 # Reject bad file names 352 # Reject bad file names
356 if not path.endswith(_IEXTEN) or path.endswith(_PEXTEN): 353 if not path.endswith(_IEXTEN) or path.endswith(_TEXTEN):
357 raise IncludeError(self.name, "#include files must end with {0} or {1}".format(_IEXTEN, _PEXTEN)) 354 raise IncludeError("file names must end with {0} or {1}".format(_IEXTEN, _TEXTEN), self.name)
358 # Normalize 355 # Normalize
359 rawpath = _normpath(subdir, path) 356 rawpath = _normpath(subdir, path)
360 # Only include once 357 # Only include once
361 relpath = '/' + '/'.join(rawpath) 358 relpath = '/' + '/'.join(rawpath)
362 if relpath in self.included: 359 if relpath in self.included:
366 npath = os.path.join(self.base, *rawpath) 363 npath = os.path.join(self.base, *rawpath)
367 nsubdir = rawpath[:-1] 364 nsubdir = rawpath[:-1]
368 try: 365 try:
369 tf = TemplateFile(npath) 366 tf = TemplateFile(npath)
370 except OSError as e: 367 except OSError as e:
371 raise IncludeError(self.name, str(e)) from e 368 raise IncludeError(str(e), self.name) from e
372 try: 369 try:
373 th = TemplateHeader(tf.headers) 370 th = TemplateHeader(tf.header)
374 except TemplateHeaderError as e: 371 except TemplateHeaderError as e:
375 raise IncludeError(npath, str(e)) from e 372 raise IncludeError(str(e), npath) from e
376 # Reject unsupported crap (included files can only #include) 373 # Reject unsupported crap (included files can only #include)
377 for i in dir(th): 374 for i in dir(th):
378 if i.startswith('_') or i == 'include': 375 if i.startswith('_') or i == 'include':
379 continue 376 continue
380 v = getattr(th, i) 377 v = getattr(th, i)
381 if callable(v): 378 if callable(v):
382 continue 379 continue
383 if v is not None and v is not False: 380 if v is not None and v != False:
384 raise IncludeError(npath, "unsupported #{0}".format(i)) 381 raise IncludeError(npath, "unsupported #{0}".format(i))
385 # Inclusion is recursive... 382 # Inclusion is recursive...
386 nested = _Includer(self.base, nsubdir, relpath, 383 nested = _Includer(self.base, nsubdir, relpath,
387 included=self.included, encoding=self.encoding) 384 included=self.included, encoding=self.encoding)
388 return nested.render(th.includes, tf.body) 385 return nested.render(th.include, tf.body)
389 386
390 # R o u t e s 387 # R o u t e s
391 # 388 #
392 # Represents a route in TinCan. Our launcher creates these on-the-fly based 389 # Represents a route in TinCan. Our launcher creates these on-the-fly based
393 # on the files it finds. 390 # on the files it finds.
415 def __call__(self, e): 412 def __call__(self, e):
416 bottle.request.environ[_FTYPE] = True 413 bottle.request.environ[_FTYPE] = True
417 try: 414 try:
418 obj = self._class(bottle.request, e) 415 obj = self._class(bottle.request, e)
419 obj.handle() 416 obj.handle()
420 return self._template.render(obj.export()).lstrip('\n') 417 return self._template.render(obj.export())
421 except bottle.HTTPResponse as e: 418 except bottle.HTTPResponse as e:
422 return e 419 return e
423 except Exception as e: 420 except Exception as e:
424 traceback.print_exc() 421 traceback.print_exc()
425 # Bottle doesn't allow error handlers to themselves cause 422 # Bottle doesn't allow error handlers to themselves cause
443 self._origin = self._urlpath 440 self._origin = self._urlpath
444 self._subdir = subdir 441 self._subdir = subdir
445 self._seen = set() 442 self._seen = set()
446 self._tclass = launcher.tclass 443 self._tclass = launcher.tclass
447 self._app = launcher.app 444 self._app = launcher.app
445 self._save_includes = launcher.debug
448 446
449 def launch(self): 447 def launch(self):
450 """ 448 """
451 Launch a single page. 449 Launch a single page.
452 """ 450 """
486 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) 484 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN))
487 self._python = self._header.python 485 self._python = self._header.python
488 self._python_specified = True 486 self._python_specified = True
489 # Obtain a class object by importing and introspecting a module. 487 # Obtain a class object by importing and introspecting a module.
490 self._getclass() 488 self._getclass()
491 # Build body object (#template) 489 # Build body object (#template) and process #includes.
492 if self._header.template is not None: 490 if self._header.template is not None:
493 if not self._header.template.endswith(_TEXTEN): 491 if not self._header.template.endswith(_TEXTEN):
494 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) 492 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN))
495 try: 493 try:
496 rawpath = self._splitpath(self._header.template) 494 rawpath = self._splitpath(self._header.template)
497 tsubdir = rawpath[:-1] 495 tsubdir = rawpath[:-1]
496 tname = rawpath[-1]
498 tpath = os.path.normpath(os.path.join(self._fsroot, *rawpath)) 497 tpath = os.path.normpath(os.path.join(self._fsroot, *rawpath))
499 turlpath = '/' + self._urljoin(rawpath) 498 turlpath = '/' + self._urljoin(rawpath)
500 tfile = TemplateFile(tpath) 499 tfile = TemplateFile(tpath)
501 thead = TemplateHeader(tpath.header) 500 thead = TemplateHeader(tfile.header)
502 except (OSError, TemplateHeaderError) as e: 501 except (OSError, TemplateHeaderError) as e:
503 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e 502 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e
504 except IndexError as e: 503 except IndexError as e:
505 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e 504 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e
506 includer = _Includer(self._fspath, tsubdir, turlpath) 505 r = self._getbody(tsubdir, tname, thead.include, tfile.body)
507 try: 506 self._dumpbody(r, tpath, turlpath)
508 self._body = self._tclass(source=includer.render(thead.include, tfile.body))
509 except IncludeError as e:
510 raise TinCanError("{0}: #include error: {1!s}".format(turlpath, e)) from e
511 else: 507 else:
512 includer = _Includer(self._fspath, self._subdir, self._urlpath) 508 r = self._getbody(self._subdir, self._name+_TEXTEN,
513 try: 509 self._header.include, self._template.body)
514 self._body = self._tclass( 510 self._dumpbody(r, self._fspath, self._urlpath)
515 source=includer.render(self._header.include, self._template.body))
516 except IncludeError as e:
517 raise TinCanError("{0}: #include error: {1!s}".format(self._urlpath, e)) from e
518 self._body.prepare()
519 # If this is an #errors page, register it as such. 511 # If this is an #errors page, register it as such.
520 if oheader.errors is not None: 512 if oheader.errors is not None:
521 self._mkerror(oheader.errors) 513 self._mkerror(oheader.errors)
522 return # this implies #hidden 514 return # this implies #hidden
523 # Get #methods for this route 515 # Get #methods for this route
528 if not methods: 520 if not methods:
529 raise TinCanError("{0}: no #methods specified".format(self._urlpath)) 521 raise TinCanError("{0}: no #methods specified".format(self._urlpath))
530 # Register this thing with Bottle 522 # Register this thing with Bottle
531 print("adding route:", self._origin, '('+','.join(methods)+')') # debug 523 print("adding route:", self._origin, '('+','.join(methods)+')') # debug
532 self._app.route(self._origin, methods, self) 524 self._app.route(self._origin, methods, self)
525
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(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
533 546
534 def _splitpath(self, unsplit): 547 def _splitpath(self, unsplit):
535 return _normpath(self._subdir, unsplit) 548 return _normpath(self._subdir, unsplit)
536 549
537 def _mkerror(self, rerrors): 550 def _mkerror(self, rerrors):
652 """ 665 """
653 target = None 666 target = None
654 try: 667 try:
655 obj = self._class(bottle.request, bottle.response) 668 obj = self._class(bottle.request, bottle.response)
656 obj.handle() 669 obj.handle()
657 return self._body.render(obj.export()).lstrip('\n') 670 return self._body.render(obj.export())
658 except ForwardException as fwd: 671 except ForwardException as fwd:
659 target = fwd.target 672 target = fwd.target
660 except bottle.HTTPResponse as e: 673 except bottle.HTTPResponse as e:
661 return e 674 return e
662 except Exception as e: 675 except Exception as e:
773 786
774 def _logger(message): 787 def _logger(message):
775 sys.stderr.write(message) 788 sys.stderr.write(message)
776 sys.stderr.write('\n') 789 sys.stderr.write('\n')
777 790
778 def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger): 791 def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger, debug=False):
779 """ 792 """
780 Launch and return a TinCan webapp. Does not run the app; it is the 793 Launch and return a TinCan webapp. Does not run the app; it is the
781 caller's responsibility to call app.run() 794 caller's responsibility to call app.run()
782 """ 795 """
783 if fsroot is None: 796 if fsroot is None:
784 fsroot = os.getcwd() 797 fsroot = os.getcwd()
785 launcher = _Launcher(fsroot, urlroot, tclass, logger) 798 launcher = _Launcher(fsroot, urlroot, tclass, logger)
786 # launcher.debug = True 799 launcher.debug = debug
787 launcher.launch() 800 launcher.launch()
788 return launcher.app, launcher.errors 801 return launcher.app, launcher.errors
789 802
790 # XXX - We cannot implement a command-line launcher here; see the 803 # XXX - We cannot implement a command-line launcher here; see the
791 # launcher script for why. 804 # launcher script for why.