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