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