# HG changeset patch # User David Barts # Date 1557776306 25200 # Node ID 0d47859f792a86d848ee4eca6c508dab69f9c3b8 # Parent c6902cded64d3340b6a957ae18110eb0c7813433 Finally got "hello, world" working. Still likely many bugs. diff -r c6902cded64d -r 0d47859f792a launch --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/launch Mon May 13 12:38:26 2019 -0700 @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +# XXX - This code must not be in tincan.py, because the built-in class +# loader will then confuse __main__.Page and tincan.Page, and fail to +# locate the code-behind. + +# I m p o r t s + +import os, sys +from argparse import ArgumentParser +from tincan import launch + +# V a r i a b l e s + +MYNAME = os.path.basename(sys.argv[0]) + +# M a i n P r o g r a m + +parser = ArgumentParser(prog=sys.argv[0], usage="%(prog)s [options] [directory [path]]") +opt = parser.add_argument +opt("-b", "--bind", default="localhost", help="address to bind to") +opt("-p", "--port", default=8080, help="port to listen on") +opt("directory", default=".", help="directory to serve", nargs='?') +opt("path", default="/", help="URL path to serve", nargs='?') +args = parser.parse_args(sys.argv[1:]) +app, errors = launch(fsroot=args.directory, urlroot=args.path) +if errors: + sys.stderr.write("{0}: {1} error{2} detected, aborting\n".format( + sys.argv[0], errors, "" if errors == 1 else "s")) + sys.exit(1) +app.run(host=args.bind, port=args.port) diff -r c6902cded64d -r 0d47859f792a tincan.py --- a/tincan.py Mon May 13 06:53:08 2019 -0700 +++ b/tincan.py Mon May 13 12:38:26 2019 -0700 @@ -8,7 +8,9 @@ import ast import binascii from base64 import b16encode, b16decode +import functools import importlib +from inspect import isclass import io import py_compile from stat import S_ISDIR, S_ISREG @@ -33,7 +35,7 @@ self.line = line def __str__(self): - return "Line {0}: {1}".format(self.line, self.message) + return "line {0}: {1}".format(self.line, self.message) class ForwardException(TinCanException): """ @@ -158,11 +160,11 @@ def prepare(self, **options): from chameleon import PageTemplate, PageTemplateFile if self.source: - self.tpl = chameleon.PageTemplate(self.source, - encoding=self.encoding, **options) + self.tpl = PageTemplate(self.source, encoding=self.encoding, + **options) else: - self.tpl = chameleon.PageTemplateFile(self.filename, - encoding=self.encoding, search_path=self.lookup, **options) + self.tpl = PageTemplateFile(self.filename, encoding=self.encoding, + search_path=self.lookup, **options) def render(self, *args, **kwargs): for dictarg in args: @@ -171,8 +173,8 @@ _defaults.update(kwargs) return self.tpl.render(**_defaults) -chameleon_template = functools.partial(template, template_adapter=ChameleonTemplate) -chameleon_view = functools.partial(view, template_adapter=ChameleonTemplate) +chameleon_template = functools.partial(bottle.template, template_adapter=ChameleonTemplate) +chameleon_view = functools.partial(bottle.view, template_adapter=ChameleonTemplate) # U t i l i t i e s @@ -266,6 +268,7 @@ if callable(value): continue ret[name] = value + return ret # R o u t e s # @@ -279,7 +282,7 @@ _FLOOP = "tincan.forwards" _FORIG = "tincan.origin" -class TinCanErrorRoute(object): +class _TinCanErrorRoute(object): """ A route to an error page. These never have code-behind, don't get routes created for them, and are only reached if an error routes them @@ -293,7 +296,7 @@ def __call__(self, e): return self._template.render(e=e, request=bottle.request).lstrip('\n') -class TinCanRoute(object): +class _TinCanRoute(object): """ A route created by the TinCan launcher. """ @@ -302,7 +305,6 @@ self._urlroot = launcher.urlroot self._name = name self._python = name + _PEXTEN - self._content = CONTENT self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) self._origin = self._urlpath @@ -319,7 +321,10 @@ hidden = None while True: self._template = TemplateFile(self._fspath) - self._header = TemplateHeader(self._template.header) + try: + self._header = TemplateHeader(self._template.header) + except TemplateHeaderException as e: + raise TinCanError("{0}: {1!s}".format(self._fspath, e)) from e if hidden is None: if self._header.errors is not None: break @@ -329,33 +334,33 @@ if self._header.forward is None: break self._redirect() - # If this is a hidden page, we ignore it for now, since hidden pages + # If this is a #hidden page, we ignore it for now, since hidden pages # don't get routes made for them. if hidden: return - # If this is an error page, register it as such. + # If this is an #errors page, register it as such. if self._header.errors is not None: self._mkerror() return # this implies #hidden - # Get methods for this route + # Get #methods for this route if self._header.methods is None: methods = [ 'GET' ] else: methods = [ i.upper() for i in self._header.methods.split() ] if not methods: raise TinCanError("{0}: no #methods specified".format(self._urlpath)) - # Process other header entries + # Get the code-behind #python if self._header.python is not None: if not self._header.python.endswith(_PEXTEN): - raise TinCanError("{0}: #python files must end in {1}", self._urlpath, _PEXTEN) + raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) self._python = self._header.python # Obtain a class object by importing and introspecting a module. self._getclass() - # Build body object (Chameleon template) + # Build body object (#template) if self._header.template is not None: if not self._header.template.endswith(_TEXTEN): - raise TinCanError("{0}: #template files must end in {1}", self._urlpath, _TEXTEN) - tpath = os.path.join(self._fsroot, *self._splitpath(self._header.template)) + raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) + tpath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._header.template))) tfile = TemplateFile(tpath) self._body = self._tclass(source=tfile.body) else: @@ -375,26 +380,28 @@ raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e if not errors: errors = range(_ERRMIN, _ERRMAX+1) - route = TinCanErrorRoute(self._tclass(source=self._template.body)) + route = _TinCanErrorRoute(self._tclass(source=self._template.body)) for error in errors: if error < _ERRMIN or error > _ERRMAX: raise TinCanError("{0}: bad #errors code".format(self._urlpath)) self._app.error(code=error, callback=route) def _getclass(self): - pypath = os.path.normpath(os.path.join(self._fsroot, *self._subdir, self._python)) + pypath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._python))) pycpath = pypath + 'c' try: pyctime = os.stat(pycpath).st_mtime except OSError: pyctime = 0 - if pyctime < os.stat(pypath).st_mtime: - try: - py_compile.compile(pypath, cfile=pycpath) - except Exception as e: - raise TinCanError("{0}: error compiling".format(pypath)) from e try: - spec = importlib.util.spec_from_file_location(_mangle(self._name), cpath) + if pyctime < os.stat(pypath).st_mtime: + py_compile.compile(pypath, cfile=pycpath, doraise=True) + except py_compile.PyCompileError as e: + raise TinCanError(str(e)) from e + except Exception as e: + raise TinCanError("{0}: {1!s}".format(pypath, e)) from e + try: + spec = importlib.util.spec_from_file_location(_mangle(self._name), pycpath) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) except Exception as e: @@ -402,19 +409,20 @@ self._class = None for i in dir(mod): v = getattr(mod, i) - if issubclass(v, Page): + if isclass(v) and issubclass(v, Page): if self._class is not None: - raise TinCanError("{0}: contains multiple Page classes", pypath) + raise TinCanError("{0}: contains multiple Page classes".format(pypath)) self._class = v if self._class is None: - raise TinCanError("{0}: contains no Page classes", pypath) + raise TinCanError("{0}: contains no Page classes".format(pypath)) def _redirect(self): - if self._header.forward in self._seen: - raise TinCanError("{0}: #forward loop".format(self._origin)) - self._seen.add(self._header.forward) try: rlist = self._splitpath(self._header.forward) + forw = '/' + '/'.join(rlist) + if forw in self.seen: + raise TinCanError("{0}: #forward loop".format(self._origin)) + self._seen.add(forw) rname = rlist.pop() except IndexError as e: raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e @@ -444,7 +452,7 @@ except ForwardException as fwd: target = fwd.target if target is None: - raise TinCanError("Unexpected null target!") + raise TinCanError("{0}: unexpected null target".format(self._urlpath)) # We get here if we are doing a server-side programmatic # forward. environ = bottle.request.environ @@ -453,7 +461,7 @@ if _FLOOP not in environ: environ[_FLOOP] = set([self._urlpath]) elif target in environ[_FLOOP]: - TinCanError("{0}: forward loop detected".format(environ[_FORIG])) + raise TinCanError("{0}: forward loop detected".format(environ[_FORIG])) environ[_FLOOP].add(target) environ['bottle.raw_path'] = target environ['PATH_INFO'] = urllib.parse.quote(target) @@ -481,14 +489,17 @@ """ Helper class for launching webapps. """ - def __init__(self, fsroot, urlroot, tclass): + def __init__(self, fsroot, urlroot, tclass, logger): """ Lightweight constructor. The real action happens in .launch() below. """ self.fsroot = fsroot self.urlroot = urlroot self.tclass = tclass + self.logger = logger self.app = None + self.errors = 0 + self.debug = False def launch(self): """ @@ -511,7 +522,7 @@ # Do what we gotta do self.app = TinCan() self._launch([]) - return self.app + return self def _launch(self, subdir): for entry in os.listdir(os.path.join(self.fsroot, *subdir)): @@ -522,16 +533,34 @@ ename, eext = os.path.splitext(entry) if eext != _TEXTEN: continue # only look at interesting files - route = TinCanRoute(self, ename, subdir) - page.launch() + route = _TinCanRoute(self, ename, subdir) + try: + route.launch() + except TinCanError as e: + self.logger(str(e)) + if self.debug: + while e.__cause__ != None: + e = e.__cause__ + self.logger("\t{0}: {1!s}".format(e.__class__.__name__, e)) + self.errors += 1 elif S_ISDIR(etype): self._launch(subdir + [entry]) -def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate): +def _logger(message): + sys.stderr.write(message) + sys.stderr.write('\n') + +def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger): """ Launch and return a TinCan webapp. Does not run the app; it is the caller's responsibility to call app.run() """ if fsroot is None: fsroot = os.getcwd() - return _Launcher(fsroot, urlroot, tclass).launch() + launcher = _Launcher(fsroot, urlroot, tclass, logger) + # launcher.debug = True + launcher.launch() + return launcher.app, launcher.errors + +# XXX - We cannot implement a command-line launcher here; see the +# launcher script for why.