1 #!/usr/bin/python
2 """
3 Web application
4 (from web.py)
5 """
6 import webapi as web
7 import webapi, wsgi, utils
8 import debugerror
9 from utils import lstrips, safeunicode
10 import sys
11
12 import urllib
13 import traceback
14 import itertools
15 import os
16 import re
17 import types
18 from exceptions import SystemExit
19
20 try:
21 import wsgiref.handlers
22 except ImportError:
23 pass # don't break people with old Pythons
24
25 __all__ = [
26 "application", "auto_application",
27 "subdir_application", "subdomain_application",
28 "loadhook", "unloadhook",
29 "autodelegate"
30 ]
31
32 class application:
33 """
34 Application to delegate requests based on path.
35
36 >>> urls = ("/hello", "hello")
37 >>> app = application(urls, globals())
38 >>> class hello:
39 ... def GET(self): return "hello"
40 >>>
41 >>> app.request("/hello").data
42 'hello'
43 """
44 def __init__(self, mapping=(), fvars={}, autoreload=None):
45 if autoreload is None:
46 autoreload = web.config.get('debug', False)
47 self.mapping = mapping
48 self.fvars = fvars
49 self.processors = []
50
51 self.add_processor(loadhook(self._load))
52 self.add_processor(unloadhook(self._unload))
53
54 if autoreload:
55 def main_module_name():
56 mod = sys.modules['__main__']
57 file = getattr(mod, '__file__', None) # make sure this works even from python interpreter
58 return file and os.path.splitext(os.path.basename(file))[0]
59
60 def modname(fvars):
61 """find name of the module name from fvars."""
62 file, name = fvars.get('__file__'), fvars.get('__name__')
63 if file is None or name is None:
64 return None
65
66 if name == '__main__':
67 # Since the __main__ module can't be reloaded, the module has
68 # to be imported using its file name.
69 name = main_module_name()
70 return name
71
72 mapping_name = utils.dictfind(fvars, mapping)
73 module_name = modname(fvars)
74
75 def reload_mapping():
76 """loadhook to reload mapping and fvars."""
77 mod = __import__(module_name)
78 mapping = getattr(mod, mapping_name, None)
79 if mapping:
80 self.fvars = mod.__dict__
81 self.mapping = mapping
82
83 self.add_processor(loadhook(Reloader()))
84 if mapping_name and module_name:
85 self.add_processor(loadhook(reload_mapping))
86
87 # load __main__ module usings its filename, so that it can be reloaded.
88 if main_module_name() and '__main__' in sys.argv:
89 try:
90 __import__(main_module_name())
91 except ImportError:
92 pass
93
94 def _load(self):
95 web.ctx.app_stack.append(self)
96
97 def _unload(self):
98 web.ctx.app_stack = web.ctx.app_stack[:-1]
99
100 if web.ctx.app_stack:
101 # this is a sub-application, revert ctx to earlier state.
102 oldctx = web.ctx.get('_oldctx')
103 if oldctx:
104 web.ctx.home = oldctx.home
105 web.ctx.homepath = oldctx.homepath
106 web.ctx.path = oldctx.path
107 web.ctx.fullpath = oldctx.fullpath
108
109 def _cleanup(self):
110 # Threads can be recycled by WSGI servers.
111 # Clearing up all thread-local state to avoid interefereing with subsequent requests.
112 utils.ThreadedDict.clear_all()
113
114 def add_mapping(self, pattern, classname):
115 self.mapping += (pattern, classname)
116
117 def add_processor(self, processor):
118 """
119 Adds a processor to the application.
120
121 >>> urls = ("/(.*)", "echo")
122 >>> app = application(urls, globals())
123 >>> class echo:
124 ... def GET(self, name): return name
125 ...
126 >>>
127 >>> def hello(handler): return "hello, " + handler()
128 ...
129 >>> app.add_processor(hello)
130 >>> app.request("/web.py").data
131 'hello, web.py'
132 """
133 self.processors.append(processor)
134
135 def request(self, localpart='/', method='GET', data=None,
136 host="0.0.0.0:8080", headers=None, https=False, **kw):
137 """Makes request to this application for the specified path and method.
138 Response will be a storage object with data, status and headers.
139
140 >>> urls = ("/hello", "hello")
141 >>> app = application(urls, globals())
142 >>> class hello:
143 ... def GET(self):
144 ... web.header('Content-Type', 'text/plain')
145 ... return "hello"
146 ...
147 >>> response = app.request("/hello")
148 >>> response.data
149 'hello'
150 >>> response.status
151 '200 OK'
152 >>> response.headers['Content-Type']
153 'text/plain'
154
155 To use https, use https=True.
156
157 >>> urls = ("/redirect", "redirect")
158 >>> app = application(urls, globals())
159 >>> class redirect:
160 ... def GET(self): raise web.seeother("/foo")
161 ...
162 >>> response = app.request("/redirect")
163 >>> response.headers['Location']
164 '[url]http://0.0.0.0:8080/foo[/url]'
165 >>> response = app.request("/redirect", https=True)
166 >>> response.headers['Location']
167 '[url]https://0.0.0.0:8080/foo[/url]'
168
169 The headers argument specifies HTTP headers as a mapping object
170 such as a dict.
171
172 >>> urls = ('/ua', 'uaprinter')
173 >>> class uaprinter:
174 ... def GET(self):
175 ... return 'your user-agent is ' + web.ctx.env['HTTP_USER_AGENT']
176 ...
177 >>> app = application(urls, globals())
178 >>> app.request('/ua', headers = {
179 ... 'User-Agent': 'a small jumping bean/1.0 (compatible)'
180 ... }).data
181 'your user-agent is a small jumping bean/1.0 (compatible)'
182
183 """
184 path, maybe_query = urllib.splitquery(localpart)
185 query = maybe_query or ""
186
187 if 'env' in kw:
188 env = kw['env']
189 else:
190 env = {}
191 env = dict(env, HTTP_HOST=host, REQUEST_METHOD=method, PATH_INFO=path, QUERY_STRING=query, HTTPS=str(https))
192 headers = headers or {}
193
224
225 def handle(self):
226 fn, args = self._match(self.mapping, web.ctx.path)
227 #return self._delegate(fn, self.fvars, args)
228
229 def handle_with_processors(self):
230 def process(processors):
231 try:
232 if processors:
233 p, processors = processors[0], processors[1:]
234 return p(lambda: process(processors))
235 else:
236 # return self.handle()
237 except web.HTTPError:
238 raise
239 except (KeyboardInterrupt, SystemExit):
240 raise
241 except:
242 print >> web.debug, traceback.format_exc()
243 raise self.internalerror()
244
245 # processors must be applied in the resvere order. (??)
246 return process(self.processors)
247
248 def wsgifunc(self, *middleware):
249 """Returns a WSGI-compatible function for this application."""
250 def peep(iterator):
251 """Peeps into an iterator by doing an iteration
252 and returns an equivalent iterator.
253 """
254 # wsgi requires the headers first
194 for k, v in headers.items():
195 env['HTTP_' + k.upper().replace('-', '_')] = v
196
197 if 'HTTP_CONTENT_LENGTH' in env:
198 env['CONTENT_LENGTH'] = env.pop('HTTP_CONTENT_LENGTH')
199
200 if 'HTTP_CONTENT_TYPE' in env:
201 env['CONTENT_TYPE'] = env.pop('HTTP_CONTENT_TYPE')
202
203 if method in ["POST", "PUT"]:
204 data = data or ''
205 import StringIO
206 if isinstance(data, dict):
207 q = urllib.urlencode(data)
208 else:
209 q = data
210 env['wsgi.input'] = StringIO.StringIO(q)
211 if not env.get('CONTENT_TYPE', '').lower().startswith('multipart/') and 'CONTENT_LENGTH' not in env:
212 env['CONTENT_LENGTH'] = len(q)
213 response = web.storage()
214 def start_response(status, headers):
215 response.status = status
216 response.headers = dict(headers)
217 response.header_items = headers
218 response.data = "".join(self.wsgifunc()(env, start_response))
219 return response
220
221 def browser(self):
222 import browser
223 return browser.AppBrowser(self)
224
225 def handle(self):
226 fn, args = self._match(self.mapping, web.ctx.path)
227 #return self._delegate(fn, self.fvars, args)
228
229 def handle_with_processors(self):
230 def process(processors):
231 try:
232 if processors:
233 p, processors = processors[0], processors[1:]
234 return p(lambda: process(processors))
235 else:
236 # return self.handle()
237 except web.HTTPError:
238 raise
239 except (KeyboardInterrupt, SystemExit):
240 raise
241 except:
242 print >> web.debug, traceback.format_exc()
243 raise self.internalerror()
244
245 # processors must be applied in the resvere order. (??)
246 return process(self.processors)
247
248 def wsgifunc(self, *middleware):
249 """Returns a WSGI-compatible function for this application."""
250 def peep(iterator):
251 """Peeps into an iterator by doing an iteration
252 and returns an equivalent iterator.
253 """
254 # wsgi requires the headers first
255 # so we need to do an iteration
256 # and save the result for later
257 try:
258 firstchunk = iterator.next()
259 except StopIteration: