Mercurial > cgi-bin > hgweb.cgi > tincan
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. |