Mercurial > cgi-bin > hgweb.cgi > tincan
comparison tincan.py @ 39:ee19984ba31d draft
Merge in the header-includes branch to the trunk.
author | David Barts <n5jrn@me.com> |
---|---|
date | Tue, 28 May 2019 20:20:19 -0700 |
parents | ce67eac10fc7 |
children | df27cf08c093 |
comparison
equal
deleted
inserted
replaced
23:e8b6ee7e5b6b | 39:ee19984ba31d |
---|---|
38 self.line = line | 38 self.line = line |
39 | 39 |
40 def __str__(self): | 40 def __str__(self): |
41 return "line {0}: {1}".format(self.line, self.message) | 41 return "line {0}: {1}".format(self.line, self.message) |
42 | 42 |
43 class LoadError(TinCanException): | |
44 """ | |
45 Raised when we run into problems #load'ing something, usually | |
46 because it doesn't exist. | |
47 """ | |
48 def __init__(self, message, source): | |
49 super().__init__(message, source) | |
50 self.message = message | |
51 self.source = source | |
52 | |
53 def __str__(self): | |
54 return "{0}: #load error: {1}".format(self.source, self.message) | |
55 | |
43 class ForwardException(TinCanException): | 56 class ForwardException(TinCanException): |
44 """ | 57 """ |
45 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 |
46 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 |
47 there seems to be no good alternative. | 60 there seems to be no good alternative. |
114 """ | 127 """ |
115 Parses and represents a set of header lines. | 128 Parses and represents a set of header lines. |
116 """ | 129 """ |
117 _NAMES = [ "errors", "forward", "methods", "python", "template" ] | 130 _NAMES = [ "errors", "forward", "methods", "python", "template" ] |
118 _FNAMES = [ "hidden" ] | 131 _FNAMES = [ "hidden" ] |
132 _ANAMES = [ "load" ] | |
119 | 133 |
120 def __init__(self, string): | 134 def __init__(self, string): |
121 # Initialize our state | 135 # Initialize our state |
122 for i in self._NAMES: | 136 for i in self._NAMES: |
123 setattr(self, i, None) | 137 setattr(self, i, None) |
124 for i in self._FNAMES: | 138 for i in self._FNAMES: |
125 setattr(self, i, False) | 139 setattr(self, i, False) |
140 for i in self._ANAMES: | |
141 setattr(self, i, []) | |
126 # Parse the string | 142 # Parse the string |
127 count = 0 | 143 count = 0 |
128 nameset = set(self._NAMES + self._FNAMES) | 144 nameset = set(self._NAMES + self._FNAMES + self._ANAMES) |
129 seen = set() | 145 seen = set() |
130 lines = string.split("\n") | 146 lines = string.split("\n") |
131 if lines and lines[-1] == "": | 147 if lines and lines[-1] == "": |
132 del lines[-1] | 148 del lines[-1] |
133 for line in lines: | 149 for line in lines: |
148 break | 164 break |
149 if name not in nameset: | 165 if name not in nameset: |
150 raise TemplateHeaderError("Invalid directive: {0!r}".format(rna), count) | 166 raise TemplateHeaderError("Invalid directive: {0!r}".format(rna), count) |
151 if name in seen: | 167 if name in seen: |
152 raise TemplateHeaderError("Duplicate {0!r} directive.".format(rna), count) | 168 raise TemplateHeaderError("Duplicate {0!r} directive.".format(rna), count) |
153 seen.add(name) | 169 if name not in self._ANAMES: |
170 seen.add(name) | |
154 # Flags | 171 # Flags |
155 if name in self._FNAMES: | 172 if name in self._FNAMES: |
156 setattr(self, name, True) | 173 setattr(self, name, True) |
157 continue | 174 continue |
158 # Get parameter | 175 # Get parameter |
162 for i in [ "'", '"']: | 179 for i in [ "'", '"']: |
163 if param.startswith(i) and param.endswith(i): | 180 if param.startswith(i) and param.endswith(i): |
164 param = ast.literal_eval(param) | 181 param = ast.literal_eval(param) |
165 break | 182 break |
166 # Update this object | 183 # Update this object |
167 setattr(self, name, param) | 184 if name in self._ANAMES: |
185 getattr(self, name).append(param) | |
186 else: | |
187 setattr(self, name, param) | |
168 | 188 |
169 # C h a m e l e o n | 189 # C h a m e l e o n |
170 # | 190 # |
171 # Support for Chameleon templates (the kind TinCan uses by default). | 191 # Support for Chameleon templates (the kind TinCan uses). |
172 | 192 |
173 class ChameleonTemplate(bottle.BaseTemplate): | 193 class ChameleonTemplate(bottle.BaseTemplate): |
174 def prepare(self, **options): | 194 def prepare(self, **options): |
175 from chameleon import PageTemplate, PageTemplateFile | 195 from chameleon import PageTemplate, PageTemplateFile |
176 if self.source: | 196 if self.source: |
177 self.tpl = PageTemplate(self.source, encoding=self.encoding, | 197 self.tpl = PageTemplate(self.source, **options) |
178 **options) | |
179 else: | 198 else: |
180 self.tpl = PageTemplateFile(self.filename, encoding=self.encoding, | 199 self.tpl = PageTemplateFile(self.filename, encoding=self.encoding, |
181 search_path=self.lookup, **options) | 200 search_path=self.lookup, **options) |
201 # XXX - work around broken Chameleon decoding | |
202 self.tpl.default_encoding = self.encoding | |
182 | 203 |
183 def render(self, *args, **kwargs): | 204 def render(self, *args, **kwargs): |
184 for dictarg in args: | 205 for dictarg in args: |
185 kwargs.update(dictarg) | 206 kwargs.update(dictarg) |
186 _defaults = self.defaults.copy() | 207 _defaults = self.defaults.copy() |
303 Constructor. This is a lightweight operation. | 324 Constructor. This is a lightweight operation. |
304 """ | 325 """ |
305 self.request = req | 326 self.request = req |
306 self.error = err | 327 self.error = err |
307 | 328 |
329 # I n c l u s i o n | |
330 # | |
331 # Most processing is in the TinCanRoute class; this just interprets and | |
332 # represents arguments to the #load header directive. | |
333 | |
334 class _LoadedFile(object): | |
335 def __init__(self, raw): | |
336 if raw.startswith('<') and raw.endswith('>'): | |
337 raw = raw[1:-1] | |
338 self.in_lib = True | |
339 else: | |
340 self.in_lib = False | |
341 equals = raw.find('=') | |
342 if equals < 0: | |
343 self.vname = os.path.splitext(os.path.basename(raw))[0] | |
344 self.fname = raw | |
345 else: | |
346 self.vname = raw[:equals] | |
347 self.fname = raw[equals+1:] | |
348 if self.vname == "": | |
349 raise ValueError("empty variable name") | |
350 if self.fname == "": | |
351 raise ValueError("empty file name") | |
352 if not self.fname.endswith(_IEXTEN): | |
353 raise ValueError("file does not end in {0}".format(_IEXTEN)) | |
354 | |
355 # Using a cache is likely to help efficiency a lot, since many pages | |
356 # will typically #load the same standard stuff. | |
357 _tcache = {} | |
358 def _get_template(name, direct, coding): | |
359 aname = os.path.abspath(os.path.join(direct, name)) | |
360 if aname not in _tcache: | |
361 tmpl = ChameleonTemplate(name=name, lookup=[direct], encoding=coding) | |
362 tmpl.prepare() | |
363 assert aname == tmpl.filename | |
364 _tcache[aname] = tmpl | |
365 return _tcache[aname] | |
366 | |
308 # R o u t e s | 367 # R o u t e s |
309 # | 368 # |
310 # Represents a route in TinCan. Our launcher creates these on-the-fly based | 369 # Represents a route in TinCan. Our launcher creates these on-the-fly based |
311 # on the files it finds. | 370 # on the files it finds. |
312 | 371 |
313 _ERRMIN = 400 | 372 _ERRMIN = 400 |
314 _ERRMAX = 599 | 373 _ERRMAX = 599 |
374 _IEXTEN = ".pt" | |
315 _PEXTEN = ".py" | 375 _PEXTEN = ".py" |
316 _TEXTEN = ".pspx" | 376 _TEXTEN = ".pspx" |
317 _FLOOP = "tincan.forwards" | 377 _FLOOP = "tincan.forwards" |
318 _FORIG = "tincan.origin" | 378 _FORIG = "tincan.origin" |
319 _FTYPE = "tincan.iserror" | 379 _FTYPE = "tincan.iserror" |
323 A route to an error page. These don't get routes created for them, | 383 A route to an error page. These don't get routes created for them, |
324 and are only reached if an error routes them there. Unless you create | 384 and are only reached if an error routes them there. Unless you create |
325 custom code-behind, only two variables are available to your template: | 385 custom code-behind, only two variables are available to your template: |
326 request (bottle.Request) and error (bottle.HTTPError). | 386 request (bottle.Request) and error (bottle.HTTPError). |
327 """ | 387 """ |
328 def __init__(self, template, klass): | 388 def __init__(self, template, loads, klass): |
329 self._template = template | 389 self._template = template |
330 self._template.prepare() | 390 self._template.prepare() |
391 self._loads = loads | |
331 self._class = klass | 392 self._class = klass |
332 | 393 |
333 def __call__(self, e): | 394 def __call__(self, e): |
334 bottle.request.environ[_FTYPE] = True | 395 bottle.request.environ[_FTYPE] = True |
335 try: | 396 try: |
336 obj = self._class(bottle.request, e) | 397 obj = self._class(bottle.request, e) |
337 obj.handle() | 398 obj.handle() |
338 return self._template.render(obj.export()).lstrip('\n') | 399 tvars = self._loads.copy() |
400 tvars.update(obj.export()) | |
401 return self._template.render(tvars).lstrip('\n') | |
339 except bottle.HTTPResponse as e: | 402 except bottle.HTTPResponse as e: |
340 return e | 403 return e |
341 except Exception as e: | 404 except Exception as e: |
342 traceback.print_exc() | 405 traceback.print_exc() |
343 # Bottle doesn't allow error handlers to themselves cause | 406 # Bottle doesn't allow error handlers to themselves cause |
359 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) | 422 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) |
360 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) | 423 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) |
361 self._origin = self._urlpath | 424 self._origin = self._urlpath |
362 self._subdir = subdir | 425 self._subdir = subdir |
363 self._seen = set() | 426 self._seen = set() |
364 self._tclass = launcher.tclass | |
365 self._app = launcher.app | 427 self._app = launcher.app |
428 self._save_loads = launcher.debug | |
429 self._encoding = launcher.encoding | |
366 | 430 |
367 def launch(self): | 431 def launch(self): |
368 """ | 432 """ |
369 Launch a single page. | 433 Launch a single page. |
370 """ | 434 """ |
404 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) | 468 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) |
405 self._python = self._header.python | 469 self._python = self._header.python |
406 self._python_specified = True | 470 self._python_specified = True |
407 # Obtain a class object by importing and introspecting a module. | 471 # Obtain a class object by importing and introspecting a module. |
408 self._getclass() | 472 self._getclass() |
409 # Build body object (#template) | 473 # Build body object (#template) and obtain #loads. |
410 if self._header.template is not None: | 474 if self._header.template is not None: |
411 if not self._header.template.endswith(_TEXTEN): | 475 if not self._header.template.endswith(_TEXTEN): |
412 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) | 476 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) |
413 try: | 477 try: |
414 tpath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._header.template))) | 478 rtpath = self._splitpath(self._header.template) |
479 tpath = os.path.normpath(os.path.join(self._fsroot, *rtpath)) | |
415 tfile = TemplateFile(tpath) | 480 tfile = TemplateFile(tpath) |
416 except OSError as e: | 481 except OSError as e: |
417 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e | 482 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e |
418 except IndexError as e: | 483 except IndexError as e: |
419 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e | 484 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e |
420 self._body = self._tclass(source=tfile.body) | 485 self._body = ChameleonTemplate(source=tfile.body, encoding=self._encoding) |
421 else: | 486 else: |
422 self._body = self._tclass(source=self._template.body) | 487 self._body = ChameleonTemplate(source=self._template.body, encoding=self._encoding) |
423 self._body.prepare() | 488 self._body.prepare() |
489 # Process loads | |
490 self._loads = {} | |
491 for load in self._header.load: | |
492 try: | |
493 load = _LoadedFile(load) | |
494 except ValueError as e: | |
495 raise TinCanError("{0}: bad #load: {1!s}".format(self._urlpath, e)) from e | |
496 if load.in_lib: | |
497 fdir = os.path.join(self._fsroot, _WINF, "tlib") | |
498 else: | |
499 fdir = os.path.join(self._fsroot, *self._subdir) | |
500 try: | |
501 tmpl = _get_template(load.fname, fdir, self._encoding) | |
502 except Exception as e: | |
503 raise TinCanError("{0}: bad #load: {1!s}".format(self._urlpath, e)) from e | |
504 self._loads[load.vname] = tmpl.tpl | |
424 # If this is an #errors page, register it as such. | 505 # If this is an #errors page, register it as such. |
425 if oheader.errors is not None: | 506 if oheader.errors is not None: |
426 self._mkerror(oheader.errors) | 507 self._mkerror(oheader.errors) |
427 return # this implies #hidden | 508 return # this implies #hidden |
428 # Get #methods for this route | 509 # Get #methods for this route |
444 errors = [ int(i) for i in rerrors.split() ] | 525 errors = [ int(i) for i in rerrors.split() ] |
445 except ValueError as e: | 526 except ValueError as e: |
446 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e | 527 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e |
447 if not errors: | 528 if not errors: |
448 errors = range(_ERRMIN, _ERRMAX+1) | 529 errors = range(_ERRMIN, _ERRMAX+1) |
449 route = _TinCanErrorRoute(self._tclass(source=self._template.body), self._class) | 530 route = _TinCanErrorRoute( |
531 ChameleonTemplate(source=self._template.body, encoding=self._encoding), | |
532 self._loads, self._class) | |
450 for error in errors: | 533 for error in errors: |
451 if error < _ERRMIN or error > _ERRMAX: | 534 if error < _ERRMIN or error > _ERRMAX: |
452 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) | 535 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) |
453 self._app.error_handler[error] = route # XXX | 536 self._app.error_handler[error] = route # XXX |
454 | 537 |
557 """ | 640 """ |
558 target = None | 641 target = None |
559 try: | 642 try: |
560 obj = self._class(bottle.request, bottle.response) | 643 obj = self._class(bottle.request, bottle.response) |
561 obj.handle() | 644 obj.handle() |
562 return self._body.render(obj.export()).lstrip('\n') | 645 tvars = self._loads.copy() |
646 tvars.update(obj.export()) | |
647 return self._body.render(tvars).lstrip('\n') | |
563 except ForwardException as fwd: | 648 except ForwardException as fwd: |
564 target = fwd.target | 649 target = fwd.target |
565 except bottle.HTTPResponse as e: | 650 except bottle.HTTPResponse as e: |
566 return e | 651 return e |
567 except Exception as e: | 652 except Exception as e: |
588 route, args = self._app.router.match(environ) | 673 route, args = self._app.router.match(environ) |
589 environ['route.handle'] = environ['bottle.route'] = route | 674 environ['route.handle'] = environ['bottle.route'] = route |
590 environ['route.url_args'] = args | 675 environ['route.url_args'] = args |
591 return route.call(**args) | 676 return route.call(**args) |
592 | 677 |
593 def _mkdict(self, obj): | |
594 ret = {} | |
595 for name in dir(obj): | |
596 if name.startswith('_'): | |
597 continue | |
598 value = getattr(obj, name) | |
599 if not callable(value): | |
600 ret[name] = value | |
601 return ret | |
602 | |
603 # L a u n c h e r | 678 # L a u n c h e r |
604 | 679 |
605 _WINF = "WEB-INF" | 680 _WINF = "WEB-INF" |
606 _BANNED = set([_WINF]) | 681 _BANNED = set([_WINF]) |
682 ENCODING = "utf-8" | |
607 | 683 |
608 class _Launcher(object): | 684 class _Launcher(object): |
609 """ | 685 """ |
610 Helper class for launching webapps. | 686 Helper class for launching webapps. |
611 """ | 687 """ |
612 def __init__(self, fsroot, urlroot, tclass, logger): | 688 def __init__(self, fsroot, urlroot, logger): |
613 """ | 689 """ |
614 Lightweight constructor. The real action happens in .launch() below. | 690 Lightweight constructor. The real action happens in .launch() below. |
615 """ | 691 """ |
616 self.fsroot = fsroot | 692 self.fsroot = fsroot |
617 self.urlroot = urlroot | 693 self.urlroot = urlroot |
618 self.tclass = tclass | |
619 self.logger = logger | 694 self.logger = logger |
620 self.app = None | 695 self.app = None |
621 self.errors = 0 | 696 self.errors = 0 |
622 self.debug = False | 697 self.debug = False |
698 self.encoding = ENCODING | |
623 | 699 |
624 def launch(self): | 700 def launch(self): |
625 """ | 701 """ |
626 Does the actual work of launching something. XXX - modifies sys.path | 702 Does the actual work of launching something. XXX - modifies sys.path |
627 and never un-modifies it. | 703 and never un-modifies it. |
678 | 754 |
679 def _logger(message): | 755 def _logger(message): |
680 sys.stderr.write(message) | 756 sys.stderr.write(message) |
681 sys.stderr.write('\n') | 757 sys.stderr.write('\n') |
682 | 758 |
683 def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger): | 759 def launch(fsroot=None, urlroot='/', logger=_logger, debug=False, encoding=ENCODING): |
684 """ | 760 """ |
685 Launch and return a TinCan webapp. Does not run the app; it is the | 761 Launch and return a TinCan webapp. Does not run the app; it is the |
686 caller's responsibility to call app.run() | 762 caller's responsibility to call app.run() |
687 """ | 763 """ |
688 if fsroot is None: | 764 if fsroot is None: |
689 fsroot = os.getcwd() | 765 fsroot = os.getcwd() |
690 launcher = _Launcher(fsroot, urlroot, tclass, logger) | 766 launcher = _Launcher(fsroot, urlroot, logger) |
691 # launcher.debug = True | 767 launcher.debug = debug |
768 launcher.encoding = encoding | |
692 launcher.launch() | 769 launcher.launch() |
693 return launcher.app, launcher.errors | 770 return launcher.app, launcher.errors |
694 | 771 |
695 # XXX - We cannot implement a command-line launcher here; see the | 772 # XXX - We cannot implement a command-line launcher here; see the |
696 # launcher script for why. | 773 # launcher script for why. |