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