# HG changeset patch # User David Barts # Date 1558536436 25200 # Node ID 34d3cfcd37ef013ee04efa9b7f37770685015e58 # Parent e8b6ee7e5b6b5bdb09ee418590c95e47f9e70823 First batch of work on getting a #include header. Unfinished. diff -r e8b6ee7e5b6b -r 34d3cfcd37ef tincan.py --- a/tincan.py Tue May 21 18:01:44 2019 -0700 +++ b/tincan.py Wed May 22 07:47:16 2019 -0700 @@ -28,6 +28,9 @@ """ pass +class BaseSyntaxError(TinCanException): + + class TemplateHeaderError(TinCanException): """ Raised upon encountering a syntax error in the template headers. @@ -40,6 +43,19 @@ def __str__(self): return "line {0}: {1}".format(self.line, self.message) +class IncludeError(TinCanException): + """ + Raised when we run into problems #include'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}: {1}".format(self.source, self.message) + class ForwardException(TinCanException): """ Raised to effect the flow control needed to do a forward (server-side @@ -116,6 +132,7 @@ """ _NAMES = [ "errors", "forward", "methods", "python", "template" ] _FNAMES = [ "hidden" ] + _ANAMES = [ "include" ] def __init__(self, string): # Initialize our state @@ -150,7 +167,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,7 +182,10 @@ 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 # @@ -305,6 +326,67 @@ self.request = req self.error = err +# I n c l u s i o n +# +# This is where the #include directives get processed + +_IEXTEN = ".pt" + +class _Includer(object): + def __init__(self, base, subdir, name, included=None, encoding="utf-8"): + self.base = base + self.subdir = subdir + self.name = name + self.included = set() if included is None else included + self.encoding = encoding + self._buf = [] + self._tlib = os.path.join(base, _WINF, "tlib") + + def render(self, includes, body): + for i in includes: + if i.startswith('<') and i.endswith('>'): + self._buf.append(self._render1(self._tlib, i[1:-1])) + else: + self._buf.append(self._render1(self.subdir, i)) + self._buf.append(body) + return ''.join(self._buf) + + def _render1(self, subdir, path): + # Reject bad file names + if not path.endswith(_IEXTEN) or path.endswith(_PEXTEN): + raise IncludeError(self.name, "#include files must end with {0} or {1}".format(_IEXTEN, _PEXTEN)) + # Normalize + rawpath = _normpath(subdir, path) + # Only include once + relpath = '/' + '/'.join(rawpath) + if relpath in self.included: + return + self.included.add(relpath) + # Do actual inclusion of a file + npath = os.path.join(self.base, *rawpath) + nsubdir = rawpath[:-1] + try: + tf = TemplateFile(npath) + except OSError as e: + raise IncludeError(self.name, str(e)) from e + try: + th = TemplateHeader(tf.headers) + except TemplateHeaderError as e: + raise IncludeError(npath, str(e)) from e + # Reject unsupported crap (included files can only #include) + for i in dir(th): + if i.startswith('_') or i == 'include': + continue + v = getattr(th, i) + if callable(v): + continue + if v is not None and v is not False: + raise IncludeError(npath, "unsupported #{0}".format(i)) + # Inclusion is recursive... + nested = _Includer(self.base, nsubdir, relpath, + included=self.included, encoding=self.encoding) + return nested.render(th.includes, tf.body) + # R o u t e s # # Represents a route in TinCan. Our launcher creates these on-the-fly based @@ -411,15 +493,28 @@ 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))) + rawpath = self._splitpath(self._header.template) + tsubdir = rawpath[:-1] + tpath = os.path.normpath(os.path.join(self._fsroot, *rawpath)) + turlpath = '/' + self._urljoin(rawpath) tfile = TemplateFile(tpath) - except OSError as e: + thead = TemplateHeader(tpath.header) + except (OSError, TemplateHeaderError) 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) + includer = _Includer(self._fspath, tsubdir, turlpath) + try: + self._body = self._tclass(source=includer.render(thead.include, tfile.body)) + except IncludeError as e: + raise TinCanError("{0}: #include error: {1!s}".format(turlpath, e)) from e else: - self._body = self._tclass(source=self._template.body) + includer = _Includer(self._fspath, self._subdir, self._urlpath) + try: + self._body = self._tclass( + source=includer.render(self._header.include, self._template.body)) + except IncludeError as e: + raise TinCanError("{0}: #include error: {1!s}".format(self._urlpath, e)) from e self._body.prepare() # If this is an #errors page, register it as such. if oheader.errors is not None: