Mercurial > cgi-bin > hgweb.cgi > tincan
comparison tincan.py @ 24:34d3cfcd37ef draft header-includes
First batch of work on getting a #include header. Unfinished.
author | David Barts <n5jrn@me.com> |
---|---|
date | Wed, 22 May 2019 07:47:16 -0700 |
parents | e8b6ee7e5b6b |
children | e93e5e746cc5 |
comparison
equal
deleted
inserted
replaced
23:e8b6ee7e5b6b | 24:34d3cfcd37ef |
---|---|
26 """ | 26 """ |
27 The parent class of all exceptions we raise. | 27 The parent class of all exceptions we raise. |
28 """ | 28 """ |
29 pass | 29 pass |
30 | 30 |
31 class BaseSyntaxError(TinCanException): | |
32 | |
33 | |
31 class TemplateHeaderError(TinCanException): | 34 class TemplateHeaderError(TinCanException): |
32 """ | 35 """ |
33 Raised upon encountering a syntax error in the template headers. | 36 Raised upon encountering a syntax error in the template headers. |
34 """ | 37 """ |
35 def __init__(self, message, line): | 38 def __init__(self, message, line): |
37 self.message = message | 40 self.message = message |
38 self.line = line | 41 self.line = line |
39 | 42 |
40 def __str__(self): | 43 def __str__(self): |
41 return "line {0}: {1}".format(self.line, self.message) | 44 return "line {0}: {1}".format(self.line, self.message) |
45 | |
46 class IncludeError(TinCanException): | |
47 """ | |
48 Raised when we run into problems #include'ing something, usually | |
49 because it doesn't exist. | |
50 """ | |
51 def __init__(self, message, source): | |
52 super().__init__(message, source) | |
53 self.message = message | |
54 self.source = source | |
55 | |
56 def __str__(self): | |
57 return "{0}: {1}".format(self.source, self.message) | |
42 | 58 |
43 class ForwardException(TinCanException): | 59 class ForwardException(TinCanException): |
44 """ | 60 """ |
45 Raised to effect the flow control needed to do a forward (server-side | 61 Raised to effect the flow control needed to do a forward (server-side |
46 redirect). It is ugly to do this, but other Python frameworks do and | 62 redirect). It is ugly to do this, but other Python frameworks do and |
114 """ | 130 """ |
115 Parses and represents a set of header lines. | 131 Parses and represents a set of header lines. |
116 """ | 132 """ |
117 _NAMES = [ "errors", "forward", "methods", "python", "template" ] | 133 _NAMES = [ "errors", "forward", "methods", "python", "template" ] |
118 _FNAMES = [ "hidden" ] | 134 _FNAMES = [ "hidden" ] |
135 _ANAMES = [ "include" ] | |
119 | 136 |
120 def __init__(self, string): | 137 def __init__(self, string): |
121 # Initialize our state | 138 # Initialize our state |
122 for i in self._NAMES: | 139 for i in self._NAMES: |
123 setattr(self, i, None) | 140 setattr(self, i, None) |
148 break | 165 break |
149 if name not in nameset: | 166 if name not in nameset: |
150 raise TemplateHeaderError("Invalid directive: {0!r}".format(rna), count) | 167 raise TemplateHeaderError("Invalid directive: {0!r}".format(rna), count) |
151 if name in seen: | 168 if name in seen: |
152 raise TemplateHeaderError("Duplicate {0!r} directive.".format(rna), count) | 169 raise TemplateHeaderError("Duplicate {0!r} directive.".format(rna), count) |
153 seen.add(name) | 170 if name not in self._ANAMES: |
171 seen.add(name) | |
154 # Flags | 172 # Flags |
155 if name in self._FNAMES: | 173 if name in self._FNAMES: |
156 setattr(self, name, True) | 174 setattr(self, name, True) |
157 continue | 175 continue |
158 # Get parameter | 176 # Get parameter |
162 for i in [ "'", '"']: | 180 for i in [ "'", '"']: |
163 if param.startswith(i) and param.endswith(i): | 181 if param.startswith(i) and param.endswith(i): |
164 param = ast.literal_eval(param) | 182 param = ast.literal_eval(param) |
165 break | 183 break |
166 # Update this object | 184 # Update this object |
167 setattr(self, name, param) | 185 if name in self._ANAMES: |
186 getattr(self, name).append(param) | |
187 else: | |
188 setattr(self, name, param) | |
168 | 189 |
169 # C h a m e l e o n | 190 # C h a m e l e o n |
170 # | 191 # |
171 # Support for Chameleon templates (the kind TinCan uses by default). | 192 # Support for Chameleon templates (the kind TinCan uses by default). |
172 | 193 |
302 """ | 323 """ |
303 Constructor. This is a lightweight operation. | 324 Constructor. This is a lightweight operation. |
304 """ | 325 """ |
305 self.request = req | 326 self.request = req |
306 self.error = err | 327 self.error = err |
328 | |
329 # I n c l u s i o n | |
330 # | |
331 # This is where the #include directives get processed | |
332 | |
333 _IEXTEN = ".pt" | |
334 | |
335 class _Includer(object): | |
336 def __init__(self, base, subdir, name, included=None, encoding="utf-8"): | |
337 self.base = base | |
338 self.subdir = subdir | |
339 self.name = name | |
340 self.included = set() if included is None else included | |
341 self.encoding = encoding | |
342 self._buf = [] | |
343 self._tlib = os.path.join(base, _WINF, "tlib") | |
344 | |
345 def render(self, includes, body): | |
346 for i in includes: | |
347 if i.startswith('<') and i.endswith('>'): | |
348 self._buf.append(self._render1(self._tlib, i[1:-1])) | |
349 else: | |
350 self._buf.append(self._render1(self.subdir, i)) | |
351 self._buf.append(body) | |
352 return ''.join(self._buf) | |
353 | |
354 def _render1(self, subdir, path): | |
355 # Reject bad file names | |
356 if not path.endswith(_IEXTEN) or path.endswith(_PEXTEN): | |
357 raise IncludeError(self.name, "#include files must end with {0} or {1}".format(_IEXTEN, _PEXTEN)) | |
358 # Normalize | |
359 rawpath = _normpath(subdir, path) | |
360 # Only include once | |
361 relpath = '/' + '/'.join(rawpath) | |
362 if relpath in self.included: | |
363 return | |
364 self.included.add(relpath) | |
365 # Do actual inclusion of a file | |
366 npath = os.path.join(self.base, *rawpath) | |
367 nsubdir = rawpath[:-1] | |
368 try: | |
369 tf = TemplateFile(npath) | |
370 except OSError as e: | |
371 raise IncludeError(self.name, str(e)) from e | |
372 try: | |
373 th = TemplateHeader(tf.headers) | |
374 except TemplateHeaderError as e: | |
375 raise IncludeError(npath, str(e)) from e | |
376 # Reject unsupported crap (included files can only #include) | |
377 for i in dir(th): | |
378 if i.startswith('_') or i == 'include': | |
379 continue | |
380 v = getattr(th, i) | |
381 if callable(v): | |
382 continue | |
383 if v is not None and v is not False: | |
384 raise IncludeError(npath, "unsupported #{0}".format(i)) | |
385 # Inclusion is recursive... | |
386 nested = _Includer(self.base, nsubdir, relpath, | |
387 included=self.included, encoding=self.encoding) | |
388 return nested.render(th.includes, tf.body) | |
307 | 389 |
308 # R o u t e s | 390 # R o u t e s |
309 # | 391 # |
310 # Represents a route in TinCan. Our launcher creates these on-the-fly based | 392 # Represents a route in TinCan. Our launcher creates these on-the-fly based |
311 # on the files it finds. | 393 # on the files it finds. |
409 # Build body object (#template) | 491 # Build body object (#template) |
410 if self._header.template is not None: | 492 if self._header.template is not None: |
411 if not self._header.template.endswith(_TEXTEN): | 493 if not self._header.template.endswith(_TEXTEN): |
412 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) | 494 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) |
413 try: | 495 try: |
414 tpath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._header.template))) | 496 rawpath = self._splitpath(self._header.template) |
497 tsubdir = rawpath[:-1] | |
498 tpath = os.path.normpath(os.path.join(self._fsroot, *rawpath)) | |
499 turlpath = '/' + self._urljoin(rawpath) | |
415 tfile = TemplateFile(tpath) | 500 tfile = TemplateFile(tpath) |
416 except OSError as e: | 501 thead = TemplateHeader(tpath.header) |
502 except (OSError, TemplateHeaderError) as e: | |
417 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e | 503 raise TinCanError("{0}: invalid #template: {1!s}".format(self._urlpath, e)) from e |
418 except IndexError as e: | 504 except IndexError as e: |
419 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e | 505 raise TinCanError("{0}: invalid #template".format(self._urlpath)) from e |
420 self._body = self._tclass(source=tfile.body) | 506 includer = _Includer(self._fspath, tsubdir, turlpath) |
507 try: | |
508 self._body = self._tclass(source=includer.render(thead.include, tfile.body)) | |
509 except IncludeError as e: | |
510 raise TinCanError("{0}: #include error: {1!s}".format(turlpath, e)) from e | |
421 else: | 511 else: |
422 self._body = self._tclass(source=self._template.body) | 512 includer = _Includer(self._fspath, self._subdir, self._urlpath) |
513 try: | |
514 self._body = self._tclass( | |
515 source=includer.render(self._header.include, self._template.body)) | |
516 except IncludeError as e: | |
517 raise TinCanError("{0}: #include error: {1!s}".format(self._urlpath, e)) from e | |
423 self._body.prepare() | 518 self._body.prepare() |
424 # If this is an #errors page, register it as such. | 519 # If this is an #errors page, register it as such. |
425 if oheader.errors is not None: | 520 if oheader.errors is not None: |
426 self._mkerror(oheader.errors) | 521 self._mkerror(oheader.errors) |
427 return # this implies #hidden | 522 return # this implies #hidden |