Mercurial > cgi-bin > hgweb.cgi > tincan
annotate tincan.py @ 4:0d47859f792a draft
Finally got "hello, world" working. Still likely many bugs.
author | David Barts <n5jrn@me.com> |
---|---|
date | Mon, 13 May 2019 12:38:26 -0700 |
parents | c6902cded64d |
children | 31bb8400e6e3 |
rev | line source |
---|---|
0 | 1 #!/usr/bin/env python3 |
2 # -*- coding: utf-8 -*- | |
3 # As with Bottle, it's all in one big, ugly file. For now. | |
4 | |
5 # I m p o r t s | |
6 | |
7 import os, sys | |
8 import ast | |
9 import binascii | |
10 from base64 import b16encode, b16decode | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
11 import functools |
2 | 12 import importlib |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
13 from inspect import isclass |
0 | 14 import io |
2 | 15 import py_compile |
16 from stat import S_ISDIR, S_ISREG | |
0 | 17 |
18 import bottle | |
19 | |
2 | 20 # E x c e p t i o n s |
0 | 21 |
22 class TinCanException(Exception): | |
23 """ | |
24 The parent class of all exceptions we raise. | |
25 """ | |
26 pass | |
27 | |
28 class TemplateHeaderException(TinCanException): | |
29 """ | |
30 Raised upon encountering a syntax error in the template headers. | |
31 """ | |
32 def __init__(self, message, line): | |
33 super().__init__(message, line) | |
34 self.message = message | |
35 self.line = line | |
36 | |
37 def __str__(self): | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
38 return "line {0}: {1}".format(self.line, self.message) |
0 | 39 |
40 class ForwardException(TinCanException): | |
41 """ | |
42 Raised to effect the flow control needed to do a forward (server-side | |
43 redirect). It is ugly to do this, but other Python frameworks do and | |
44 there seems to be no good alternative. | |
45 """ | |
46 def __init__(self, target): | |
47 self.target = target | |
48 | |
49 class TinCanError(TinCanException): | |
50 """ | |
51 General-purpose exception thrown by TinCan when things go wrong, often | |
52 when attempting to launch webapps. | |
53 """ | |
54 pass | |
55 | |
2 | 56 # T e m p l a t e s |
57 # | |
0 | 58 # Template (.pspx) files. These are standard templates for a supported |
59 # template engine, but with an optional set of header lines that begin | |
60 # with '#'. | |
61 | |
62 class TemplateFile(object): | |
63 """ | |
64 Parse a template file into a header part and the body part. The header | |
65 is always a leading set of lines, each starting with '#', that is of the | |
66 same format regardless of the template body. The template body varies | |
67 depending on the selected templating engine. The body part has | |
68 each header line replaced by a blank line. This preserves the overall | |
69 line numbering when processing the body. The added newlines are normally | |
70 stripped out before the rendered page is sent back to the client. | |
71 """ | |
72 def __init__(self, raw, encoding='utf-8'): | |
73 if isinstance(raw, io.TextIOBase): | |
74 self._do_init(raw) | |
75 elif isinstance(raw, str): | |
76 with open(raw, "r", encoding=encoding) as fp: | |
77 self._do_init(fp) | |
78 else: | |
79 raise TypeError("Expecting a string or Text I/O object.") | |
80 | |
81 def _do_init(self, fp): | |
82 self._hbuf = [] | |
83 self._bbuf = [] | |
84 self._state = self._header | |
85 while True: | |
86 line = fp.readline() | |
87 if line == '': | |
88 break | |
89 self._state(line) | |
90 self.header = ''.join(self._hbuf) | |
91 self.body = ''.join(self._bbuf) | |
92 | |
93 def _header(self, line): | |
94 if not line.startswith('#'): | |
95 self._state = self._body | |
96 self._state(line) | |
97 return | |
98 self._hbuf.append(line) | |
99 self._bbuf.append("\n") | |
100 | |
101 def _body(self, line): | |
102 self._bbuf.append(line) | |
103 | |
104 class TemplateHeader(object): | |
105 """ | |
106 Parses and represents a set of header lines. | |
107 """ | |
2 | 108 _NAMES = [ "errors", "forward", "methods", "python", "template" ] |
0 | 109 _FNAMES = [ "hidden" ] |
110 | |
111 def __init__(self, string): | |
112 # Initialize our state | |
113 for i in self._NAMES: | |
114 setattr(self, i, None) | |
115 for i in self._FNAMES: | |
116 setattr(self, i, False) | |
117 # Parse the string | |
118 count = 0 | |
119 nameset = set(self._NAMES + self._FNAMES) | |
120 seen = set() | |
121 lines = string.split("\n") | |
122 if lines and lines[-1] == "": | |
123 del lines[-1] | |
124 for line in lines: | |
125 # Get line | |
126 count += 1 | |
127 if not line.startswith("#"): | |
128 raise TemplateHeaderException("Does not start with '#'.", count) | |
129 try: | |
130 rna, rpa = line.split(maxsplit=1) | |
131 except ValueError: | |
132 raise TemplateHeaderException("Missing parameter.", count) | |
133 # Get name, ignoring remarks. | |
134 name = rna[1:] | |
135 if name == "rem": | |
136 continue | |
137 if name not in nameset: | |
138 raise TemplateHeaderException("Invalid directive: {0!r}".format(rna), count) | |
139 if name in seen: | |
140 raise TemplateHeaderException("Duplicate {0!r} directive.".format(rna), count) | |
141 seen.add(name) | |
142 # Flags | |
143 if name in self._FLAGS: | |
144 setattr(self, name, True) | |
145 continue | |
146 # Get parameter | |
147 param = rpa.strip() | |
148 for i in [ "'", '"']: | |
149 if param.startswith(i) and param.endswith(i): | |
150 param = ast.literal_eval(param) | |
151 break | |
152 # Update this object | |
153 setattr(self, name, param) | |
154 | |
2 | 155 # C h a m e l e o n |
156 # | |
0 | 157 # Support for Chameleon templates (the kind TinCan uses by default). |
158 | |
159 class ChameleonTemplate(bottle.BaseTemplate): | |
160 def prepare(self, **options): | |
161 from chameleon import PageTemplate, PageTemplateFile | |
162 if self.source: | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
163 self.tpl = PageTemplate(self.source, encoding=self.encoding, |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
164 **options) |
0 | 165 else: |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
166 self.tpl = PageTemplateFile(self.filename, encoding=self.encoding, |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
167 search_path=self.lookup, **options) |
0 | 168 |
169 def render(self, *args, **kwargs): | |
170 for dictarg in args: | |
171 kwargs.update(dictarg) | |
172 _defaults = self.defaults.copy() | |
173 _defaults.update(kwargs) | |
174 return self.tpl.render(**_defaults) | |
175 | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
176 chameleon_template = functools.partial(bottle.template, template_adapter=ChameleonTemplate) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
177 chameleon_view = functools.partial(bottle.view, template_adapter=ChameleonTemplate) |
0 | 178 |
2 | 179 # U t i l i t i e s |
0 | 180 |
181 def _normpath(base, unsplit): | |
182 """ | |
183 Split, normalize and ensure a possibly relative path is absolute. First | |
184 argument is a list of directory names, defining a base. Second | |
185 argument is a string, which may either be relative to that base, or | |
186 absolute. Only '/' is supported as a separator. | |
187 """ | |
188 scratch = unsplit.strip('/').split('/') | |
189 if not unsplit.startswith('/'): | |
190 scratch = base + scratch | |
191 ret = [] | |
192 for i in scratch: | |
193 if i == '.': | |
194 continue | |
195 if i == '..': | |
196 ret.pop() # may raise IndexError | |
197 continue | |
198 ret.append(i) | |
199 return ret | |
200 | |
201 def _mangle(string): | |
202 """ | |
203 Turn a possibly troublesome identifier into a mangled one. | |
204 """ | |
205 first = True | |
206 ret = [] | |
207 for ch in string: | |
208 if ch == '_' or not (ch if first else "x" + ch).isidentifier(): | |
209 ret.append('_') | |
210 ret.append(b16encode(ch.encode("utf-8")).decode("us-ascii")) | |
211 else: | |
212 ret.append(ch) | |
213 first = False | |
214 return ''.join(ret) | |
215 | |
216 # The TinCan class. Simply a Bottle webapp that contains a forward method, so | |
217 # the code-behind can call request.app.forward(). | |
218 | |
219 class TinCan(bottle.Bottle): | |
220 def forward(self, target): | |
221 """ | |
222 Forward this request to the specified target route. | |
223 """ | |
224 source = bottle.request.environ['PATH_INFO'] | |
225 base = source.strip('/').split('/')[:-1] | |
226 try: | |
227 exc = ForwardException('/' + '/'.join(_normpath(base, target))) | |
228 except IndexError as e: | |
229 raise TinCanError("{0}: invalid forward to {1!r}".format(source, target)) from e | |
230 raise exc | |
231 | |
2 | 232 # C o d e B e h i n d |
233 # | |
0 | 234 # Represents the code-behind of one of our pages. This gets subclassed, of |
235 # course. | |
236 | |
237 class Page(object): | |
238 # Non-private things we refuse to export anyhow. | |
239 __HIDDEN = set([ "request", "response" ]) | |
240 | |
241 def __init__(self, req, resp): | |
242 """ | |
243 Constructor. This is a lightweight operation. | |
244 """ | |
245 self.request = req # app context is request.app in Bottle | |
246 self.response = resp | |
247 | |
248 def handle(self): | |
249 """ | |
250 This is the entry point for the code-behind logic. It is intended | |
251 to be overridden. | |
252 """ | |
253 pass | |
254 | |
255 def export(self): | |
256 """ | |
257 Export template variables. The default behavior is to export all | |
258 non-hidden non-callables that don't start with an underscore, | |
259 plus a an export named page that contains this object itself. | |
260 This method can be overridden if a different behavior is | |
261 desired. It should always return a dict or dict-like object. | |
262 """ | |
263 ret = { "page": self } # feature: will be clobbered if self.page exists | |
264 for name in dir(self): | |
265 if name in self.__HIDDEN or name.startswith('_'): | |
266 continue | |
267 value = getattr(self, name) | |
268 if callable(value): | |
269 continue | |
270 ret[name] = value | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
271 return ret |
0 | 272 |
2 | 273 # R o u t e s |
274 # | |
0 | 275 # Represents a route in TinCan. Our launcher creates these on-the-fly based |
276 # on the files it finds. | |
277 | |
2 | 278 _ERRMIN = 400 |
279 _ERRMAX = 599 | |
280 _PEXTEN = ".py" | |
281 _TEXTEN = ".pspx" | |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
282 _FLOOP = "tincan.forwards" |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
283 _FORIG = "tincan.origin" |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
284 |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
285 class _TinCanErrorRoute(object): |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
286 """ |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
287 A route to an error page. These never have code-behind, don't get |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
288 routes created for them, and are only reached if an error routes them |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
289 there. Error templates only have two variables available: e (the |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
290 HTTPError object associated with the error) and request. |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
291 """ |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
292 def __init__(self, template): |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
293 self._template = template |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
294 self._template.prepare() |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
295 |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
296 def __call__(self, e): |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
297 return self._template.render(e=e, request=bottle.request).lstrip('\n') |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
298 |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
299 class _TinCanRoute(object): |
0 | 300 """ |
301 A route created by the TinCan launcher. | |
302 """ | |
303 def __init__(self, launcher, name, subdir): | |
304 self._fsroot = launcher.fsroot | |
305 self._urlroot = launcher.urlroot | |
306 self._name = name | |
2 | 307 self._python = name + _PEXTEN |
308 self._fspath = os.path.join(launcher.fsroot, *subdir, name + _TEXTEN) | |
309 self._urlpath = self._urljoin(launcher.urlroot, *subdir, name + _TEXTEN) | |
0 | 310 self._origin = self._urlpath |
311 self._subdir = subdir | |
312 self._seen = set() | |
313 self._tclass = launcher.tclass | |
314 self._app = launcher.app | |
315 | |
2 | 316 def launch(self): |
0 | 317 """ |
318 Launch a single page. | |
319 """ | |
320 # Build master and header objects, process #forward directives | |
321 hidden = None | |
322 while True: | |
323 self._template = TemplateFile(self._fspath) | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
324 try: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
325 self._header = TemplateHeader(self._template.header) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
326 except TemplateHeaderException as e: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
327 raise TinCanError("{0}: {1!s}".format(self._fspath, e)) from e |
0 | 328 if hidden is None: |
3 | 329 if self._header.errors is not None: |
330 break | |
0 | 331 hidden = self._header.hidden |
3 | 332 elif self._header.errors is not None: |
333 raise TinCanError("{0}: #forward to #errors not allowed".format(self._origin)) | |
0 | 334 if self._header.forward is None: |
335 break | |
336 self._redirect() | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
337 # If this is a #hidden page, we ignore it for now, since hidden pages |
0 | 338 # don't get routes made for them. |
339 if hidden: | |
340 return | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
341 # If this is an #errors page, register it as such. |
2 | 342 if self._header.errors is not None: |
3 | 343 self._mkerror() |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
344 return # this implies #hidden |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
345 # Get #methods for this route |
0 | 346 if self._header.methods is None: |
347 methods = [ 'GET' ] | |
348 else: | |
349 methods = [ i.upper() for i in self._header.methods.split() ] | |
3 | 350 if not methods: |
351 raise TinCanError("{0}: no #methods specified".format(self._urlpath)) | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
352 # Get the code-behind #python |
0 | 353 if self._header.python is not None: |
2 | 354 if not self._header.python.endswith(_PEXTEN): |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
355 raise TinCanError("{0}: #python files must end in {1}".format(self._urlpath, _PEXTEN)) |
0 | 356 self._python = self._header.python |
357 # Obtain a class object by importing and introspecting a module. | |
3 | 358 self._getclass() |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
359 # Build body object (#template) |
3 | 360 if self._header.template is not None: |
361 if not self._header.template.endswith(_TEXTEN): | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
362 raise TinCanError("{0}: #template files must end in {1}".format(self._urlpath, _TEXTEN)) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
363 tpath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._header.template))) |
3 | 364 tfile = TemplateFile(tpath) |
365 self._body = self._tclass(source=tfile.body) | |
366 else: | |
367 self._body = self._tclass(source=self._template.body) | |
368 self._body.prepare() | |
369 # Register this thing with Bottle | |
370 print("adding route:", self._origin) # debug | |
371 self._app.route(self._origin, methods, self) | |
372 | |
373 def _splitpath(self, unsplit): | |
374 return _normpath(self._subdir, unsplit) | |
375 | |
376 def _mkerror(self): | |
377 try: | |
378 errors = [ int(i) for i in self._header.errors.split() ] | |
379 except ValueError as e: | |
380 raise TinCanError("{0}: bad #errors line".format(self._urlpath)) from e | |
381 if not errors: | |
382 errors = range(_ERRMIN, _ERRMAX+1) | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
383 route = _TinCanErrorRoute(self._tclass(source=self._template.body)) |
3 | 384 for error in errors: |
385 if error < _ERRMIN or error > _ERRMAX: | |
386 raise TinCanError("{0}: bad #errors code".format(self._urlpath)) | |
387 self._app.error(code=error, callback=route) | |
388 | |
389 def _getclass(self): | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
390 pypath = os.path.normpath(os.path.join(self._fsroot, *self._splitpath(self._python))) |
0 | 391 pycpath = pypath + 'c' |
392 try: | |
393 pyctime = os.stat(pycpath).st_mtime | |
394 except OSError: | |
395 pyctime = 0 | |
396 try: | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
397 if pyctime < os.stat(pypath).st_mtime: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
398 py_compile.compile(pypath, cfile=pycpath, doraise=True) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
399 except py_compile.PyCompileError as e: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
400 raise TinCanError(str(e)) from e |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
401 except Exception as e: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
402 raise TinCanError("{0}: {1!s}".format(pypath, e)) from e |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
403 try: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
404 spec = importlib.util.spec_from_file_location(_mangle(self._name), pycpath) |
0 | 405 mod = importlib.util.module_from_spec(spec) |
406 spec.loader.exec_module(mod) | |
407 except Exception as e: | |
408 raise TinCanError("{0}: error importing".format(pycpath)) from e | |
409 self._class = None | |
410 for i in dir(mod): | |
411 v = getattr(mod, i) | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
412 if isclass(v) and issubclass(v, Page): |
0 | 413 if self._class is not None: |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
414 raise TinCanError("{0}: contains multiple Page classes".format(pypath)) |
0 | 415 self._class = v |
3 | 416 if self._class is None: |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
417 raise TinCanError("{0}: contains no Page classes".format(pypath)) |
0 | 418 |
419 def _redirect(self): | |
420 try: | |
421 rlist = self._splitpath(self._header.forward) | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
422 forw = '/' + '/'.join(rlist) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
423 if forw in self.seen: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
424 raise TinCanError("{0}: #forward loop".format(self._origin)) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
425 self._seen.add(forw) |
0 | 426 rname = rlist.pop() |
427 except IndexError as e: | |
428 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) from e | |
429 name, ext = os.path.splitext(rname)[1] | |
2 | 430 if ext != _TEXTEN: |
0 | 431 raise TinCanError("{0}: invalid #forward".format(self._urlpath)) |
432 self._subdir = rlist | |
2 | 433 self._python = name + _PEXTEN |
0 | 434 self._fspath = os.path.join(self._fsroot, *self._subdir, rname) |
435 self._urlpath = self._urljoin(*self._subdir, rname) | |
436 | |
437 def _urljoin(self, *args): | |
438 args = list(args) | |
439 if args[0] == '/': | |
440 args[0] = '' | |
441 return '/'.join(args) | |
442 | |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
443 def __call__(self): |
0 | 444 """ |
445 This gets called by the framework AFTER the page is launched. | |
446 """ | |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
447 target = None |
2 | 448 obj = self._class(bottle.request, bottle.response) |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
449 try: |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
450 obj.handle() |
2 | 451 return self._body.render(obj.export()).lstrip('\n') |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
452 except ForwardException as fwd: |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
453 target = fwd.target |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
454 if target is None: |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
455 raise TinCanError("{0}: unexpected null target".format(self._urlpath)) |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
456 # We get here if we are doing a server-side programmatic |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
457 # forward. |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
458 environ = bottle.request.environ |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
459 if _FORIG not in environ: |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
460 environ[_FORIG] = self._urlpath |
2 | 461 if _FLOOP not in environ: |
462 environ[_FLOOP] = set([self._urlpath]) | |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
463 elif target in environ[_FLOOP]: |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
464 raise TinCanError("{0}: forward loop detected".format(environ[_FORIG])) |
1
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
465 environ[_FLOOP].add(target) |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
466 environ['bottle.raw_path'] = target |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
467 environ['PATH_INFO'] = urllib.parse.quote(target) |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
468 route, args = self._app.router.match(environ) |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
469 environ['route.handle'] = environ['bottle.route'] = route |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
470 environ['route.url_args'] = args |
94b36e721500
Another check in to back stuff up.
David Barts <n5jrn@me.com>
parents:
0
diff
changeset
|
471 return route.call(**args) |
0 | 472 |
473 def _mkdict(self, obj): | |
474 ret = {} | |
475 for name in dir(obj): | |
476 if name.startswith('_'): | |
477 continue | |
478 value = getattr(obj, name) | |
479 if not callable(value): | |
480 ret[name] = value | |
481 return ret | |
2 | 482 |
483 # L a u n c h e r | |
484 | |
485 _WINF = "WEB-INF" | |
486 _BANNED = set([_WINF]) | |
487 | |
488 class _Launcher(object): | |
489 """ | |
490 Helper class for launching webapps. | |
491 """ | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
492 def __init__(self, fsroot, urlroot, tclass, logger): |
2 | 493 """ |
494 Lightweight constructor. The real action happens in .launch() below. | |
495 """ | |
496 self.fsroot = fsroot | |
497 self.urlroot = urlroot | |
498 self.tclass = tclass | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
499 self.logger = logger |
2 | 500 self.app = None |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
501 self.errors = 0 |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
502 self.debug = False |
2 | 503 |
504 def launch(self): | |
505 """ | |
506 Does the actual work of launching something. XXX - modifies sys.path | |
507 and never un-modifies it. | |
508 """ | |
509 # Sanity checks | |
510 if not self.urlroot.startswith("/"): | |
511 raise TinCanError("urlroot must be absolute") | |
512 if not os.path.isdir(self.fsroot): | |
513 raise TinCanError("no such directory: {0!r}".format(self.fsroot)) | |
514 # Make WEB-INF, if needed | |
515 winf = os.path.join(self.fsroot, _WINF) | |
516 lib = os.path.join(winf, "lib") | |
517 for i in [ winf, lib ]: | |
518 if not os.path.isdir(i): | |
519 os.mkdir(i) | |
520 # Add our private lib directory to sys.path | |
521 sys.path.insert(1, os.path.abspath(lib)) | |
522 # Do what we gotta do | |
523 self.app = TinCan() | |
524 self._launch([]) | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
525 return self |
2 | 526 |
527 def _launch(self, subdir): | |
528 for entry in os.listdir(os.path.join(self.fsroot, *subdir)): | |
529 if not subdir and entry in _BANNED: | |
530 continue | |
531 etype = os.stat(os.path.join(self.fsroot, *subdir, entry)).st_mode | |
532 if S_ISREG(etype): | |
533 ename, eext = os.path.splitext(entry) | |
534 if eext != _TEXTEN: | |
535 continue # only look at interesting files | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
536 route = _TinCanRoute(self, ename, subdir) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
537 try: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
538 route.launch() |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
539 except TinCanError as e: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
540 self.logger(str(e)) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
541 if self.debug: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
542 while e.__cause__ != None: |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
543 e = e.__cause__ |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
544 self.logger("\t{0}: {1!s}".format(e.__class__.__name__, e)) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
545 self.errors += 1 |
2 | 546 elif S_ISDIR(etype): |
547 self._launch(subdir + [entry]) | |
548 | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
549 def _logger(message): |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
550 sys.stderr.write(message) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
551 sys.stderr.write('\n') |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
552 |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
553 def launch(fsroot=None, urlroot='/', tclass=ChameleonTemplate, logger=_logger): |
2 | 554 """ |
555 Launch and return a TinCan webapp. Does not run the app; it is the | |
556 caller's responsibility to call app.run() | |
557 """ | |
558 if fsroot is None: | |
559 fsroot = os.getcwd() | |
4
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
560 launcher = _Launcher(fsroot, urlroot, tclass, logger) |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
561 # launcher.debug = True |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
562 launcher.launch() |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
563 return launcher.app, launcher.errors |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
564 |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
565 # XXX - We cannot implement a command-line launcher here; see the |
0d47859f792a
Finally got "hello, world" working. Still likely many bugs.
David Barts <n5jrn@me.com>
parents:
3
diff
changeset
|
566 # launcher script for why. |