Mercurial > cgi-bin > hgweb.cgi > tincan
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. |