OLD | NEW |
(Empty) | |
| 1 """CherryPy Application and Tree objects.""" |
| 2 |
| 3 import os |
| 4 import sys |
| 5 |
| 6 import cherrypy |
| 7 from cherrypy._cpcompat import ntou, py3k |
| 8 from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools |
| 9 from cherrypy.lib import httputil |
| 10 |
| 11 |
| 12 class Application(object): |
| 13 """A CherryPy Application. |
| 14 |
| 15 Servers and gateways should not instantiate Request objects directly. |
| 16 Instead, they should ask an Application object for a request object. |
| 17 |
| 18 An instance of this class may also be used as a WSGI callable |
| 19 (WSGI application object) for itself. |
| 20 """ |
| 21 |
| 22 root = None |
| 23 """The top-most container of page handlers for this app. Handlers should |
| 24 be arranged in a hierarchy of attributes, matching the expected URI |
| 25 hierarchy; the default dispatcher then searches this hierarchy for a |
| 26 matching handler. When using a dispatcher other than the default, |
| 27 this value may be None.""" |
| 28 |
| 29 config = {} |
| 30 """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict |
| 31 of {key: value} pairs.""" |
| 32 |
| 33 namespaces = _cpconfig.NamespaceSet() |
| 34 toolboxes = {'tools': cherrypy.tools} |
| 35 |
| 36 log = None |
| 37 """A LogManager instance. See _cplogging.""" |
| 38 |
| 39 wsgiapp = None |
| 40 """A CPWSGIApp instance. See _cpwsgi.""" |
| 41 |
| 42 request_class = _cprequest.Request |
| 43 response_class = _cprequest.Response |
| 44 |
| 45 relative_urls = False |
| 46 |
| 47 def __init__(self, root, script_name="", config=None): |
| 48 self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) |
| 49 self.root = root |
| 50 self.script_name = script_name |
| 51 self.wsgiapp = _cpwsgi.CPWSGIApp(self) |
| 52 |
| 53 self.namespaces = self.namespaces.copy() |
| 54 self.namespaces["log"] = lambda k, v: setattr(self.log, k, v) |
| 55 self.namespaces["wsgi"] = self.wsgiapp.namespace_handler |
| 56 |
| 57 self.config = self.__class__.config.copy() |
| 58 if config: |
| 59 self.merge(config) |
| 60 |
| 61 def __repr__(self): |
| 62 return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__, |
| 63 self.root, self.script_name) |
| 64 |
| 65 script_name_doc = """The URI "mount point" for this app. A mount point is th
at portion of |
| 66 the URI which is constant for all URIs that are serviced by this |
| 67 application; it does not include scheme, host, or proxy ("virtual host") |
| 68 portions of the URI. |
| 69 |
| 70 For example, if script_name is "/my/cool/app", then the URL |
| 71 "http://www.example.com/my/cool/app/page1" might be handled by a |
| 72 "page1" method on the root object. |
| 73 |
| 74 The value of script_name MUST NOT end in a slash. If the script_name |
| 75 refers to the root of the URI, it MUST be an empty string (not "/"). |
| 76 |
| 77 If script_name is explicitly set to None, then the script_name will be |
| 78 provided for each call from request.wsgi_environ['SCRIPT_NAME']. |
| 79 """ |
| 80 def _get_script_name(self): |
| 81 if self._script_name is None: |
| 82 # None signals that the script name should be pulled from WSGI envir
on. |
| 83 return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("
/") |
| 84 return self._script_name |
| 85 def _set_script_name(self, value): |
| 86 if value: |
| 87 value = value.rstrip("/") |
| 88 self._script_name = value |
| 89 script_name = property(fget=_get_script_name, fset=_set_script_name, |
| 90 doc=script_name_doc) |
| 91 |
| 92 def merge(self, config): |
| 93 """Merge the given config into self.config.""" |
| 94 _cpconfig.merge(self.config, config) |
| 95 |
| 96 # Handle namespaces specified in config. |
| 97 self.namespaces(self.config.get("/", {})) |
| 98 |
| 99 def find_config(self, path, key, default=None): |
| 100 """Return the most-specific value for key along path, or default.""" |
| 101 trail = path or "/" |
| 102 while trail: |
| 103 nodeconf = self.config.get(trail, {}) |
| 104 |
| 105 if key in nodeconf: |
| 106 return nodeconf[key] |
| 107 |
| 108 lastslash = trail.rfind("/") |
| 109 if lastslash == -1: |
| 110 break |
| 111 elif lastslash == 0 and trail != "/": |
| 112 trail = "/" |
| 113 else: |
| 114 trail = trail[:lastslash] |
| 115 |
| 116 return default |
| 117 |
| 118 def get_serving(self, local, remote, scheme, sproto): |
| 119 """Create and return a Request and Response object.""" |
| 120 req = self.request_class(local, remote, scheme, sproto) |
| 121 req.app = self |
| 122 |
| 123 for name, toolbox in self.toolboxes.items(): |
| 124 req.namespaces[name] = toolbox |
| 125 |
| 126 resp = self.response_class() |
| 127 cherrypy.serving.load(req, resp) |
| 128 cherrypy.engine.publish('acquire_thread') |
| 129 cherrypy.engine.publish('before_request') |
| 130 |
| 131 return req, resp |
| 132 |
| 133 def release_serving(self): |
| 134 """Release the current serving (request and response).""" |
| 135 req = cherrypy.serving.request |
| 136 |
| 137 cherrypy.engine.publish('after_request') |
| 138 |
| 139 try: |
| 140 req.close() |
| 141 except: |
| 142 cherrypy.log(traceback=True, severity=40) |
| 143 |
| 144 cherrypy.serving.clear() |
| 145 |
| 146 def __call__(self, environ, start_response): |
| 147 return self.wsgiapp(environ, start_response) |
| 148 |
| 149 |
| 150 class Tree(object): |
| 151 """A registry of CherryPy applications, mounted at diverse points. |
| 152 |
| 153 An instance of this class may also be used as a WSGI callable |
| 154 (WSGI application object), in which case it dispatches to all |
| 155 mounted apps. |
| 156 """ |
| 157 |
| 158 apps = {} |
| 159 """ |
| 160 A dict of the form {script name: application}, where "script name" |
| 161 is a string declaring the URI mount point (no trailing slash), and |
| 162 "application" is an instance of cherrypy.Application (or an arbitrary |
| 163 WSGI callable if you happen to be using a WSGI server).""" |
| 164 |
| 165 def __init__(self): |
| 166 self.apps = {} |
| 167 |
| 168 def mount(self, root, script_name="", config=None): |
| 169 """Mount a new app from a root object, script_name, and config. |
| 170 |
| 171 root |
| 172 An instance of a "controller class" (a collection of page |
| 173 handler methods) which represents the root of the application. |
| 174 This may also be an Application instance, or None if using |
| 175 a dispatcher other than the default. |
| 176 |
| 177 script_name |
| 178 A string containing the "mount point" of the application. |
| 179 This should start with a slash, and be the path portion of the |
| 180 URL at which to mount the given root. For example, if root.index() |
| 181 will handle requests to "http://www.example.com:8080/dept/app1/", |
| 182 then the script_name argument would be "/dept/app1". |
| 183 |
| 184 It MUST NOT end in a slash. If the script_name refers to the |
| 185 root of the URI, it MUST be an empty string (not "/"). |
| 186 |
| 187 config |
| 188 A file or dict containing application config. |
| 189 """ |
| 190 if script_name is None: |
| 191 raise TypeError( |
| 192 "The 'script_name' argument may not be None. Application " |
| 193 "objects may, however, possess a script_name of None (in " |
| 194 "order to inpect the WSGI environ for SCRIPT_NAME upon each " |
| 195 "request). You cannot mount such Applications on this Tree; " |
| 196 "you must pass them to a WSGI server interface directly.") |
| 197 |
| 198 # Next line both 1) strips trailing slash and 2) maps "/" -> "". |
| 199 script_name = script_name.rstrip("/") |
| 200 |
| 201 if isinstance(root, Application): |
| 202 app = root |
| 203 if script_name != "" and script_name != app.script_name: |
| 204 raise ValueError("Cannot specify a different script name and " |
| 205 "pass an Application instance to cherrypy.mount
") |
| 206 script_name = app.script_name |
| 207 else: |
| 208 app = Application(root, script_name) |
| 209 |
| 210 # If mounted at "", add favicon.ico |
| 211 if (script_name == "" and root is not None |
| 212 and not hasattr(root, "favicon_ico")): |
| 213 favicon = os.path.join(os.getcwd(), os.path.dirname(__file__), |
| 214 "favicon.ico") |
| 215 root.favicon_ico = tools.staticfile.handler(favicon) |
| 216 |
| 217 if config: |
| 218 app.merge(config) |
| 219 |
| 220 self.apps[script_name] = app |
| 221 |
| 222 return app |
| 223 |
| 224 def graft(self, wsgi_callable, script_name=""): |
| 225 """Mount a wsgi callable at the given script_name.""" |
| 226 # Next line both 1) strips trailing slash and 2) maps "/" -> "". |
| 227 script_name = script_name.rstrip("/") |
| 228 self.apps[script_name] = wsgi_callable |
| 229 |
| 230 def script_name(self, path=None): |
| 231 """The script_name of the app at the given path, or None. |
| 232 |
| 233 If path is None, cherrypy.request is used. |
| 234 """ |
| 235 if path is None: |
| 236 try: |
| 237 request = cherrypy.serving.request |
| 238 path = httputil.urljoin(request.script_name, |
| 239 request.path_info) |
| 240 except AttributeError: |
| 241 return None |
| 242 |
| 243 while True: |
| 244 if path in self.apps: |
| 245 return path |
| 246 |
| 247 if path == "": |
| 248 return None |
| 249 |
| 250 # Move one node up the tree and try again. |
| 251 path = path[:path.rfind("/")] |
| 252 |
| 253 def __call__(self, environ, start_response): |
| 254 # If you're calling this, then you're probably setting SCRIPT_NAME |
| 255 # to '' (some WSGI servers always set SCRIPT_NAME to ''). |
| 256 # Try to look up the app using the full path. |
| 257 env1x = environ |
| 258 if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): |
| 259 env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) |
| 260 path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), |
| 261 env1x.get('PATH_INFO', '')) |
| 262 sn = self.script_name(path or "/") |
| 263 if sn is None: |
| 264 start_response('404 Not Found', []) |
| 265 return [] |
| 266 |
| 267 app = self.apps[sn] |
| 268 |
| 269 # Correct the SCRIPT_NAME and PATH_INFO environ entries. |
| 270 environ = environ.copy() |
| 271 if not py3k: |
| 272 if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): |
| 273 # Python 2/WSGI u.0: all strings MUST be of type unicode |
| 274 enc = environ[ntou('wsgi.url_encoding')] |
| 275 environ[ntou('SCRIPT_NAME')] = sn.decode(enc) |
| 276 environ[ntou('PATH_INFO')] = path[len(sn.rstrip("/")):].decode(e
nc) |
| 277 else: |
| 278 # Python 2/WSGI 1.x: all strings MUST be of type str |
| 279 environ['SCRIPT_NAME'] = sn |
| 280 environ['PATH_INFO'] = path[len(sn.rstrip("/")):] |
| 281 else: |
| 282 if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): |
| 283 # Python 3/WSGI u.0: all strings MUST be full unicode |
| 284 environ['SCRIPT_NAME'] = sn |
| 285 environ['PATH_INFO'] = path[len(sn.rstrip("/")):] |
| 286 else: |
| 287 # Python 3/WSGI 1.x: all strings MUST be ISO-8859-1 str |
| 288 environ['SCRIPT_NAME'] = sn.encode('utf-8').decode('ISO-8859-1') |
| 289 environ['PATH_INFO'] = path[len(sn.rstrip("/")):].encode('utf-8'
).decode('ISO-8859-1') |
| 290 return app(environ, start_response) |
OLD | NEW |