Mercurial > cgi-bin > hgweb.cgi > tincan
comparison tincan.py @ 2:ca6f8ca38cf2 draft
Another backup commit.
author | David Barts <n5jrn@me.com> |
---|---|
date | Sun, 12 May 2019 22:51:34 -0700 |
parents | 94b36e721500 |
children | c6902cded64d |
comparison
equal
deleted
inserted
replaced
1:94b36e721500 | 2:ca6f8ca38cf2 |
---|---|
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 importlib, py_compile | 11 import importlib |
12 import io | 12 import io |
13 import py_compile | |
14 from stat import S_ISDIR, S_ISREG | |
13 | 15 |
14 import bottle | 16 import bottle |
15 | 17 |
16 # C l a s s e s | 18 # E x c e p t i o n s |
17 | |
18 # Exceptions | |
19 | 19 |
20 class TinCanException(Exception): | 20 class TinCanException(Exception): |
21 """ | 21 """ |
22 The parent class of all exceptions we raise. | 22 The parent class of all exceptions we raise. |
23 """ | 23 """ |
49 General-purpose exception thrown by TinCan when things go wrong, often | 49 General-purpose exception thrown by TinCan when things go wrong, often |
50 when attempting to launch webapps. | 50 when attempting to launch webapps. |
51 """ | 51 """ |
52 pass | 52 pass |
53 | 53 |
54 # T e m p l a t e s | |
55 # | |
54 # Template (.pspx) files. These are standard templates for a supported | 56 # Template (.pspx) files. These are standard templates for a supported |
55 # template engine, but with an optional set of header lines that begin | 57 # template engine, but with an optional set of header lines that begin |
56 # with '#'. | 58 # with '#'. |
57 | 59 |
58 class TemplateFile(object): | 60 class TemplateFile(object): |
99 | 101 |
100 class TemplateHeader(object): | 102 class TemplateHeader(object): |
101 """ | 103 """ |
102 Parses and represents a set of header lines. | 104 Parses and represents a set of header lines. |
103 """ | 105 """ |
104 _NAMES = [ "error", "forward", "methods", "python", "template" ] | 106 _NAMES = [ "errors", "forward", "methods", "python", "template" ] |
105 _FNAMES = [ "hidden" ] | 107 _FNAMES = [ "hidden" ] |
106 | 108 |
107 def __init__(self, string): | 109 def __init__(self, string): |
108 # Initialize our state | 110 # Initialize our state |
109 for i in self._NAMES: | 111 for i in self._NAMES: |
146 param = ast.literal_eval(param) | 148 param = ast.literal_eval(param) |
147 break | 149 break |
148 # Update this object | 150 # Update this object |
149 setattr(self, name, param) | 151 setattr(self, name, param) |
150 | 152 |
153 # C h a m e l e o n | |
154 # | |
151 # Support for Chameleon templates (the kind TinCan uses by default). | 155 # Support for Chameleon templates (the kind TinCan uses by default). |
152 | 156 |
153 class ChameleonTemplate(bottle.BaseTemplate): | 157 class ChameleonTemplate(bottle.BaseTemplate): |
154 def prepare(self, **options): | 158 def prepare(self, **options): |
155 from chameleon import PageTemplate, PageTemplateFile | 159 from chameleon import PageTemplate, PageTemplateFile |
168 return self.tpl.render(**_defaults) | 172 return self.tpl.render(**_defaults) |
169 | 173 |
170 chameleon_template = functools.partial(template, template_adapter=ChameleonTemplate) | 174 chameleon_template = functools.partial(template, template_adapter=ChameleonTemplate) |
171 chameleon_view = functools.partial(view, template_adapter=ChameleonTemplate) | 175 chameleon_view = functools.partial(view, template_adapter=ChameleonTemplate) |
172 | 176 |
173 # Utility functions, used in various places. | 177 # U t i l i t i e s |
174 | 178 |
175 def _normpath(base, unsplit): | 179 def _normpath(base, unsplit): |
176 """ | 180 """ |
177 Split, normalize and ensure a possibly relative path is absolute. First | 181 Split, normalize and ensure a possibly relative path is absolute. First |
178 argument is a list of directory names, defining a base. Second | 182 argument is a list of directory names, defining a base. Second |
221 exc = ForwardException('/' + '/'.join(_normpath(base, target))) | 225 exc = ForwardException('/' + '/'.join(_normpath(base, target))) |
222 except IndexError as e: | 226 except IndexError as e: |
223 raise TinCanError("{0}: invalid forward to {1!r}".format(source, target)) from e | 227 raise TinCanError("{0}: invalid forward to {1!r}".format(source, target)) from e |
224 raise exc | 228 raise exc |
225 | 229 |
230 # C o d e B e h i n d | |
231 # | |
226 # Represents the code-behind of one of our pages. This gets subclassed, of | 232 # Represents the code-behind of one of our pages. This gets subclassed, of |
227 # course. | 233 # course. |
228 | 234 |
229 class Page(object): | 235 class Page(object): |
230 # Non-private things we refuse to export anyhow. | 236 # Non-private things we refuse to export anyhow. |
259 value = getattr(self, name) | 265 value = getattr(self, name) |
260 if callable(value): | 266 if callable(value): |
261 continue | 267 continue |
262 ret[name] = value | 268 ret[name] = value |
263 | 269 |
270 # R o u t e s | |
271 # | |
264 # Represents a route in TinCan. Our launcher creates these on-the-fly based | 272 # Represents a route in TinCan. Our launcher creates these on-the-fly based |
265 # on the files it finds. | 273 # on the files it finds. |
266 | 274 |
267 EXTENSION = ".pspx" | 275 _ERRMIN = 400 |
268 CONTENT = "text/html" | 276 _ERRMAX = 599 |
269 _WINF = "WEB-INF" | 277 _PEXTEN = ".py" |
270 _BANNED = set([_WINF]) | 278 _TEXTEN = ".pspx" |
271 _CODING = "utf-8" | |
272 _CLASS = "Page" | |
273 _FLOOP = "tincan.forwards" | 279 _FLOOP = "tincan.forwards" |
274 _FORIG = "tincan.origin" | 280 _FORIG = "tincan.origin" |
275 | 281 |
276 class TinCanErrorRoute(object): | 282 class TinCanErrorRoute(object): |
277 """ | 283 """ |
290 class TinCanRoute(object): | 296 class TinCanRoute(object): |
291 """ | 297 """ |
292 A route created by the TinCan launcher. | 298 A route created by the TinCan launcher. |
293 """ | 299 """ |
294 def __init__(self, launcher, name, subdir): | 300 def __init__(self, launcher, name, subdir): |
295 self._plib = launcher.plib | |
296 self._fsroot = launcher.fsroot | 301 self._fsroot = launcher.fsroot |
297 self._urlroot = launcher.urlroot | 302 self._urlroot = launcher.urlroot |
298 self._name = name | 303 self._name = name |
299 self._python = name + ".py" | 304 self._python = name + _PEXTEN |
300 self._content = CONTENT | 305 self._content = CONTENT |
301 self._fspath = os.path.join(launcher.fsroot, *subdir, name + EXTENSION) | 306 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) |
302 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + EXTENSION) | 307 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) |
303 self._origin = self._urlpath | 308 self._origin = self._urlpath |
304 self._subdir = subdir | 309 self._subdir = subdir |
305 self._seen = set() | 310 self._seen = set() |
306 self._class = None | |
307 self._tclass = launcher.tclass | 311 self._tclass = launcher.tclass |
308 self._app = launcher.app | 312 self._app = launcher.app |
309 | 313 |
310 def launch(self, config): | 314 def launch(self): |
311 """ | 315 """ |
312 Launch a single page. | 316 Launch a single page. |
313 """ | 317 """ |
314 # Build master and header objects, process #forward directives | 318 # Build master and header objects, process #forward directives |
315 hidden = None | 319 hidden = None |
324 # If this is a hidden page, we ignore it for now, since hidden pages | 328 # If this is a hidden page, we ignore it for now, since hidden pages |
325 # don't get routes made for them. | 329 # don't get routes made for them. |
326 if hidden: | 330 if hidden: |
327 return | 331 return |
328 # If this is an error page, register it as such. | 332 # If this is an error page, register it as such. |
329 if self._header.error is not None: | 333 if self._header.errors is not None: |
334 if self._header.template is not None: | |
335 tpath = os.path.join(self._fsroot, *self._splitpath(self._header.template)) | |
336 self._template = | |
330 try: | 337 try: |
331 errors = [ int(i) for i in self._header.error.split() ] | 338 errors = [ int(i) for i in self._header.errors.split() ] |
332 except ValueError as e: | 339 except ValueError as e: |
333 raise TinCanError("{0}: bad #error line".format(self._urlpath)) from e | 340 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e |
334 if not errors: | 341 if not errors: |
335 errors = range(400, 600) | 342 errors = range(_ERRMIN, _ERRMAX+1) |
336 route = TinCanErrorRoute(self._tclass(source=self._template.body)) | 343 route = TinCanErrorRoute(self._tclass(source=self._template.body)) |
337 for error in errors: | 344 for error in errors: |
345 if error < _ERRMIN or error > _ERRMAX: | |
346 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) | |
338 self._app.error(code=error, callback=route) | 347 self._app.error(code=error, callback=route) |
339 return # this implies #hidden | 348 return # this implies #hidden |
340 # Get methods for this route | 349 # Get methods for this route |
341 if self._header.methods is None: | 350 if self._header.methods is None: |
342 methods = [ 'GET' ] | 351 methods = [ 'GET' ] |
343 else: | 352 else: |
344 methods = [ i.upper() for i in self._header.methods.split() ] | 353 methods = [ i.upper() for i in self._header.methods.split() ] |
345 # Process other header entries | 354 # Process other header entries |
346 if self._header.python is not None: | 355 if self._header.python is not None: |
347 if not self._header.python.endswith('.py'): | 356 if not self._header.python.endswith(_PEXTEN): |
348 raise TinCanError("{0}: #python files must end in .py", self._urlpath) | 357 raise TinCanError("{0}: #python files must end in {1}", self._urlpath, _PEXTEN) |
349 self._python = self._header.python | 358 self._python = self._header.python |
350 # Obtain a class object by importing and introspecting a module. | 359 # Obtain a class object by importing and introspecting a module. |
351 pypath = os.path.normpath(os.path.join(self._fsroot, *self._subdir, self._python)) | 360 pypath = os.path.normpath(os.path.join(self._fsroot, *self._subdir, self._python)) |
352 pycpath = pypath + 'c' | 361 pycpath = pypath + 'c' |
353 try: | 362 try: |
358 try: | 367 try: |
359 py_compile.compile(pypath, cfile=pycpath) | 368 py_compile.compile(pypath, cfile=pycpath) |
360 except Exception as e: | 369 except Exception as e: |
361 raise TinCanError("{0}: error compiling".format(pypath)) from e | 370 raise TinCanError("{0}: error compiling".format(pypath)) from e |
362 try: | 371 try: |
363 self._mangled = self._manage_module() | |
364 spec = importlib.util.spec_from_file_location(_mangle(self._name), cpath) | 372 spec = importlib.util.spec_from_file_location(_mangle(self._name), cpath) |
365 mod = importlib.util.module_from_spec(spec) | 373 mod = importlib.util.module_from_spec(spec) |
366 spec.loader.exec_module(mod) | 374 spec.loader.exec_module(mod) |
367 except Exception as e: | 375 except Exception as e: |
368 raise TinCanError("{0}: error importing".format(pycpath)) from e | 376 raise TinCanError("{0}: error importing".format(pycpath)) from e |
396 rlist = self._splitpath(self._header.forward) | 404 rlist = self._splitpath(self._header.forward) |
397 rname = rlist.pop() | 405 rname = rlist.pop() |
398 except IndexError as e: | 406 except IndexError as e: |
399 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e | 407 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e |
400 name, ext = os.path.splitext(rname)[1] | 408 name, ext = os.path.splitext(rname)[1] |
401 if ext != EXTENSION: | 409 if ext != _TEXTEN: |
402 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) | 410 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) |
403 self._subdir = rlist | 411 self._subdir = rlist |
404 self._python = name + ".py" | 412 self._python = name + _PEXTEN |
405 self._fspath = os.path.join(self._fsroot, *self._subdir, rname) | 413 self._fspath = os.path.join(self._fsroot, *self._subdir, rname) |
406 self._urlpath = self._urljoin(*self._subdir, rname) | 414 self._urlpath = self._urljoin(*self._subdir, rname) |
407 | 415 |
408 def _urljoin(self, *args): | 416 def _urljoin(self, *args): |
409 args = list(args) | 417 args = list(args) |
414 def __call__(self): | 422 def __call__(self): |
415 """ | 423 """ |
416 This gets called by the framework AFTER the page is launched. | 424 This gets called by the framework AFTER the page is launched. |
417 """ | 425 """ |
418 target = None | 426 target = None |
427 obj = self._class(bottle.request, bottle.response) | |
419 try: | 428 try: |
420 obj = self._class(bottle.request, bottle.response) | |
421 obj.handle() | 429 obj.handle() |
422 return self._body.render(obj.export()) | 430 return self._body.render(obj.export()).lstrip('\n') |
423 except ForwardException as fwd: | 431 except ForwardException as fwd: |
424 target = fwd.target | 432 target = fwd.target |
425 if target is None: | 433 if target is None: |
426 raise TinCanError("Unexpected null target!") | 434 raise TinCanError("Unexpected null target!") |
427 # We get here if we are doing a server-side programmatic | 435 # We get here if we are doing a server-side programmatic |
428 # forward. | 436 # forward. |
429 environ = bottle.request.environ | 437 environ = bottle.request.environ |
430 if _FLOOP not in environ: | |
431 environ[_FLOOP] = set() | |
432 if _FORIG not in environ: | 438 if _FORIG not in environ: |
433 environ[_FORIG] = self._urlpath | 439 environ[_FORIG] = self._urlpath |
440 if _FLOOP not in environ: | |
441 environ[_FLOOP] = set([self._urlpath]) | |
434 elif target in environ[_FLOOP]: | 442 elif target in environ[_FLOOP]: |
435 TinCanError("{0}: forward loop detected".format(environ[_FORIG])) | 443 TinCanError("{0}: forward loop detected".format(environ[_FORIG])) |
436 environ[_FLOOP].add(target) | 444 environ[_FLOOP].add(target) |
437 environ['bottle.raw_path'] = target | 445 environ['bottle.raw_path'] = target |
438 environ['PATH_INFO'] = urllib.parse.quote(target) | 446 environ['PATH_INFO'] = urllib.parse.quote(target) |
448 continue | 456 continue |
449 value = getattr(obj, name) | 457 value = getattr(obj, name) |
450 if not callable(value): | 458 if not callable(value): |
451 ret[name] = value | 459 ret[name] = value |
452 return ret | 460 return ret |
461 | |
462 # L a u n c h e r | |
463 | |
464 _WINF = "WEB-INF" | |
465 _BANNED = set([_WINF]) | |
466 | |
467 class _Launcher(object): | |
468 """ | |
469 Helper class for launching webapps. | |
470 """ | |
471 def __init__(self, fsroot, urlroot, tclass): | |
472 """ | |
473 Lightweight constructor. The real action happens in .launch() below. | |
474 """ | |
475 self.fsroot = fsroot | |
476 self.urlroot = urlroot | |
477 self.tclass = tclass | |
478 self.app = None | |
479 | |
480 def launch(self): | |
481 """ | |
482 Does the actual work of launching something. XXX - modifies sys.path | |
483 and never un-modifies it. | |
484 """ | |
485 # Sanity checks | |
486 if not self.urlroot.startswith("/"): | |
487 raise TinCanError("urlroot must be absolute") | |
488 if not os.path.isdir(self.fsroot): | |
489 raise TinCanError("no such directory: {0!r}".format(self.fsroot)) | |
490 # Make WEB-INF, if needed | |
491 winf = os.path.join(self.fsroot, _WINF) | |
492 lib = os.path.join(winf, "lib") | |
493 for i in [ winf, lib ]: | |
494 if not os.path.isdir(i): | |
495 os.mkdir(i) | |
496 # Add our private lib directory to sys.path | |
497 sys.path.insert(1, os.path.abspath(lib)) | |
498 # Do what we gotta do | |
499 self.app = TinCan() | |
500 self._launch([]) | |
501 return self.app | |
502 | |
503 def _launch(self, subdir): | |
504 for entry in os.listdir(os.path.join(self.fsroot, *subdir)): | |
505 if not subdir and entry in _BANNED: | |
506 continue | |
507 etype = os.stat(os.path.join(self.fsroot, *subdir, entry)).st_mode | |
508 if S_ISREG(etype): | |
509 ename, eext = os.path.splitext(entry) | |
510 if eext != _TEXTEN: | |
511 continue # only look at interesting files | |
512 route = TinCanRoute(self, ename, subdir) | |
513 page.launch() | |
514 elif S_ISDIR(etype): | |
515 self._launch(subdir + [entry]) | |
516 | |
517 def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate): | |
518 """ | |
519 Launch and return a TinCan webapp. Does not run the app; it is the | |
520 caller's responsibility to call app.run() | |
521 """ | |
522 if fsroot is None: | |
523 fsroot = os.getcwd() | |
524 return _Launcher(fsroot, urlroot, tclass).launch() |