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.