Mercurial > cgi-bin > hgweb.cgi > tincan
comparison tincan.py @ 21:ca2029ce95c7 draft
Mostly done with new template logic, working through testing.
author | David Barts <n5jrn@me.com> |
---|---|
date | Tue, 21 May 2019 15:57:15 -0700 |
parents | 5d9a1b82251a |
children | f6a1492fe56e |
comparison
equal
deleted
inserted
replaced
20:6bf9a41a09f2 | 21:ca2029ce95c7 |
---|---|
16 from stat import S_ISDIR, S_ISREG | 16 from stat import S_ISDIR, S_ISREG |
17 from string import whitespace | 17 from string import whitespace |
18 import traceback | 18 import traceback |
19 import urllib | 19 import urllib |
20 | 20 |
21 from chameleon import PageTemplate, PageTemplateFile | |
21 import bottle | 22 import bottle |
22 | 23 |
23 # E x c e p t i o n s | 24 # E x c e p t i o n s |
24 | 25 |
25 class TinCanException(Exception): | 26 class TinCanException(Exception): |
164 param = ast.literal_eval(param) | 165 param = ast.literal_eval(param) |
165 break | 166 break |
166 # Update this object | 167 # Update this object |
167 setattr(self, name, param) | 168 setattr(self, name, param) |
168 | 169 |
169 # C h a m e l e o n | 170 # C h a m e l e o n ( B o t t l e ) |
170 # | 171 # |
171 # Support for Chameleon templates (the kind TinCan uses by default). | 172 # Support for Chameleon templates (the kind TinCan uses by default). This |
173 # allows the usage of Chameleon with Bottle itself. It is here as a | |
174 # convenience for those using Bottle routes as well as TinCan webapps; | |
175 # this is NOT how Tincan uses Chameleon | |
172 | 176 |
173 class ChameleonTemplate(bottle.BaseTemplate): | 177 class ChameleonTemplate(bottle.BaseTemplate): |
174 def prepare(self, **options): | 178 def prepare(self, **options): |
175 from chameleon import PageTemplate, PageTemplateFile | |
176 if self.source: | 179 if self.source: |
177 self.tpl = PageTemplate(self.source, encoding=self.encoding, | 180 self.tpl = PageTemplate(self.source, encoding=self.encoding, |
178 **options) | 181 **options) |
179 else: | 182 else: |
180 self.tpl = PageTemplateFile(self.filename, encoding=self.encoding, | 183 self.tpl = PageTemplateFile(self.filename, encoding=self.encoding, |
187 _defaults.update(kwargs) | 190 _defaults.update(kwargs) |
188 return self.tpl.render(**_defaults) | 191 return self.tpl.render(**_defaults) |
189 | 192 |
190 chameleon_template = functools.partial(bottle.template, template_adapter=ChameleonTemplate) | 193 chameleon_template = functools.partial(bottle.template, template_adapter=ChameleonTemplate) |
191 chameleon_view = functools.partial(bottle.view, template_adapter=ChameleonTemplate) | 194 chameleon_view = functools.partial(bottle.view, template_adapter=ChameleonTemplate) |
195 | |
196 # C h a m e l e o n ( T i n c a n ) | |
197 # | |
198 # How we use Chameleon ourselves. Everything is loaded as a string, and we | |
199 # provide convenience routines to load other templates. | |
200 | |
201 class TinCanChameleon(PageTemplate): | |
202 """ | |
203 Basically, a standard PageTemplate with load and lload functionality. | |
204 """ | |
205 def __init__(self, body, base=None, subdir=None, **config): | |
206 super(TinCanChameleon, self).__init__(body, **config) | |
207 if base is not None: | |
208 encoding = config.get("encoding", "utf-8") | |
209 if subdir is not None: | |
210 self.expression_types['load'] = \ | |
211 _LoaderFactory(base, subdir, encoding) | |
212 self.expression_types['lload'] = \ | |
213 _LoaderFactory(os.path.join(base,_WINF,"tlib"), None, encoding) | |
214 | |
215 class _LoaderFactory(object): | |
216 """ | |
217 One of two helper classes for the above. | |
218 """ | |
219 def __init__(self, base, subdir, encoding): | |
220 self.base = base | |
221 self.subdir = subdir | |
222 self.encoding = encoding | |
223 | |
224 def __call__(self, string): | |
225 return _Loader(self, string) | |
226 | |
227 class _Loader(object): | |
228 """ | |
229 Two of two helper classes for the above. | |
230 """ | |
231 def __init__(self, based_on, string): | |
232 if not (string.endswith(".pspx") or string.endswith(".pt")): | |
233 raise ValueError("loaded templates must end in .pspx or .pt") | |
234 self.path = string | |
235 self.params = based_on | |
236 | |
237 def __call__(self, target, engine): | |
238 if self.params.subdir is None: | |
239 npath = os.path.join(self.params.base, self.path.lstrip('/')) | |
240 else: | |
241 try: | |
242 normalized = _normpath(self.params.subdir, self.path) | |
243 except IndexError: | |
244 raise ValueError("invalid path: {0!s}".format(self.path)) | |
245 npath = os.path.join(self.params.base, *normalized) | |
246 with open(npath, "r", encoding=self.params.encoding) as fp: | |
247 contents = fp.read() | |
248 value = ast.Str(contents) | |
249 return [ast.Assign(targets=[target], value=value)] | |
192 | 250 |
193 # U t i l i t i e s | 251 # U t i l i t i e s |
194 | 252 |
195 def _normpath(base, unsplit): | 253 def _normpath(base, unsplit): |
196 """ | 254 """ |
325 custom code-behind, only two variables are available to your template: | 383 custom code-behind, only two variables are available to your template: |
326 request (bottle.Request) and error (bottle.HTTPError). | 384 request (bottle.Request) and error (bottle.HTTPError). |
327 """ | 385 """ |
328 def __init__(self, template, klass): | 386 def __init__(self, template, klass): |
329 self._template = template | 387 self._template = template |
330 self._template.prepare() | |
331 self._class = klass | 388 self._class = klass |
332 | 389 |
333 def __call__(self, e): | 390 def __call__(self, e): |
334 bottle.request.environ[_FTYPE] = True | 391 bottle.request.environ[_FTYPE] = True |
335 try: | 392 try: |
336 obj = self._class(bottle.request, e) | 393 obj = self._class(bottle.request, e) |
337 obj.handle() | 394 obj.handle() |
338 return self._template.render(obj.export()).lstrip('\n') | 395 return self._template.render(**obj.export()).lstrip('\n') |
339 except bottle.HTTPResponse as e: | 396 except bottle.HTTPResponse as e: |
340 return e | 397 return e |
341 except Exception as e: | 398 except Exception as e: |
342 traceback.print_exc() | 399 traceback.print_exc() |
343 # Bottle doesn't allow error handlers to themselves cause | 400 # Bottle doesn't allow error handlers to themselves cause |
359 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) | 416 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) |
360 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) | 417 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) |
361 self._origin = self._urlpath | 418 self._origin = self._urlpath |
362 self._subdir = subdir | 419 self._subdir = subdir |
363 self._seen = set() | 420 self._seen = set() |
364 self._tclass = launcher.tclass | |
365 self._app = launcher.app | 421 self._app = launcher.app |
366 | 422 |
367 def launch(self): | 423 def launch(self): |
368 """ | 424 """ |
369 Launch a single page. | 425 Launch a single page. |
415 tfile = TemplateFile(tpath) | 471 tfile = TemplateFile(tpath) |
416 except OSError as e: | 472 except OSError as e: |
417 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e | 473 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e |
418 except IndexError as e: | 474 except IndexError as e: |
419 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e | 475 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e |
420 self._body = self._tclass(source=tfile.body) | 476 try: |
477 self._body = TinCanChameleon(tfile.body, base=self._fsroot, subdir=self._subdir) | |
478 except Exception as e: | |
479 raise TinCanError("{0}: template error: {1!s}".format(self._urlpath, e)) from e | |
421 else: | 480 else: |
422 self._body = self._tclass(source=self._template.body) | 481 try: |
423 self._body.prepare() | 482 self._body = TinCanChameleon(self._template.body, self._fsroot, subdir=self._subdir) |
483 except Exception as e: | |
484 raise TinCanError("{0}: template error: {1!s}".format(self._urlpath, e)) from e | |
424 # If this is an #errors page, register it as such. | 485 # If this is an #errors page, register it as such. |
425 if oheader.errors is not None: | 486 if oheader.errors is not None: |
426 self._mkerror(oheader.errors) | 487 self._mkerror(oheader.errors) |
427 return # this implies #hidden | 488 return # this implies #hidden |
428 # Get #methods for this route | 489 # Get #methods for this route |
444 errors = [ int(i) for i in rerrors.split() ] | 505 errors = [ int(i) for i in rerrors.split() ] |
445 except ValueError as e: | 506 except ValueError as e: |
446 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e | 507 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e |
447 if not errors: | 508 if not errors: |
448 errors = range(_ERRMIN, _ERRMAX+1) | 509 errors = range(_ERRMIN, _ERRMAX+1) |
449 route = _TinCanErrorRoute(self._tclass(source=self._template.body), self._class) | 510 try: |
511 template = TinCanChameleon(self._template.body, base=self._fsroot, subdir=self._subdir) | |
512 except Exception as e: | |
513 raise TinCanError("{0}: template error: {1!s}".format(self._urlpath, e)) from e | |
514 route = _TinCanErrorRoute(template, self._class) | |
450 for error in errors: | 515 for error in errors: |
451 if error < _ERRMIN or error > _ERRMAX: | 516 if error < _ERRMIN or error > _ERRMAX: |
452 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) | 517 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) |
453 self._app.error_handler[error] = route # XXX | 518 self._app.error_handler[error] = route # XXX |
454 | 519 |
557 """ | 622 """ |
558 target = None | 623 target = None |
559 try: | 624 try: |
560 obj = self._class(bottle.request, bottle.response) | 625 obj = self._class(bottle.request, bottle.response) |
561 obj.handle() | 626 obj.handle() |
562 return self._body.render(obj.export()).lstrip('\n') | 627 return self._body.render(**obj.export()).lstrip('\n') |
563 except ForwardException as fwd: | 628 except ForwardException as fwd: |
564 target = fwd.target | 629 target = fwd.target |
565 except bottle.HTTPResponse as e: | 630 except bottle.HTTPResponse as e: |
566 return e | 631 return e |
567 except Exception as e: | 632 except Exception as e: |
607 | 672 |
608 class _Launcher(object): | 673 class _Launcher(object): |
609 """ | 674 """ |
610 Helper class for launching webapps. | 675 Helper class for launching webapps. |
611 """ | 676 """ |
612 def __init__(self, fsroot, urlroot, tclass, logger): | 677 def __init__(self, fsroot, urlroot, logger): |
613 """ | 678 """ |
614 Lightweight constructor. The real action happens in .launch() below. | 679 Lightweight constructor. The real action happens in .launch() below. |
615 """ | 680 """ |
616 self.fsroot = fsroot | 681 self.fsroot = fsroot |
617 self.urlroot = urlroot | 682 self.urlroot = urlroot |
618 self.tclass = tclass | |
619 self.logger = logger | 683 self.logger = logger |
620 self.app = None | 684 self.app = None |
621 self.errors = 0 | 685 self.errors = 0 |
622 self.debug = False | 686 self.debug = False |
623 | 687 |
678 | 742 |
679 def _logger(message): | 743 def _logger(message): |
680 sys.stderr.write(message) | 744 sys.stderr.write(message) |
681 sys.stderr.write('\n') | 745 sys.stderr.write('\n') |
682 | 746 |
683 def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger): | 747 def launch(fsroot=None, urlroot='/', logger=_logger): |
684 """ | 748 """ |
685 Launch and return a TinCan webapp. Does not run the app; it is the | 749 Launch and return a TinCan webapp. Does not run the app; it is the |
686 caller's responsibility to call app.run() | 750 caller's responsibility to call app.run() |
687 """ | 751 """ |
688 if fsroot is None: | 752 if fsroot is None: |
689 fsroot = os.getcwd() | 753 fsroot = os.getcwd() |
690 launcher = _Launcher(fsroot, urlroot, tclass, logger) | 754 launcher = _Launcher(fsroot, urlroot, logger) |
691 # launcher.debug = True | 755 # launcher.debug = True |
692 launcher.launch() | 756 launcher.launch() |
693 return launcher.app, launcher.errors | 757 return launcher.app, launcher.errors |
694 | 758 |
695 # XXX - We cannot implement a command-line launcher here; see the | 759 # XXX - We cannot implement a command-line launcher here; see the |