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()