comparison tincan.py @ 4:0d47859f792a draft

Finally got "hello, world" working. Still likely many bugs.
author David Barts <n5jrn@me.com>
date Mon, 13 May 2019 12:38:26 -0700
parents c6902cded64d
children 31bb8400e6e3
comparison
equal deleted inserted replaced
3:c6902cded64d 4:0d47859f792a
6 6
7 import os, sys 7 import os, sys
8 import ast 8 import ast
9 import binascii 9 import binascii
10 from base64 import b16encode, b16decode 10 from base64 import b16encode, b16decode
11 import functools
11 import importlib 12 import importlib
13 from inspect import isclass
12 import io 14 import io
13 import py_compile 15 import py_compile
14 from stat import S_ISDIR, S_ISREG 16 from stat import S_ISDIR, S_ISREG
15 17
16 import bottle 18 import bottle
31 super().__init__(message, line) 33 super().__init__(message, line)
32 self.message = message 34 self.message = message
33 self.line = line 35 self.line = line
34 36
35 def __str__(self): 37 def __str__(self):
36 return "Line {0}: {1}".format(self.line, self.message) 38 return "line {0}: {1}".format(self.line, self.message)
37 39
38 class ForwardException(TinCanException): 40 class ForwardException(TinCanException):
39 """ 41 """
40 Raised to effect the flow control needed to do a forward (server-side 42 Raised to effect the flow control needed to do a forward (server-side
41 redirect). It is ugly to do this, but other Python frameworks do and 43 redirect). It is ugly to do this, but other Python frameworks do and
156 158
157 class ChameleonTemplate(bottle.BaseTemplate): 159 class ChameleonTemplate(bottle.BaseTemplate):
158 def prepare(self, **options): 160 def prepare(self, **options):
159 from chameleon import PageTemplate, PageTemplateFile 161 from chameleon import PageTemplate, PageTemplateFile
160 if self.source: 162 if self.source:
161 self.tpl = chameleon.PageTemplate(self.source, 163 self.tpl = PageTemplate(self.source, encoding=self.encoding,
162 encoding=self.encoding, **options) 164 **options)
163 else: 165 else:
164 self.tpl = chameleon.PageTemplateFile(self.filename, 166 self.tpl = PageTemplateFile(self.filename, encoding=self.encoding,
165 encoding=self.encoding, search_path=self.lookup, **options) 167 search_path=self.lookup, **options)
166 168
167 def render(self, *args, **kwargs): 169 def render(self, *args, **kwargs):
168 for dictarg in args: 170 for dictarg in args:
169 kwargs.update(dictarg) 171 kwargs.update(dictarg)
170 _defaults = self.defaults.copy() 172 _defaults = self.defaults.copy()
171 _defaults.update(kwargs) 173 _defaults.update(kwargs)
172 return self.tpl.render(**_defaults) 174 return self.tpl.render(**_defaults)
173 175
174 chameleon_template = functools.partial(template, template_adapter=ChameleonTemplate) 176 chameleon_template = functools.partial(bottle.template, template_adapter=ChameleonTemplate)
175 chameleon_view = functools.partial(view, template_adapter=ChameleonTemplate) 177 chameleon_view = functools.partial(bottle.view, template_adapter=ChameleonTemplate)
176 178
177 # U t i l i t i e s 179 # U t i l i t i e s
178 180
179 def _normpath(base, unsplit): 181 def _normpath(base, unsplit):
180 """ 182 """
264 continue 266 continue
265 value = getattr(self, name) 267 value = getattr(self, name)
266 if callable(value): 268 if callable(value):
267 continue 269 continue
268 ret[name] = value 270 ret[name] = value
271 return ret
269 272
270 # R o u t e s 273 # R o u t e s
271 # 274 #
272 # Represents a route in TinCan. Our launcher creates these on-the-fly based 275 # Represents a route in TinCan. Our launcher creates these on-the-fly based
273 # on the files it finds. 276 # on the files it finds.
277 _PEXTEN = ".py" 280 _PEXTEN = ".py"
278 _TEXTEN = ".pspx" 281 _TEXTEN = ".pspx"
279 _FLOOP = "tincan.forwards" 282 _FLOOP = "tincan.forwards"
280 _FORIG = "tincan.origin" 283 _FORIG = "tincan.origin"
281 284
282 class TinCanErrorRoute(object): 285 class _TinCanErrorRoute(object):
283 """ 286 """
284 A route to an error page. These never have code-behind, don't get 287 A route to an error page. These never have code-behind, don't get
285 routes created for them, and are only reached if an error routes them 288 routes created for them, and are only reached if an error routes them
286 there. Error templates only have two variables available: e (the 289 there. Error templates only have two variables available: e (the
287 HTTPError object associated with the error) and request. 290 HTTPError object associated with the error) and request.
291 self._template.prepare() 294 self._template.prepare()
292 295
293 def __call__(self, e): 296 def __call__(self, e):
294 return self._template.render(e=e, request=bottle.request).lstrip('\n') 297 return self._template.render(e=e, request=bottle.request).lstrip('\n')
295 298
296 class TinCanRoute(object): 299 class _TinCanRoute(object):
297 """ 300 """
298 A route created by the TinCan launcher. 301 A route created by the TinCan launcher.
299 """ 302 """
300 def __init__(self, launcher, name, subdir): 303 def __init__(self, launcher, name, subdir):
301 self._fsroot = launcher.fsroot 304 self._fsroot = launcher.fsroot
302 self._urlroot = launcher.urlroot 305 self._urlroot = launcher.urlroot
303 self._name = name 306 self._name = name
304 self._python = name + _PEXTEN 307 self._python = name + _PEXTEN
305 self._content = CONTENT
306 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) 308 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN)
307 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) 309 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN)
308 self._origin = self._urlpath 310 self._origin = self._urlpath
309 self._subdir = subdir 311 self._subdir = subdir
310 self._seen = set() 312 self._seen = set()
317 """ 319 """
318 # Build master and header objects, process #forward directives 320 # Build master and header objects, process #forward directives
319 hidden = None 321 hidden = None
320 while True: 322 while True:
321 self._template = TemplateFile(self._fspath) 323 self._template = TemplateFile(self._fspath)
322 self._header = TemplateHeader(self._template.header) 324 try:
325 self._header = TemplateHeader(self._template.header)
326 except TemplateHeaderException as e:
327 raise TinCanError("{0}: {1!s}".format(self._fspath, e)) from e
323 if hidden is None: 328 if hidden is None:
324 if self._header.errors is not None: 329 if self._header.errors is not None:
325 break 330 break
326 hidden = self._header.hidden 331 hidden = self._header.hidden
327 elif self._header.errors is not None: 332 elif self._header.errors is not None:
328 raise TinCanError("{0}: #forward to #errors not allowed".format(self._origin)) 333 raise TinCanError("{0}: #forward to #errors not allowed".format(self._origin))
329 if self._header.forward is None: 334 if self._header.forward is None:
330 break 335 break
331 self._redirect() 336 self._redirect()
332 # If this is a hidden page, we ignore it for now, since hidden pages 337 # If this is a #hidden page, we ignore it for now, since hidden pages
333 # don't get routes made for them. 338 # don't get routes made for them.
334 if hidden: 339 if hidden:
335 return 340 return
336 # If this is an error page, register it as such. 341 # If this is an #errors page, register it as such.
337 if self._header.errors is not None: 342 if self._header.errors is not None:
338 self._mkerror() 343 self._mkerror()
339 return # this implies #hidden 344 return # this implies #hidden
340 # Get methods for this route 345 # Get #methods for this route
341 if self._header.methods is None: 346 if self._header.methods is None:
342 methods = [ 'GET' ] 347 methods = [ 'GET' ]
343 else: 348 else:
344 methods = [ i.upper() for i in self._header.methods.split() ] 349 methods = [ i.upper() for i in self._header.methods.split() ]
345 if not methods: 350 if not methods:
346 raise TinCanError("{0}: no #methods specified".format(self._urlpath)) 351 raise TinCanError("{0}: no #methods specified".format(self._urlpath))
347 # Process other header entries 352 # Get the code-behind #python
348 if self._header.python is not None: 353 if self._header.python is not None:
349 if not self._header.python.endswith(_PEXTEN): 354 if not self._header.python.endswith(_PEXTEN):
350 raise TinCanError("{0}: #python files must end in {1}", self._urlpath, _PEXTEN) 355 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN))
351 self._python = self._header.python 356 self._python = self._header.python
352 # Obtain a class object by importing and introspecting a module. 357 # Obtain a class object by importing and introspecting a module.
353 self._getclass() 358 self._getclass()
354 # Build body object (Chameleon template) 359 # Build body object (#template)
355 if self._header.template is not None: 360 if self._header.template is not None:
356 if not self._header.template.endswith(_TEXTEN): 361 if not self._header.template.endswith(_TEXTEN):
357 raise TinCanError("{0}: #template files must end in {1}", self._urlpath, _TEXTEN) 362 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN))
358 tpath = os.path.join(self._fsroot, *self._splitpath(self._header.template)) 363 tpath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._header.template)))
359 tfile = TemplateFile(tpath) 364 tfile = TemplateFile(tpath)
360 self._body = self._tclass(source=tfile.body) 365 self._body = self._tclass(source=tfile.body)
361 else: 366 else:
362 self._body = self._tclass(source=self._template.body) 367 self._body = self._tclass(source=self._template.body)
363 self._body.prepare() 368 self._body.prepare()
373 errors = [ int(i) for i in self._header.errors.split() ] 378 errors = [ int(i) for i in self._header.errors.split() ]
374 except ValueError as e: 379 except ValueError as e:
375 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e 380 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e
376 if not errors: 381 if not errors:
377 errors = range(_ERRMIN, _ERRMAX+1) 382 errors = range(_ERRMIN, _ERRMAX+1)
378 route = TinCanErrorRoute(self._tclass(source=self._template.body)) 383 route = _TinCanErrorRoute(self._tclass(source=self._template.body))
379 for error in errors: 384 for error in errors:
380 if error < _ERRMIN or error > _ERRMAX: 385 if error < _ERRMIN or error > _ERRMAX:
381 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) 386 raise TinCanError("{0}: bad #errors code".format(self._urlpath))
382 self._app.error(code=error, callback=route) 387 self._app.error(code=error, callback=route)
383 388
384 def _getclass(self): 389 def _getclass(self):
385 pypath = os.path.normpath(os.path.join(self._fsroot, *self._subdir, self._python)) 390 pypath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._python)))
386 pycpath = pypath + 'c' 391 pycpath = pypath + 'c'
387 try: 392 try:
388 pyctime = os.stat(pycpath).st_mtime 393 pyctime = os.stat(pycpath).st_mtime
389 except OSError: 394 except OSError:
390 pyctime = 0 395 pyctime = 0
391 if pyctime < os.stat(pypath).st_mtime: 396 try:
392 try: 397 if pyctime < os.stat(pypath).st_mtime:
393 py_compile.compile(pypath, cfile=pycpath) 398 py_compile.compile(pypath, cfile=pycpath, doraise=True)
394 except Exception as e: 399 except py_compile.PyCompileError as e:
395 raise TinCanError("{0}: error compiling".format(pypath)) from e 400 raise TinCanError(str(e)) from e
396 try: 401 except Exception as e:
397 spec = importlib.util.spec_from_file_location(_mangle(self._name), cpath) 402 raise TinCanError("{0}: {1!s}".format(pypath, e)) from e
403 try:
404 spec = importlib.util.spec_from_file_location(_mangle(self._name), pycpath)
398 mod = importlib.util.module_from_spec(spec) 405 mod = importlib.util.module_from_spec(spec)
399 spec.loader.exec_module(mod) 406 spec.loader.exec_module(mod)
400 except Exception as e: 407 except Exception as e:
401 raise TinCanError("{0}: error importing".format(pycpath)) from e 408 raise TinCanError("{0}: error importing".format(pycpath)) from e
402 self._class = None 409 self._class = None
403 for i in dir(mod): 410 for i in dir(mod):
404 v = getattr(mod, i) 411 v = getattr(mod, i)
405 if issubclass(v, Page): 412 if isclass(v) and issubclass(v, Page):
406 if self._class is not None: 413 if self._class is not None:
407 raise TinCanError("{0}: contains multiple Page classes", pypath) 414 raise TinCanError("{0}: contains multiple Page classes".format(pypath))
408 self._class = v 415 self._class = v
409 if self._class is None: 416 if self._class is None:
410 raise TinCanError("{0}: contains no Page classes", pypath) 417 raise TinCanError("{0}: contains no Page classes".format(pypath))
411 418
412 def _redirect(self): 419 def _redirect(self):
413 if self._header.forward in self._seen:
414 raise TinCanError("{0}: #forward loop".format(self._origin))
415 self._seen.add(self._header.forward)
416 try: 420 try:
417 rlist = self._splitpath(self._header.forward) 421 rlist = self._splitpath(self._header.forward)
422 forw = '/' + '/'.join(rlist)
423 if forw in self.seen:
424 raise TinCanError("{0}: #forward loop".format(self._origin))
425 self._seen.add(forw)
418 rname = rlist.pop() 426 rname = rlist.pop()
419 except IndexError as e: 427 except IndexError as e:
420 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e 428 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e
421 name, ext = os.path.splitext(rname)[1] 429 name, ext = os.path.splitext(rname)[1]
422 if ext != _TEXTEN: 430 if ext != _TEXTEN:
442 obj.handle() 450 obj.handle()
443 return self._body.render(obj.export()).lstrip('\n') 451 return self._body.render(obj.export()).lstrip('\n')
444 except ForwardException as fwd: 452 except ForwardException as fwd:
445 target = fwd.target 453 target = fwd.target
446 if target is None: 454 if target is None:
447 raise TinCanError("Unexpected null target!") 455 raise TinCanError("{0}: unexpected null target".format(self._urlpath))
448 # We get here if we are doing a server-side programmatic 456 # We get here if we are doing a server-side programmatic
449 # forward. 457 # forward.
450 environ = bottle.request.environ 458 environ = bottle.request.environ
451 if _FORIG not in environ: 459 if _FORIG not in environ:
452 environ[_FORIG] = self._urlpath 460 environ[_FORIG] = self._urlpath
453 if _FLOOP not in environ: 461 if _FLOOP not in environ:
454 environ[_FLOOP] = set([self._urlpath]) 462 environ[_FLOOP] = set([self._urlpath])
455 elif target in environ[_FLOOP]: 463 elif target in environ[_FLOOP]:
456 TinCanError("{0}: forward loop detected".format(environ[_FORIG])) 464 raise TinCanError("{0}: forward loop detected".format(environ[_FORIG]))
457 environ[_FLOOP].add(target) 465 environ[_FLOOP].add(target)
458 environ['bottle.raw_path'] = target 466 environ['bottle.raw_path'] = target
459 environ['PATH_INFO'] = urllib.parse.quote(target) 467 environ['PATH_INFO'] = urllib.parse.quote(target)
460 route, args = self._app.router.match(environ) 468 route, args = self._app.router.match(environ)
461 environ['route.handle'] = environ['bottle.route'] = route 469 environ['route.handle'] = environ['bottle.route'] = route
479 487
480 class _Launcher(object): 488 class _Launcher(object):
481 """ 489 """
482 Helper class for launching webapps. 490 Helper class for launching webapps.
483 """ 491 """
484 def __init__(self, fsroot, urlroot, tclass): 492 def __init__(self, fsroot, urlroot, tclass, logger):
485 """ 493 """
486 Lightweight constructor. The real action happens in .launch() below. 494 Lightweight constructor. The real action happens in .launch() below.
487 """ 495 """
488 self.fsroot = fsroot 496 self.fsroot = fsroot
489 self.urlroot = urlroot 497 self.urlroot = urlroot
490 self.tclass = tclass 498 self.tclass = tclass
499 self.logger = logger
491 self.app = None 500 self.app = None
501 self.errors = 0
502 self.debug = False
492 503
493 def launch(self): 504 def launch(self):
494 """ 505 """
495 Does the actual work of launching something. XXX - modifies sys.path 506 Does the actual work of launching something. XXX - modifies sys.path
496 and never un-modifies it. 507 and never un-modifies it.
509 # Add our private lib directory to sys.path 520 # Add our private lib directory to sys.path
510 sys.path.insert(1, os.path.abspath(lib)) 521 sys.path.insert(1, os.path.abspath(lib))
511 # Do what we gotta do 522 # Do what we gotta do
512 self.app = TinCan() 523 self.app = TinCan()
513 self._launch([]) 524 self._launch([])
514 return self.app 525 return self
515 526
516 def _launch(self, subdir): 527 def _launch(self, subdir):
517 for entry in os.listdir(os.path.join(self.fsroot, *subdir)): 528 for entry in os.listdir(os.path.join(self.fsroot, *subdir)):
518 if not subdir and entry in _BANNED: 529 if not subdir and entry in _BANNED:
519 continue 530 continue
520 etype = os.stat(os.path.join(self.fsroot, *subdir, entry)).st_mode 531 etype = os.stat(os.path.join(self.fsroot, *subdir, entry)).st_mode
521 if S_ISREG(etype): 532 if S_ISREG(etype):
522 ename, eext = os.path.splitext(entry) 533 ename, eext = os.path.splitext(entry)
523 if eext != _TEXTEN: 534 if eext != _TEXTEN:
524 continue # only look at interesting files 535 continue # only look at interesting files
525 route = TinCanRoute(self, ename, subdir) 536 route = _TinCanRoute(self, ename, subdir)
526 page.launch() 537 try:
538 route.launch()
539 except TinCanError as e:
540 self.logger(str(e))
541 if self.debug:
542 while e.__cause__ != None:
543 e = e.__cause__
544 self.logger("\t{0}: {1!s}".format(e.__class__.__name__, e))
545 self.errors += 1
527 elif S_ISDIR(etype): 546 elif S_ISDIR(etype):
528 self._launch(subdir + [entry]) 547 self._launch(subdir + [entry])
529 548
530 def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate): 549 def _logger(message):
550 sys.stderr.write(message)
551 sys.stderr.write('\n')
552
553 def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger):
531 """ 554 """
532 Launch and return a TinCan webapp. Does not run the app; it is the 555 Launch and return a TinCan webapp. Does not run the app; it is the
533 caller's responsibility to call app.run() 556 caller's responsibility to call app.run()
534 """ 557 """
535 if fsroot is None: 558 if fsroot is None:
536 fsroot = os.getcwd() 559 fsroot = os.getcwd()
537 return _Launcher(fsroot, urlroot, tclass).launch() 560 launcher = _Launcher(fsroot, urlroot, tclass, logger)
561 # launcher.debug = True
562 launcher.launch()
563 return launcher.app, launcher.errors
564
565 # XXX - We cannot implement a command-line launcher here; see the
566 # launcher script for why.