Mercurial > cgi-bin > hgweb.cgi > tincan
diff tincan.py @ 39:ee19984ba31d draft
Merge in the header-includes branch to the trunk.
author | David Barts <n5jrn@me.com> |
---|---|
date | Tue, 28 May 2019 20:20:19 -0700 |
parents | ce67eac10fc7 |
children | df27cf08c093 |
line wrap: on
line diff
--- a/tincan.py Tue May 21 18:01:44 2019 -0700 +++ b/tincan.py Tue May 28 20:20:19 2019 -0700 @@ -40,6 +40,19 @@ def __str__(self): return "line {0}: {1}".format(self.line, self.message) +class LoadError(TinCanException): + """ + Raised when we run into problems #load'ing something, usually + because it doesn't exist. + """ + def __init__(self, message, source): + super().__init__(message, source) + self.message = message + self.source = source + + def __str__(self): + return "{0}: #load error: {1}".format(self.source, self.message) + class ForwardException(TinCanException): """ Raised to effect the flow control needed to do a forward (server-side @@ -116,6 +129,7 @@ """ _NAMES = [ "errors", "forward", "methods", "python", "template" ] _FNAMES = [ "hidden" ] + _ANAMES = [ "load" ] def __init__(self, string): # Initialize our state @@ -123,9 +137,11 @@ setattr(self, i, None) for i in self._FNAMES: setattr(self, i, False) + for i in self._ANAMES: + setattr(self, i, []) # Parse the string count = 0 - nameset = set(self._NAMES + self._FNAMES) + nameset = set(self._NAMES + self._FNAMES + self._ANAMES) seen = set() lines = string.split("\n") if lines and lines[-1] == "": @@ -150,7 +166,8 @@ raise TemplateHeaderError("Invalid directive: {0!r}".format(rna), count) if name in seen: raise TemplateHeaderError("Duplicate {0!r} directive.".format(rna), count) - seen.add(name) + if name not in self._ANAMES: + seen.add(name) # Flags if name in self._FNAMES: setattr(self, name, True) @@ -164,21 +181,25 @@ param = ast.literal_eval(param) break # Update this object - setattr(self, name, param) + if name in self._ANAMES: + getattr(self, name).append(param) + else: + setattr(self, name, param) # C h a m e l e o n # -# Support for Chameleon templates (the kind TinCan uses by default). +# Support for Chameleon templates (the kind TinCan uses). class ChameleonTemplate(bottle.BaseTemplate): def prepare(self, **options): from chameleon import PageTemplate, PageTemplateFile if self.source: - self.tpl = PageTemplate(self.source, encoding=self.encoding, - **options) + self.tpl = PageTemplate(self.source, **options) else: self.tpl = PageTemplateFile(self.filename, encoding=self.encoding, search_path=self.lookup, **options) + # XXX - work around broken Chameleon decoding + self.tpl.default_encoding = self.encoding def render(self, *args, **kwargs): for dictarg in args: @@ -305,6 +326,44 @@ self.request = req self.error = err +# I n c l u s i o n +# +# Most processing is in the TinCanRoute class; this just interprets and +# represents arguments to the #load header directive. + +class _LoadedFile(object): + def __init__(self, raw): + if raw.startswith('<') and raw.endswith('>'): + raw = raw[1:-1] + self.in_lib = True + else: + self.in_lib = False + equals = raw.find('=') + if equals < 0: + self.vname = os.path.splitext(os.path.basename(raw))[0] + self.fname = raw + else: + self.vname = raw[:equals] + self.fname = raw[equals+1:] + if self.vname == "": + raise ValueError("empty variable name") + if self.fname == "": + raise ValueError("empty file name") + if not self.fname.endswith(_IEXTEN): + raise ValueError("file does not end in {0}".format(_IEXTEN)) + +# Using a cache is likely to help efficiency a lot, since many pages +# will typically #load the same standard stuff. +_tcache = {} +def _get_template(name, direct, coding): + aname = os.path.abspath(os.path.join(direct, name)) + if aname not in _tcache: + tmpl = ChameleonTemplate(name=name, lookup=[direct], encoding=coding) + tmpl.prepare() + assert aname == tmpl.filename + _tcache[aname] = tmpl + return _tcache[aname] + # R o u t e s # # Represents a route in TinCan. Our launcher creates these on-the-fly based @@ -312,6 +371,7 @@ _ERRMIN = 400 _ERRMAX = 599 +_IEXTEN = ".pt" _PEXTEN = ".py" _TEXTEN = ".pspx" _FLOOP = "tincan.forwards" @@ -325,9 +385,10 @@ custom code-behind, only two variables are available to your template: request (bottle.Request) and error (bottle.HTTPError). """ - def __init__(self, template, klass): + def __init__(self, template, loads, klass): self._template = template self._template.prepare() + self._loads = loads self._class = klass def __call__(self, e): @@ -335,7 +396,9 @@ try: obj = self._class(bottle.request, e) obj.handle() - return self._template.render(obj.export()).lstrip('\n') + tvars = self._loads.copy() + tvars.update(obj.export()) + return self._template.render(tvars).lstrip('\n') except bottle.HTTPResponse as e: return e except Exception as e: @@ -361,8 +424,9 @@ self._origin = self._urlpath self._subdir = subdir self._seen = set() - self._tclass = launcher.tclass self._app = launcher.app + self._save_loads = launcher.debug + self._encoding = launcher.encoding def launch(self): """ @@ -406,21 +470,38 @@ self._python_specified = True # Obtain a class object by importing and introspecting a module. self._getclass() - # Build body object (#template) + # Build body object (#template) and obtain #loads. if self._header.template is not None: if not self._header.template.endswith(_TEXTEN): raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) try: - tpath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._header.template))) + rtpath = self._splitpath(self._header.template) + tpath = os.path.normpath(os.path.join(self._fsroot, *rtpath)) tfile = TemplateFile(tpath) except OSError as e: raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e except IndexError as e: raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e - self._body = self._tclass(source=tfile.body) + self._body = ChameleonTemplate(source=tfile.body, encoding=self._encoding) else: - self._body = self._tclass(source=self._template.body) + self._body = ChameleonTemplate(source=self._template.body, encoding=self._encoding) self._body.prepare() + # Process loads + self._loads = {} + for load in self._header.load: + try: + load = _LoadedFile(load) + except ValueError as e: + raise TinCanError("{0}: bad #load: {1!s}".format(self._urlpath, e)) from e + if load.in_lib: + fdir = os.path.join(self._fsroot, _WINF, "tlib") + else: + fdir = os.path.join(self._fsroot, *self._subdir) + try: + tmpl = _get_template(load.fname, fdir, self._encoding) + except Exception as e: + raise TinCanError("{0}: bad #load: {1!s}".format(self._urlpath, e)) from e + self._loads[load.vname] = tmpl.tpl # If this is an #errors page, register it as such. if oheader.errors is not None: self._mkerror(oheader.errors) @@ -446,7 +527,9 @@ 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), self._class) + route = _TinCanErrorRoute( + ChameleonTemplate(source=self._template.body, encoding=self._encoding), + self._loads, self._class) for error in errors: if error < _ERRMIN or error > _ERRMAX: raise TinCanError("{0}: bad #errors code".format(self._urlpath)) @@ -559,7 +642,9 @@ try: obj = self._class(bottle.request, bottle.response) obj.handle() - return self._body.render(obj.export()).lstrip('\n') + tvars = self._loads.copy() + tvars.update(obj.export()) + return self._body.render(tvars).lstrip('\n') except ForwardException as fwd: target = fwd.target except bottle.HTTPResponse as e: @@ -590,36 +675,27 @@ environ['route.url_args'] = args return route.call(**args) - def _mkdict(self, obj): - ret = {} - for name in dir(obj): - if name.startswith('_'): - continue - value = getattr(obj, name) - if not callable(value): - ret[name] = value - return ret - # L a u n c h e r _WINF = "WEB-INF" _BANNED = set([_WINF]) +ENCODING = "utf-8" class _Launcher(object): """ Helper class for launching webapps. """ - def __init__(self, fsroot, urlroot, tclass, logger): + def __init__(self, fsroot, urlroot, 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 + self.encoding = ENCODING def launch(self): """ @@ -680,15 +756,16 @@ sys.stderr.write(message) sys.stderr.write('\n') -def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger): +def launch(fsroot=None, urlroot='/', logger=_logger, debug=False, encoding=ENCODING): """ 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() - launcher = _Launcher(fsroot, urlroot, tclass, logger) - # launcher.debug = True + launcher = _Launcher(fsroot, urlroot, logger) + launcher.debug = debug + launcher.encoding = encoding launcher.launch() return launcher.app, launcher.errors