Imported Upstream version 0.2
SVN-Git Migration
8 years ago
5 | 5 | - Georg Brandl |
6 | 6 | - Leif K-Brooks <eurleif@gmail.com> |
7 | 7 | - Marek Kubica |
8 | - Thomas Johansson | |
9 | - Marian Sigler |
1 | 1 | ================== |
2 | 2 | |
3 | 3 | |
4 | Version 0.2 | |
5 | ----------- | |
6 | (codename Faustkeil, estimated release date Feb 14th 2008) | |
7 | ||
8 | - Added `AnyConverter` to the routing system. | |
9 | - Added `werkzeug.contrib.securecookie` | |
10 | - Exceptions have a ``get_response()`` method that return a resposne object | |
11 | - fixed the path ordering bug (#293), thanks Thomas Johansson | |
12 | - `BaseReporterStream` is now part of the werkzeug contrib module. With | |
13 | Werkzeug 0.3 onwards you will have to import it from there. | |
14 | - added `DispatcherMiddleware`. | |
15 | - `RequestRedirect` is now a subclass of `HTTPException` and uses a | |
16 | 301 status code instead of 302. | |
17 | - `url_encode` and `url_decode` can optionally treat keys as unicode strings | |
18 | now too. | |
19 | - `werkzeug.script` has a different caller format for boolean arguments now. | |
20 | - renamed `lazy_property` to `cached_property`. | |
21 | - added `import_string`. | |
22 | - added is_* properties to request objects. | |
23 | - added `empty()` method to routing rules. | |
24 | - added `werkzeug.contrib.profiler`. | |
25 | - added `extends` to `Headers`. | |
26 | - added `dump_cookie` and `parse_cookie`. | |
27 | - added `as_tuple` to the `Client`. | |
28 | - added `werkzeug.contrib.testtools`. | |
29 | - added `werkzeug.unescape` | |
30 | - added `BaseResponse.freeze` | |
31 | - added `werkzeug.contrib.atom` | |
32 | - the HTTPExceptions accept an argument `description` now which overrides the | |
33 | default description. | |
34 | - the `MapAdapter` has a default for path info now. If you use | |
35 | `bind_to_environ` you don't have to pass the path later. | |
36 | - the wsgiref subclass werkzeug uses for the dev server does not use direct | |
37 | sys.stderr logging any more but a logger called "werkzeug". | |
38 | - implemented `Href`. | |
39 | - implemented `find_modules` | |
40 | - refactored request and response objects into base objects, mixins and | |
41 | full featured subclasses that implement all mixins. | |
42 | - added simple user agent parser | |
43 | - werkzeug's routing raises `MethodNotAllowed` now if it matches a | |
44 | rule but for a different method. | |
45 | - many fixes and small improvements | |
46 | ||
47 | ||
4 | 48 | Version 0.1 |
5 | 49 | ----------- |
6 | (codename Wictorinoxger, release Dec 9th 2007) | |
50 | (codename Wictorinoxger, released Dec 9th 2007) | |
7 | 51 | |
8 | 52 | - Initial release |
0 | 0 | include Makefile CHANGES LICENSE AUTHORS TODO ez_setup.py |
1 | recursive-include werkzeug.debug shared/* | |
1 | recursive-include werkzeug debug/shared/* | |
2 | 2 | recursive-include tests * |
8 | 8 | # |
9 | 9 | |
10 | 10 | documentation: |
11 | @(cd docs; ./generate.py) | |
12 | ||
13 | webpage: | |
14 | @(cd ../www; ./generate.py) | |
11 | @(cd docs; python ./generate.py) | |
15 | 12 | |
16 | 13 | test: |
17 | 14 | @(cd tests; py.test $(TESTS)) |
0 | 0 | Metadata-Version: 1.0 |
1 | 1 | Name: Werkzeug |
2 | Version: 0.1 | |
2 | Version: 0.2 | |
3 | 3 | Summary: The Swiss Army knife of Python web development |
4 | 4 | Home-page: http://werkzeug.pocoo.org/ |
5 | 5 | Author: Armin Ronacher |
6 | 6 | Author-email: armin.ronacher@active-4.com |
7 | 7 | License: BSD |
8 | Download-URL: http://trac.pocoo.org/repos/werkzeug/trunk | |
9 | Description: werkzeug | |
10 | ~~~~~~~~ | |
8 | Download-URL: http://werkzeug.pocoo.org/download | |
9 | Description: | |
10 | Werkzeug | |
11 | ======== | |
11 | 12 | |
12 | Werkzeug is the Swiss Army(r) knife of Python web development. | |
13 | Werkzeug started as simple collection of various utilities for WSGI | |
14 | applications and has become one of the most advanced WSGI utility | |
15 | modules. It includes a powerful debugger, full featured request and | |
16 | response objects, HTTP utilities to handle entity tags, cache control | |
17 | headers, HTTP dates, cookie handling, file uploads, a powerful URL | |
18 | routing system and a bunch of community contributed addon modules. | |
13 | 19 | |
14 | It provides useful classes and functions for any WSGI application take | |
15 | make life much easier. All of the provided classes are independed from | |
16 | each other so you can mix it with any other library. | |
20 | Werkzeug is unicode aware and doesn't enforce a specific template | |
21 | engine, database adapter or anything else. It doesn't even enforce | |
22 | a specific way of handling requests and leaves all that up to the | |
23 | developer. It's most useful for end user applications which should work | |
24 | on as many server environments as possible (such as blogs, wikis, | |
25 | bulletin boards, etc.). | |
26 | ||
27 | Details and example applications are available on the | |
28 | `Werkzeug website <http://werkzeug.pocoo.org/>`_. | |
17 | 29 | |
18 | 30 | |
19 | Builtin blades^Wfeatures | |
20 | ~~~~~~~~~~~~~~~~~~~~~~~~ | |
31 | Features | |
32 | -------- | |
21 | 33 | |
22 | **Request / Response objects** | |
34 | - unicode awareness | |
23 | 35 | |
24 | These objects wrap the WSGI `environ` and `start_response` objects. | |
25 | They handle unicode conversion, form data parsing, cookie management | |
26 | and much more in a django like manner. | |
36 | - request and response objects | |
27 | 37 | |
28 | Just subclass them and hook your own features in. | |
38 | - various utility functions for dealing with HTTP headers such as | |
39 | `Accept` and `Cache-Control` headers. | |
29 | 40 | |
30 | **Reporter stream** | |
41 | - thread local objects with proper cleanup at request end | |
31 | 42 | |
32 | A class that can wrap a `wsgi.input` stream so that it reports it's | |
33 | progress into the active session, a file on the filesystem etc. This | |
34 | is very useful if you want to give your users a visual feedback for | |
35 | file uploads using AJAX. | |
43 | - an interactive debugger | |
36 | 44 | |
37 | **Application debugger middleware** | |
45 | - wrapper around wsgiref that works around some of the limitations | |
46 | and bugs, adds threading and fork support for test environments | |
47 | and adds an automatic reloader. | |
38 | 48 | |
39 | If you want to debug your WSGI application you can hook in the | |
40 | `DebuggedApplication` middleware that allows you to inspect the frames | |
41 | of tracebacks either by looking at the current locals and sourcecode | |
42 | or starting an interactive shell in one of the frames. | |
49 | - a flexible URL routing system with REST support. | |
43 | 50 | |
44 | **Shared data middleware** | |
45 | ||
46 | In production environments static data is usually served by a | |
47 | lightweight webserver like lighttpd or nginx. But during development | |
48 | it makes no sense to install another service on the computer so the | |
49 | `SharedDataMiddleware` can serve static files in your WSGI | |
50 | application. | |
51 | ||
52 | **Unicode aware data processing** | |
53 | ||
54 | The utils package contains functions that work like their counterparts | |
55 | in the builtin `urllib` or `cgi` module but are unicode aware. Per | |
56 | default they expect utf-8 strings like the request/response objects | |
57 | but you can pass an encoding to the too. | |
58 | ||
59 | **Mini template engine** | |
60 | ||
61 | For small projects you often face the problem that a real template | |
62 | engine means another requirement but the builtin string formattings | |
63 | (or string template) operations are not enough for the application. | |
64 | Werkzeug provides a minimal template engine that looks and behaves | |
65 | like the e-ruby template engine. | |
66 | ||
67 | **Context Locals** | |
68 | ||
69 | The `Local` object works pretty much like a normal thread local but | |
70 | it has support for py.magic greenlets too. Additionally there is a | |
71 | `LocalManager` that allows you to clean up all the context locals you | |
72 | have instanciated. | |
73 | ||
74 | **Test utilities** | |
75 | ||
76 | Werkzeug provides a `Client` class that can be used to test | |
77 | applications. Just instanciate it with the app and fire virtual | |
78 | requests. | |
51 | - fully WSGI compatible | |
79 | 52 | |
80 | 53 | |
81 | :copyright: 2007 by Armin Ronacher. | |
82 | :license: BSD, see LICENSE for more details. | |
54 | Development Version | |
55 | ------------------- | |
56 | ||
57 | The `Werkzeug tip <http://dev.pocoo.org/hg/werkzeug-main/archive/tip.zip#egg=Werkzeug-dev` | |
58 | is installable via `easy_install` with ``easy_install Werkzeug==dev``. | |
59 | ||
83 | 60 | Platform: any |
84 | Classifier: Development Status :: 2 - Pre-Alpha | |
61 | Classifier: Development Status :: 3 - Alpha | |
85 | 62 | Classifier: Environment :: Web Environment |
86 | 63 | Classifier: Intended Audience :: Developers |
87 | 64 | Classifier: License :: OSI Approved :: BSD License |
0 | 0 | Metadata-Version: 1.0 |
1 | 1 | Name: Werkzeug |
2 | Version: 0.1 | |
2 | Version: 0.2 | |
3 | 3 | Summary: The Swiss Army knife of Python web development |
4 | 4 | Home-page: http://werkzeug.pocoo.org/ |
5 | 5 | Author: Armin Ronacher |
6 | 6 | Author-email: armin.ronacher@active-4.com |
7 | 7 | License: BSD |
8 | Download-URL: http://trac.pocoo.org/repos/werkzeug/trunk | |
9 | Description: werkzeug | |
10 | ~~~~~~~~ | |
8 | Download-URL: http://werkzeug.pocoo.org/download | |
9 | Description: | |
10 | Werkzeug | |
11 | ======== | |
11 | 12 | |
12 | Werkzeug is the Swiss Army(r) knife of Python web development. | |
13 | Werkzeug started as simple collection of various utilities for WSGI | |
14 | applications and has become one of the most advanced WSGI utility | |
15 | modules. It includes a powerful debugger, full featured request and | |
16 | response objects, HTTP utilities to handle entity tags, cache control | |
17 | headers, HTTP dates, cookie handling, file uploads, a powerful URL | |
18 | routing system and a bunch of community contributed addon modules. | |
13 | 19 | |
14 | It provides useful classes and functions for any WSGI application take | |
15 | make life much easier. All of the provided classes are independed from | |
16 | each other so you can mix it with any other library. | |
20 | Werkzeug is unicode aware and doesn't enforce a specific template | |
21 | engine, database adapter or anything else. It doesn't even enforce | |
22 | a specific way of handling requests and leaves all that up to the | |
23 | developer. It's most useful for end user applications which should work | |
24 | on as many server environments as possible (such as blogs, wikis, | |
25 | bulletin boards, etc.). | |
26 | ||
27 | Details and example applications are available on the | |
28 | `Werkzeug website <http://werkzeug.pocoo.org/>`_. | |
17 | 29 | |
18 | 30 | |
19 | Builtin blades^Wfeatures | |
20 | ~~~~~~~~~~~~~~~~~~~~~~~~ | |
31 | Features | |
32 | -------- | |
21 | 33 | |
22 | **Request / Response objects** | |
34 | - unicode awareness | |
23 | 35 | |
24 | These objects wrap the WSGI `environ` and `start_response` objects. | |
25 | They handle unicode conversion, form data parsing, cookie management | |
26 | and much more in a django like manner. | |
36 | - request and response objects | |
27 | 37 | |
28 | Just subclass them and hook your own features in. | |
38 | - various utility functions for dealing with HTTP headers such as | |
39 | `Accept` and `Cache-Control` headers. | |
29 | 40 | |
30 | **Reporter stream** | |
41 | - thread local objects with proper cleanup at request end | |
31 | 42 | |
32 | A class that can wrap a `wsgi.input` stream so that it reports it's | |
33 | progress into the active session, a file on the filesystem etc. This | |
34 | is very useful if you want to give your users a visual feedback for | |
35 | file uploads using AJAX. | |
43 | - an interactive debugger | |
36 | 44 | |
37 | **Application debugger middleware** | |
45 | - wrapper around wsgiref that works around some of the limitations | |
46 | and bugs, adds threading and fork support for test environments | |
47 | and adds an automatic reloader. | |
38 | 48 | |
39 | If you want to debug your WSGI application you can hook in the | |
40 | `DebuggedApplication` middleware that allows you to inspect the frames | |
41 | of tracebacks either by looking at the current locals and sourcecode | |
42 | or starting an interactive shell in one of the frames. | |
49 | - a flexible URL routing system with REST support. | |
43 | 50 | |
44 | **Shared data middleware** | |
45 | ||
46 | In production environments static data is usually served by a | |
47 | lightweight webserver like lighttpd or nginx. But during development | |
48 | it makes no sense to install another service on the computer so the | |
49 | `SharedDataMiddleware` can serve static files in your WSGI | |
50 | application. | |
51 | ||
52 | **Unicode aware data processing** | |
53 | ||
54 | The utils package contains functions that work like their counterparts | |
55 | in the builtin `urllib` or `cgi` module but are unicode aware. Per | |
56 | default they expect utf-8 strings like the request/response objects | |
57 | but you can pass an encoding to the too. | |
58 | ||
59 | **Mini template engine** | |
60 | ||
61 | For small projects you often face the problem that a real template | |
62 | engine means another requirement but the builtin string formattings | |
63 | (or string template) operations are not enough for the application. | |
64 | Werkzeug provides a minimal template engine that looks and behaves | |
65 | like the e-ruby template engine. | |
66 | ||
67 | **Context Locals** | |
68 | ||
69 | The `Local` object works pretty much like a normal thread local but | |
70 | it has support for py.magic greenlets too. Additionally there is a | |
71 | `LocalManager` that allows you to clean up all the context locals you | |
72 | have instanciated. | |
73 | ||
74 | **Test utilities** | |
75 | ||
76 | Werkzeug provides a `Client` class that can be used to test | |
77 | applications. Just instanciate it with the app and fire virtual | |
78 | requests. | |
51 | - fully WSGI compatible | |
79 | 52 | |
80 | 53 | |
81 | :copyright: 2007 by Armin Ronacher. | |
82 | :license: BSD, see LICENSE for more details. | |
54 | Development Version | |
55 | ------------------- | |
56 | ||
57 | The `Werkzeug tip <http://dev.pocoo.org/hg/werkzeug-main/archive/tip.zip#egg=Werkzeug-dev` | |
58 | is installable via `easy_install` with ``easy_install Werkzeug==dev``. | |
59 | ||
83 | 60 | Platform: any |
84 | Classifier: Development Status :: 2 - Pre-Alpha | |
61 | Classifier: Development Status :: 3 - Alpha | |
85 | 62 | Classifier: Environment :: Web Environment |
86 | 63 | Classifier: Intended Audience :: Developers |
87 | 64 | Classifier: License :: OSI Approved :: BSD License |
20 | 20 | tests/test_utils.pyc |
21 | 21 | tests/test_wrappers.py |
22 | 22 | tests/test_wrappers.pyc |
23 | tests/contrib/test_testtools.py | |
24 | tests/contrib/test_testtools.pyc | |
23 | 25 | tests/res/test.txt |
24 | 26 | werkzeug/__init__.py |
25 | 27 | werkzeug/exceptions.py |
31 | 33 | werkzeug/templates.py |
32 | 34 | werkzeug/test.py |
33 | 35 | werkzeug/testapp.py |
36 | werkzeug/useragents.py | |
34 | 37 | werkzeug/utils.py |
35 | 38 | werkzeug/wrappers.py |
36 | 39 | werkzeug/contrib/__init__.py |
40 | werkzeug/contrib/atom.py | |
41 | werkzeug/contrib/cache.py | |
37 | 42 | werkzeug/contrib/iterio.py |
38 | 43 | werkzeug/contrib/jsrouting.py |
39 | 44 | werkzeug/contrib/kickstart.py |
40 | 45 | werkzeug/contrib/limiter.py |
46 | werkzeug/contrib/profiler.py | |
47 | werkzeug/contrib/reporterstream.py | |
48 | werkzeug/contrib/securecookie.py | |
41 | 49 | werkzeug/contrib/sessions.py |
50 | werkzeug/contrib/testtools.py | |
42 | 51 | werkzeug/debug/__init__.py |
43 | 52 | werkzeug/debug/render.py |
44 | 53 | werkzeug/debug/util.py |
54 | werkzeug/debug/shared/body.tmpl | |
55 | werkzeug/debug/shared/codetable.tmpl | |
56 | werkzeug/debug/shared/debugger.js | |
57 | werkzeug/debug/shared/jquery.js | |
58 | werkzeug/debug/shared/style.css | |
59 | werkzeug/debug/shared/vartable.tmpl |
0 | 0 | # -*- coding: utf-8 -*- |
1 | import werkzeug | |
1 | """ | |
2 | Werkzeug | |
3 | ======== | |
4 | ||
5 | Werkzeug started as simple collection of various utilities for WSGI | |
6 | applications and has become one of the most advanced WSGI utility | |
7 | modules. It includes a powerful debugger, full featured request and | |
8 | response objects, HTTP utilities to handle entity tags, cache control | |
9 | headers, HTTP dates, cookie handling, file uploads, a powerful URL | |
10 | routing system and a bunch of community contributed addon modules. | |
11 | ||
12 | Werkzeug is unicode aware and doesn't enforce a specific template | |
13 | engine, database adapter or anything else. It doesn't even enforce | |
14 | a specific way of handling requests and leaves all that up to the | |
15 | developer. It's most useful for end user applications which should work | |
16 | on as many server environments as possible (such as blogs, wikis, | |
17 | bulletin boards, etc.). | |
18 | ||
19 | Details and example applications are available on the | |
20 | `Werkzeug website <http://werkzeug.pocoo.org/>`_. | |
21 | ||
22 | ||
23 | Features | |
24 | -------- | |
25 | ||
26 | - unicode awareness | |
27 | ||
28 | - request and response objects | |
29 | ||
30 | - various utility functions for dealing with HTTP headers such as | |
31 | `Accept` and `Cache-Control` headers. | |
32 | ||
33 | - thread local objects with proper cleanup at request end | |
34 | ||
35 | - an interactive debugger | |
36 | ||
37 | - wrapper around wsgiref that works around some of the limitations | |
38 | and bugs, adds threading and fork support for test environments | |
39 | and adds an automatic reloader. | |
40 | ||
41 | - a flexible URL routing system with REST support. | |
42 | ||
43 | - fully WSGI compatible | |
44 | ||
45 | ||
46 | Development Version | |
47 | ------------------- | |
48 | ||
49 | The `Werkzeug tip <http://dev.pocoo.org/hg/werkzeug-main/archive/tip.zip#egg=Werkzeug-dev` | |
50 | is installable via `easy_install` with ``easy_install Werkzeug==dev``. | |
51 | """ | |
2 | 52 | import os |
3 | 53 | import ez_setup |
4 | from inspect import getdoc | |
5 | 54 | ez_setup.use_setuptools() |
6 | 55 | |
7 | 56 | from setuptools import setup, Feature |
8 | 57 | |
9 | 58 | setup( |
10 | 59 | name='Werkzeug', |
11 | version='0.1', | |
60 | version='0.2', | |
12 | 61 | url='http://werkzeug.pocoo.org/', |
13 | download_url='http://trac.pocoo.org/repos/werkzeug/trunk', | |
62 | download_url='http://werkzeug.pocoo.org/download', | |
14 | 63 | license='BSD', |
15 | 64 | author='Armin Ronacher', |
16 | 65 | author_email='armin.ronacher@active-4.com', |
17 | 66 | description='The Swiss Army knife of Python web development', |
18 | long_description=getdoc(werkzeug), | |
67 | long_description=__doc__, | |
19 | 68 | zip_safe=False, |
20 | 69 | classifiers=[ |
21 | 'Development Status :: 2 - Pre-Alpha', | |
70 | 'Development Status :: 3 - Alpha', | |
22 | 71 | 'Environment :: Web Environment', |
23 | 72 | 'Intended Audience :: Developers', |
24 | 73 | 'License :: OSI Approved :: BSD License', |
0 | from werkzeug.contrib.testtools import * | |
1 | from werkzeug import Client, BaseRequest, responder | |
2 | ||
3 | from py.test import raises | |
4 | ||
5 | def response(content, mimetype): | |
6 | return TestResponse( | |
7 | status=200, | |
8 | response=content, | |
9 | mimetype=mimetype, | |
10 | ) | |
11 | ||
12 | @responder | |
13 | def application(environ, start_response): | |
14 | request = BaseRequest(environ) | |
15 | return response('This is a Test.', 'text/plain') | |
16 | ||
17 | def test_json(): | |
18 | resp = response('{ "a": 1}', 'application/json') | |
19 | assert resp.json == {'a': 1} | |
20 | ||
21 | def test_json_fail(): | |
22 | resp = response('{ "a": 1}', 'text/plain') | |
23 | raises(AttributeError, 'resp.json') | |
24 | ||
25 | def test_lxml_html(): | |
26 | resp = response( | |
27 | '<html><head><title>Test</title></head></html>', | |
28 | 'text/html') | |
29 | assert resp.lxml.xpath('//text()') == ['Test'] | |
30 | ||
31 | def test_lxml_xml(): | |
32 | resp = response( | |
33 | '<html><head><title>Test</title></head></html>', | |
34 | 'application/xml') | |
35 | assert resp.lxml.xpath('//text()') == ['Test'] | |
36 | ||
37 | def test_lxml_fail(): | |
38 | resp = response( | |
39 | '<html><head><title>Test</title></head></html>', | |
40 | 'text/plain') | |
41 | raises(AttributeError, 'resp.lxml') |
Binary diff not shown
8 | 8 | """ |
9 | 9 | from py.test import raises |
10 | 10 | from werkzeug.routing import Map, Rule, NotFound, BuildError, RequestRedirect |
11 | from werkzeug.utils import create_environ | |
11 | 12 | |
12 | 13 | |
13 | 14 | def test_basic_routing(): |
22 | 23 | assert adapter.match('/bar/') == ('bar', {}) |
23 | 24 | raises(RequestRedirect, lambda: adapter.match('/bar')) |
24 | 25 | raises(NotFound, lambda: adapter.match('/blub')) |
26 | ||
27 | ||
28 | test_environ_defaults = ''' | |
29 | >>> from werkzeug.routing import Map, Rule | |
30 | >>> from werkzeug import create_environ | |
31 | >>> environ = create_environ("/foo") | |
32 | >>> environ["PATH_INFO"] | |
33 | '/foo' | |
34 | >>> m = Map([Rule("/foo", endpoint="foo"), Rule("/bar", endpoint="bar")]) | |
35 | >>> a = m.bind_to_environ(environ) | |
36 | >>> a.match("/foo") | |
37 | ('foo', {}) | |
38 | >>> a.match() | |
39 | ('foo', {}) | |
40 | >>> a.match("/bar") | |
41 | ('bar', {}) | |
42 | >>> a.match("/bars") | |
43 | Traceback (most recent call last): | |
44 | ... | |
45 | NotFound: 404 Not Found | |
46 | ''' | |
25 | 47 | |
26 | 48 | |
27 | 49 | def test_basic_building(): |
76 | 98 | assert adapter.build('foo', {}) == '/foo' |
77 | 99 | assert adapter.build('bar', {'bar': 'blub'}) == '/blub' |
78 | 100 | assert adapter.build('bar', {'bar': 'blub', 'blub': 'bar'}) == '/blub/bar' |
101 | ||
102 | ||
103 | def test_path(): | |
104 | map = Map([ | |
105 | Rule('/', defaults={'name': 'FrontPage'}, endpoint='page'), | |
106 | Rule('/Special', endpoint='special'), | |
107 | Rule('/<int:year>', endpoint='year'), | |
108 | Rule('/<path:name>', endpoint='page'), | |
109 | Rule('/<path:name>/edit', endpoint='editpage'), | |
110 | Rule('/<path:name>/silly/<path:name2>', endpoint='sillypage'), | |
111 | Rule('/<path:name>/silly/<path:name2>/edit', endpoint='editsillypage'), | |
112 | Rule('/Talk:<path:name>', endpoint='talk'), | |
113 | Rule('/User:<username>', endpoint='user'), | |
114 | Rule('/User:<username>/<path:name>', endpoint='userpage'), | |
115 | Rule('/Files/<path:file>', endpoint='files'), | |
116 | ]) | |
117 | adapter = map.bind('example.org', '/') | |
118 | ||
119 | assert adapter.match('/') == ('page', {'name':'FrontPage'}) | |
120 | raises(RequestRedirect, lambda: adapter.match('/FrontPage')) | |
121 | assert adapter.match('/Special') == ('special', {}) | |
122 | assert adapter.match('/2007') == ('year', {'year':2007}) | |
123 | assert adapter.match('/Some/Page') == ('page', {'name':'Some/Page'}) | |
124 | assert adapter.match('/Some/Page/edit') == ('editpage', {'name':'Some/Page'}) | |
125 | assert adapter.match('/Foo/silly/bar') == ('sillypage', {'name':'Foo', 'name2':'bar'}) | |
126 | assert adapter.match('/Foo/silly/bar/edit') == ('editsillypage', {'name':'Foo', 'name2':'bar'}) | |
127 | assert adapter.match('/Talk:Foo/Bar') == ('talk', {'name':'Foo/Bar'}) | |
128 | assert adapter.match('/User:thomas') == ('user', {'username':'thomas'}) | |
129 | assert adapter.match('/User:thomas/projects/werkzeug') == ('userpage', {'username':'thomas', 'name':'projects/werkzeug'}) | |
130 | assert adapter.match('/Files/downloads/werkzeug/0.2.zip') == ('files', {'file':'downloads/werkzeug/0.2.zip'}) |
Binary diff not shown
Binary diff not shown
6 | 6 | :license: BSD license. |
7 | 7 | """ |
8 | 8 | import sys |
9 | from datetime import datetime | |
9 | 10 | from os import path |
10 | 11 | from py.test import raises |
11 | 12 | from werkzeug.utils import * |
13 | from werkzeug.wrappers import BaseResponse | |
14 | from werkzeug.http import parse_date | |
12 | 15 | from werkzeug.test import Client |
16 | ||
17 | ||
18 | def test_import_patch(): | |
19 | import werkzeug | |
20 | from werkzeug import __all__ as public_methods | |
21 | for name in public_methods: | |
22 | getattr(werkzeug, name) | |
13 | 23 | |
14 | 24 | |
15 | 25 | def test_multidict(): |
105 | 115 | popped = md.popitemlist() |
106 | 116 | assert popped in [('b', [2]), ('c', [3])] |
107 | 117 | |
118 | # type conversion | |
119 | md = MultiDict({'a': '4', 'b': ['2', '3']}) | |
120 | assert md.get('a', type=int) == 4 | |
121 | assert md.getlist('b', type=int) == [2, 3] | |
122 | ||
108 | 123 | |
109 | 124 | def test_combined_multidict(): |
110 | d1 = MultiDict([('foo', 1)]) | |
111 | d2 = MultiDict([('bar', 2)]) | |
125 | d1 = MultiDict([('foo', '1')]) | |
126 | d2 = MultiDict([('bar', '2'), ('bar', '3')]) | |
112 | 127 | d = CombinedMultiDict([d1, d2]) |
113 | 128 | |
114 | 129 | # lookup |
115 | assert d['foo'] == 1 | |
116 | assert d['bar'] == 2 | |
130 | assert d['foo'] == '1' | |
131 | assert d['bar'] == '2' | |
132 | assert d.getlist('bar') == ['2', '3'] | |
133 | ||
134 | # type lookup | |
135 | assert d.get('foo', type=int) == 1 | |
136 | assert d.getlist('bar', type=int) == [2, 3] | |
117 | 137 | |
118 | 138 | # get key errors for missing stuff |
119 | 139 | raises(KeyError, 'd["missing"]') |
150 | 170 | assert headers.get('x-Bar') == '1' |
151 | 171 | assert headers.get('Content-Type') == 'text/plain' |
152 | 172 | |
173 | # type conversion | |
174 | assert headers.get('x-bar', type=int) == 1 | |
175 | assert headers.getlist('x-bar', type=int) == [1, 2] | |
176 | ||
153 | 177 | # copying |
154 | 178 | a = Headers([('foo', 'bar')]) |
155 | 179 | b = a.copy() |
158 | 182 | assert b.getlist('foo') == ['bar'] |
159 | 183 | |
160 | 184 | |
161 | def test_lazy_property(): | |
185 | def test_cached_property(): | |
162 | 186 | foo = [] |
163 | 187 | class A(object): |
164 | 188 | def prop(self): |
165 | 189 | foo.append(42) |
166 | 190 | return 42 |
167 | prop = lazy_property(prop) | |
191 | prop = cached_property(prop) | |
168 | 192 | |
169 | 193 | a = A() |
170 | 194 | p = a.prop |
174 | 198 | |
175 | 199 | foo = [] |
176 | 200 | class A(object): |
177 | def prop(self): | |
201 | def _prop(self): | |
178 | 202 | foo.append(42) |
179 | 203 | return 42 |
180 | prop = lazy_property(prop, name='propval') | |
204 | prop = cached_property(_prop, name='prop') | |
205 | del _prop | |
181 | 206 | |
182 | 207 | a = A() |
183 | 208 | p = a.prop |
184 | 209 | q = a.prop |
185 | r = a.propval | |
186 | assert p == q == r == 42 | |
187 | assert foo == [42, 42] | |
210 | assert p == q == 42 | |
211 | assert foo == [42] | |
188 | 212 | |
189 | 213 | |
190 | 214 | def test_environ_property(): |
194 | 218 | string = environ_property('string') |
195 | 219 | missing = environ_property('missing', 'spam') |
196 | 220 | read_only = environ_property('number', read_only=True) |
197 | number = environ_property('number', convert=int) | |
198 | broken_number = environ_property('broken_number', convert=int) | |
221 | number = environ_property('number', load_func=int) | |
222 | broken_number = environ_property('broken_number', load_func=int) | |
223 | date = environ_property('date', None, parse_date, http_date) | |
199 | 224 | |
200 | 225 | a = A() |
201 | 226 | assert a.string == 'abc' |
203 | 228 | raises(AttributeError, 'a.read_only = "something"') |
204 | 229 | assert a.number == 42 |
205 | 230 | assert a.broken_number == None |
231 | assert a.date is None | |
232 | a.date = datetime(2008, 1, 22, 10, 0, 0, 0) | |
233 | assert a.environ['date'] == 'Tue, 22 Jan 2008 10:00:00 GMT' | |
206 | 234 | |
207 | 235 | |
208 | 236 | def test_quoting(): |
211 | 239 | assert url_quote_plus('foo bar') == 'foo+bar' |
212 | 240 | assert url_unquote_plus('foo+bar') == 'foo bar' |
213 | 241 | assert url_encode({'a': None, 'b': 'foo bar'}) == 'b=foo+bar' |
242 | assert url_fix(u'http://de.wikipedia.org/wiki/Elf (Begriffsklärung)') == \ | |
243 | 'http://de.wikipedia.org/wiki/Elf%20%28Begriffskl%C3%A4rung%29' | |
244 | ||
245 | ||
246 | test_href_tool = '>>> from werkzeug import Href\n\n' + Href.__doc__ | |
214 | 247 | |
215 | 248 | |
216 | 249 | def test_escape(): |
250 | assert escape(None) == '' | |
251 | assert escape(42) == '42' | |
217 | 252 | assert escape('<>') == '<>' |
218 | 253 | assert escape('"foo"') == '"foo"' |
219 | 254 | assert escape('"foo"', True) == '"foo"' |
255 | ||
256 | ||
257 | def test_unescape(): | |
258 | assert unescape('<ä>') == u'<ä>' | |
220 | 259 | |
221 | 260 | |
222 | 261 | def test_create_environ(): |
263 | 302 | assert ''.join(app_iter).strip() == 'NOT FOUND' |
264 | 303 | |
265 | 304 | |
305 | def test_run_wsgi_app(): | |
306 | def foo(environ, start_response): | |
307 | start_response('200 OK', [('Content-Type', 'text/plain')]) | |
308 | yield '1' | |
309 | yield '2' | |
310 | yield '3' | |
311 | ||
312 | app_iter, status, headers = run_wsgi_app(foo, {}) | |
313 | assert status == '200 OK' | |
314 | assert headers == [('Content-Type', 'text/plain')] | |
315 | assert app_iter.next() == '1' | |
316 | assert app_iter.next() == '2' | |
317 | assert app_iter.next() == '3' | |
318 | raises(StopIteration, app_iter.next) | |
319 | ||
320 | got_close = [] | |
321 | class CloseIter(object): | |
322 | def __init__(self): | |
323 | self.iterated = False | |
324 | def __iter__(self): | |
325 | return self | |
326 | def close(self): | |
327 | got_close.append(None) | |
328 | def next(self): | |
329 | if self.iterated: | |
330 | raise StopIteration() | |
331 | self.iterated = True | |
332 | return 'bar' | |
333 | ||
334 | def bar(environ, start_response): | |
335 | start_response('200 OK', [('Content-Type', 'text/plain')]) | |
336 | return CloseIter() | |
337 | ||
338 | app_iter, status, headers = run_wsgi_app(bar, {}) | |
339 | assert status == '200 OK' | |
340 | assert headers == [('Content-Type', 'text/plain')] | |
341 | assert app_iter.next() == 'bar' | |
342 | raises(StopIteration, app_iter.next) | |
343 | app_iter.close() | |
344 | ||
345 | assert run_wsgi_app(bar, {}, True)[0] == ['bar'] | |
346 | ||
347 | assert len(got_close) == 2 | |
348 | ||
349 | ||
266 | 350 | def test_date_funcs(): |
267 | 351 | assert http_date(0) == 'Thu, 01 Jan 1970 00:00:00 GMT' |
268 | 352 | assert cookie_date(0) == 'Thu, 01-Jan-1970 00:00:00 GMT' |
288 | 372 | >>> x(env, strip_querystring=True) |
289 | 373 | 'http://example.org/blub/foo' |
290 | 374 | ''' |
375 | ||
376 | ||
377 | def test_dates(): | |
378 | assert cookie_date(0) == 'Thu, 01-Jan-1970 00:00:00 GMT' | |
379 | assert cookie_date(datetime(1970, 1, 1)) == 'Thu, 01-Jan-1970 00:00:00 GMT' | |
380 | assert http_date(0) == 'Thu, 01 Jan 1970 00:00:00 GMT' | |
381 | assert http_date(datetime(1970, 1, 1)) == 'Thu, 01 Jan 1970 00:00:00 GMT' | |
382 | ||
383 | ||
384 | def test_cookies(): | |
385 | assert parse_cookie('dismiss-top=6; CP=null*; PHPSESSID=0a539d42abc001cd' | |
386 | 'c762809248d4beed; a=42') == { | |
387 | 'CP': u'null*', | |
388 | 'PHPSESSID': u'0a539d42abc001cdc762809248d4beed', | |
389 | 'a': u'42', | |
390 | 'dismiss-top': u'6' | |
391 | } | |
392 | assert set(dump_cookie('foo', 'bar baz blub', 360, httponly=True, | |
393 | sync_expires=False).split('; ')) == \ | |
394 | set(['HttpOnly', 'Max-Age=360', 'Path=/', 'foo=bar baz blub']) | |
395 | assert parse_cookie('fo234{=bar blub=Blah') == {'blub': 'Blah'} | |
396 | ||
397 | ||
398 | def test_responder(): | |
399 | def foo(environ, start_response): | |
400 | return BaseResponse('Test') | |
401 | client = Client(responder(foo), BaseResponse) | |
402 | response = client.get('/') | |
403 | assert response.status_code == 200 | |
404 | assert response.data == 'Test' | |
405 | ||
406 | ||
407 | def test_import_string(): | |
408 | import cgi | |
409 | assert import_string('cgi.escape') is cgi.escape | |
410 | assert import_string('cgi:escape') is cgi.escape | |
411 | assert import_string('XXXXXXXXXXXX', True) is None | |
412 | assert import_string('cgi.XXXXXXXXXXXX', True) is None | |
413 | raises(ImportError, "import_string('XXXXXXXXXXXXXXXX')") | |
414 | raises(AttributeError, "import_string('cgi.XXXXXXXXXX')") | |
415 | ||
416 | ||
417 | def test_find_modules(): | |
418 | assert list(find_modules('werkzeug.debug')) == ['werkzeug.debug.render', | |
419 | 'werkzeug.debug.util'] | |
420 | ||
421 | ||
422 | def test_html_builder(): | |
423 | assert html.p('Hello World') == '<p>Hello World</p>' | |
424 | assert html.a('Test', href='#') == '<a href="#">Test</a>' | |
425 | assert html.br() == '<br>' | |
426 | assert xhtml.br() == '<br />' | |
427 | assert html.img(src='foo') == '<img src="foo">' | |
428 | assert xhtml.img(src='foo') == '<img src="foo" />' | |
429 | assert html.html( | |
430 | html.head( | |
431 | html.title('foo'), | |
432 | html.script(type='text/javascript') | |
433 | ) | |
434 | ) == '<html><head><title>foo</title><script type="text/javascript">' \ | |
435 | '</script></head></html>' |
Binary diff not shown
7 | 7 | :license: BSD license. |
8 | 8 | """ |
9 | 9 | import pickle |
10 | from werkzeug.wrappers import BaseResponse, BaseRequest | |
10 | from py.test import raises | |
11 | from datetime import datetime, timedelta | |
12 | from werkzeug.wrappers import * | |
11 | 13 | from werkzeug.utils import MultiDict |
12 | 14 | from werkzeug.test import Client |
13 | 15 | |
21 | 23 | |
22 | 24 | def __init__(self, response, status, headers): |
23 | 25 | BaseResponse.__init__(self, response, status, headers) |
24 | self.body_data = pickle.loads(self.response_body) | |
26 | self.body_data = pickle.loads(self.data) | |
25 | 27 | |
26 | 28 | def __getitem__(self, key): |
27 | 29 | return self.body_data[key] |
62 | 64 | assert environ['wsgi.url_scheme'] == 'http' |
63 | 65 | |
64 | 66 | |
65 | def test_request(): | |
67 | def test_base_request(): | |
66 | 68 | client = Client(request_test_app, RequestTestResponse) |
67 | 69 | |
68 | 70 | # get requests |
94 | 96 | assert response['form'] == MultiDict() |
95 | 97 | |
96 | 98 | |
97 | def test_response(): | |
99 | def test_base_response(): | |
98 | 100 | # unicode |
99 | 101 | response = BaseResponse(u'öäü') |
100 | assert response.response_body == 'öäü' | |
102 | assert response.data == 'öäü' | |
101 | 103 | |
102 | 104 | # writing |
103 | 105 | response = BaseResponse('foo') |
104 | 106 | response.write('bar') |
105 | assert response.response_body == 'foobar' | |
107 | assert response.data == 'foobar' | |
106 | 108 | |
107 | 109 | # set cookie |
108 | 110 | response = BaseResponse() |
112 | 114 | ('Set-Cookie', 'foo=bar; Domain=example.org; expires=Thu, ' |
113 | 115 | '01-Jan-1970 00:00:00 GMT; Max-Age=60; Path=/blub') |
114 | 116 | ] |
117 | ||
118 | ||
119 | def test_type_forcing(): | |
120 | def wsgi_application(environ, start_response): | |
121 | start_response('200 OK', [('Content-Type', 'text/html')]) | |
122 | return ['Hello World!'] | |
123 | base_response = BaseResponse('Hello World!', content_type='text/html') | |
124 | ||
125 | class SpecialResponse(Response): | |
126 | def foo(self): | |
127 | return 42 | |
128 | ||
129 | # good enough for this simple application, but don't ever use that in | |
130 | # real world examples! | |
131 | fake_env = {} | |
132 | ||
133 | for orig_resp in wsgi_application, base_response: | |
134 | response = SpecialResponse.force_type(orig_resp, fake_env) | |
135 | assert response.__class__ is SpecialResponse | |
136 | assert response.foo() == 42 | |
137 | assert response.data == 'Hello World!' | |
138 | assert response.content_type == 'text/html' | |
139 | ||
140 | # without env, no arbitrary conversion | |
141 | raises(TypeError, "SpecialResponse.force_type(wsgi_application)") | |
142 | ||
143 | ||
144 | def test_accept_mixin(): | |
145 | request = Request({ | |
146 | 'HTTP_ACCEPT': 'text/xml,application/xml,application/xhtml+xml,' | |
147 | 'text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5', | |
148 | 'HTTP_ACCEPT_CHARSET': 'ISO-8859-1,utf-8;q=0.7,*;q=0.7', | |
149 | 'HTTP_ACCEPT_ENCODING': 'gzip,deflate', | |
150 | 'HTTP_ACCEPT_LANGUAGE': 'en-us,en;q=0.5' | |
151 | }) | |
152 | assert request.accept_mimetypes == Accept([ | |
153 | ('text/xml', 1), ('image/png', 1), ('application/xml', 1), | |
154 | ('application/xhtml+xml', 1), ('text/html', 0.9), | |
155 | ('text/plain', 0.8), ('*/*', 0.5) | |
156 | ]) | |
157 | assert request.accept_charsets == Accept([ | |
158 | ('ISO-8859-1', 1), ('utf-8', 0.7), ('*', 0.7) | |
159 | ]) | |
160 | assert request.accept_encodings == Accept([('gzip', 1), ('deflate', 1)]) | |
161 | assert request.accept_languages == Accept([('en-us', 1), ('en', 0.5)]) | |
162 | ||
163 | ||
164 | def test_etag_request_mixin(): | |
165 | request = Request({ | |
166 | 'HTTP_CACHE_CONTROL': 'private, no-cache', | |
167 | 'HTTP_IF_MATCH': 'w/"foo", bar, "baz"', | |
168 | 'HTTP_IF_NONE_MATCH': 'w/"foo", bar, "baz"', | |
169 | 'HTTP_IF_MODIFIED_SINCE': 'Tue, 22 Jan 2008 11:18:44 GMT', | |
170 | 'HTTP_IF_UNMODIFIED_SINCE': 'Tue, 22 Jan 2008 11:18:44 GMT' | |
171 | }) | |
172 | assert request.cache_control.private | |
173 | assert request.cache_control.no_cache | |
174 | ||
175 | for etags in request.if_match, request.if_none_match: | |
176 | assert etags('bar') | |
177 | assert etags.contains_raw('w/"foo"') | |
178 | assert etags.contains_weak('foo') | |
179 | assert not etags.contains('foo') | |
180 | ||
181 | assert request.if_modified_since == datetime(2008, 1, 22, 11, 18, 44) | |
182 | assert request.if_unmodified_since == datetime(2008, 1, 22, 11, 18, 44) | |
183 | ||
184 | ||
185 | def test_user_agent_mixin(): | |
186 | user_agents = [ | |
187 | ('Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.1.11) ' | |
188 | 'Gecko/20071127 Firefox/2.0.0.11', 'firefox', 'macos', '2.0.0.11', | |
189 | 'en-US'), | |
190 | ('Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; de-DE) Opera 8.54', | |
191 | 'opera', 'windows', '8.54', 'de-DE'), | |
192 | ('Mozilla/5.0 (iPhone; U; CPU like Mac OS X; en) AppleWebKit/420 ' | |
193 | '(KHTML, like Gecko) Version/3.0 Mobile/1A543a Safari/419.3', | |
194 | 'safari', 'iphone', '419.3', 'en'), | |
195 | ('Bot Googlebot/2.1 ( http://www.googlebot.com/bot.html)', | |
196 | 'google', None, '2.1', None) | |
197 | ] | |
198 | for ua, browser, platform, version, lang in user_agents: | |
199 | request = Request({'HTTP_USER_AGENT': ua}) | |
200 | assert request.user_agent.browser == browser | |
201 | assert request.user_agent.platform == platform | |
202 | assert request.user_agent.version == version | |
203 | assert request.user_agent.language == lang | |
204 | ||
205 | ||
206 | def test_etag_response_mixin(): | |
207 | response = Response('Hello World') | |
208 | assert response.get_etag() == (None, None) | |
209 | response.add_etag() | |
210 | assert response.get_etag() == ('b10a8db164e0754105b7a99be72e3fe5', False) | |
211 | assert not response.cache_control | |
212 | response.cache_control.must_revalidate = True | |
213 | response.cache_control.max_age = 60 | |
214 | assert response.headers['Cache-Control'] == 'must-revalidate, max-age=60' | |
215 | ||
216 | response.make_conditional({ | |
217 | 'REQUEST_METHOD': 'GET', | |
218 | 'HTTP_IF_NONE_MATCH': response.get_etag()[0] | |
219 | }) | |
220 | assert response.status_code == 304 | |
221 | ||
222 | ||
223 | def test_response_stream_mixin(): | |
224 | response = Response() | |
225 | response.stream.write('Hello ') | |
226 | response.stream.write('World!') | |
227 | assert response.response == ['Hello ', 'World!'] | |
228 | assert response.data == 'Hello World!' | |
229 | ||
230 | ||
231 | def test_common_response_descriptors_mixin(): | |
232 | response = Response() | |
233 | response.mimetype = 'text/html' | |
234 | assert response.mimetype == 'text/html' | |
235 | assert response.content_type == 'text/html; charset=utf-8' | |
236 | ||
237 | now = datetime.utcnow().replace(microsecond=0) | |
238 | ||
239 | assert response.content_length is None | |
240 | response.content_length = '42' | |
241 | assert response.content_length == 42 | |
242 | ||
243 | for attr in 'date', 'age', 'expires': | |
244 | assert getattr(response, attr) is None | |
245 | setattr(response, attr, now) | |
246 | assert getattr(response, attr) == now | |
247 | ||
248 | assert response.retry_after is None | |
249 | response.retry_after = now | |
250 | assert response.retry_after == now | |
251 | ||
252 | assert not response.vary | |
253 | response.vary.add('Cookie') | |
254 | response.vary.add('Content-Language') | |
255 | assert 'cookie' in response.vary | |
256 | assert response.vary.to_header() == 'Cookie, Content-Language' | |
257 | response.headers['Vary'] = 'Content-Encoding' | |
258 | assert response.vary.as_set() == set(['content-encoding']) | |
259 | ||
260 | response.allow.update(['GET', 'POST']) | |
261 | assert response.headers['Allow'] == 'GET, POST' | |
262 | ||
263 | response.content_language.add('en-US') | |
264 | response.content_language.add('fr') | |
265 | assert response.headers['Content-Language'] == 'en-US, fr' |
Binary diff not shown
2 | 2 | werkzeug |
3 | 3 | ~~~~~~~~ |
4 | 4 | |
5 | Werkzeug is the Swiss Army(r) knife of Python web development. | |
5 | Werkzeug is the Swiss Army knife of Python web development. | |
6 | 6 | |
7 | It provides useful classes and functions for any WSGI application take | |
8 | make life much easier. All of the provided classes are independed from | |
9 | each other so you can mix it with any other library. | |
7 | It provides useful classes and functions for any WSGI application to make | |
8 | the life of a python web developer much easier. All of the provided | |
9 | classes are independed from each other so you can mix it with any other | |
10 | library. | |
10 | 11 | |
11 | 12 | |
12 | Builtin blades^Wfeatures | |
13 | ~~~~~~~~~~~~~~~~~~~~~~~~ | |
14 | ||
15 | **Request / Response objects** | |
16 | ||
17 | These objects wrap the WSGI `environ` and `start_response` objects. | |
18 | They handle unicode conversion, form data parsing, cookie management | |
19 | and much more in a django like manner. | |
20 | ||
21 | Just subclass them and hook your own features in. | |
22 | ||
23 | **Reporter stream** | |
24 | ||
25 | A class that can wrap a `wsgi.input` stream so that it reports it's | |
26 | progress into the active session, a file on the filesystem etc. This | |
27 | is very useful if you want to give your users a visual feedback for | |
28 | file uploads using AJAX. | |
29 | ||
30 | **Application debugger middleware** | |
31 | ||
32 | If you want to debug your WSGI application you can hook in the | |
33 | `DebuggedApplication` middleware that allows you to inspect the frames | |
34 | of tracebacks either by looking at the current locals and sourcecode | |
35 | or starting an interactive shell in one of the frames. | |
36 | ||
37 | **Shared data middleware** | |
38 | ||
39 | In production environments static data is usually served by a | |
40 | lightweight webserver like lighttpd or nginx. But during development | |
41 | it makes no sense to install another service on the computer so the | |
42 | `SharedDataMiddleware` can serve static files in your WSGI | |
43 | application. | |
44 | ||
45 | **Unicode aware data processing** | |
46 | ||
47 | The utils package contains functions that work like their counterparts | |
48 | in the builtin `urllib` or `cgi` module but are unicode aware. Per | |
49 | default they expect utf-8 strings like the request/response objects | |
50 | but you can pass an encoding to the too. | |
51 | ||
52 | **Mini template engine** | |
53 | ||
54 | For small projects you often face the problem that a real template | |
55 | engine means another requirement but the builtin string formattings | |
56 | (or string template) operations are not enough for the application. | |
57 | Werkzeug provides a minimal template engine that looks and behaves | |
58 | like the e-ruby template engine. | |
59 | ||
60 | **Context Locals** | |
61 | ||
62 | The `Local` object works pretty much like a normal thread local but | |
63 | it has support for py.magic greenlets too. Additionally there is a | |
64 | `LocalManager` that allows you to clean up all the context locals you | |
65 | have instanciated. | |
66 | ||
67 | **Test utilities** | |
68 | ||
69 | Werkzeug provides a `Client` class that can be used to test | |
70 | applications. Just instanciate it with the app and fire virtual | |
71 | requests. | |
72 | ||
73 | ||
74 | :copyright: 2007 by Armin Ronacher. | |
13 | :copyright: 2007-2008 by Armin Ronacher. | |
75 | 14 | :license: BSD, see LICENSE for more details. |
76 | 15 | """ |
77 | 16 | from types import ModuleType |
79 | 18 | |
80 | 19 | |
81 | 20 | all_by_module = { |
82 | 'werkzeug': ['exceptions', 'routing', 'script'], | |
83 | 21 | 'werkzeug.debug': ['DebuggedApplication'], |
84 | 22 | 'werkzeug.local': ['Local', 'LocalManager', 'LocalProxy'], |
85 | 23 | 'werkzeug.templates': ['Template'], |
86 | 24 | 'werkzeug.serving': ['run_simple'], |
87 | 25 | 'werkzeug.test': ['Client'], |
88 | 26 | 'werkzeug.testapp': ['test_app'], |
27 | 'werkzeug.exceptions': ['abort', 'Aborter'], | |
89 | 28 | 'werkzeug.utils': ['escape', 'create_environ', 'url_quote', |
90 | 29 | 'environ_property', 'cookie_date', 'http_date', |
91 | 30 | 'url_encode', 'url_quote_plus', 'Headers', |
92 | 'EnvironHeaders', 'CombinedMultiDict', | |
93 | 'run_wsgi_app', 'get_host', | |
31 | 'EnvironHeaders', 'CombinedMultiDict', 'url_fix', | |
32 | 'run_wsgi_app', 'get_host', 'responder', | |
94 | 33 | 'SharedDataMiddleware', 'ClosingIterator', |
95 | 'FileStorage', 'url_unquote_plus', | |
34 | 'FileStorage', 'url_unquote_plus', 'url_decode', | |
96 | 35 | 'url_unquote', 'get_current_url', 'redirect', |
97 | 'lazy_property', 'MultiDict', 'url_decode'], | |
98 | 'werkzeug.http': ['Accept', 'CacheControl', 'parse_accept_header', | |
99 | 'parse_cache_control_header', | |
100 | 'HTTP_STATUS_CODES'], | |
101 | 'werkzeug.wrappers': ['BaseResponse', 'BaseRequest', | |
102 | 'BaseReporterStream'] | |
36 | 'append_slash_redirect', 'lazy_property', | |
37 | 'cached_property', 'MultiDict', 'import_string', | |
38 | 'dump_cookie', 'parse_cookie', 'unescape', | |
39 | 'format_string', 'Href', 'DispatcherMiddleware', | |
40 | 'find_modules', 'header_property', 'html', | |
41 | 'xhtml'], | |
42 | 'werkzeug.useragents': ['UserAgent'], | |
43 | 'werkzeug.http': ['Accept', 'CacheControl', 'ETags', 'parse_etags', | |
44 | 'parse_date', 'parse_cache_control_header', | |
45 | 'is_resource_modified', 'parse_accept_header', | |
46 | 'parse_set_header', 'quote_etag', 'unquote_etag', | |
47 | 'HeaderSet', 'HTTP_STATUS_CODES'], | |
48 | 'werkzeug.wrappers': ['BaseResponse', 'BaseRequest', 'Request', | |
49 | 'Response', 'AcceptMixin', 'ETagRequestMixin', | |
50 | 'ETagResponseMixin', 'ResponseStreamMixin', | |
51 | 'CommonResponseDescriptorsMixin', | |
52 | 'UserAgentMixin', 'BaseReporterStream'] | |
103 | 53 | } |
54 | ||
55 | attribute_modules = ['exceptions', 'routing', 'script'] | |
104 | 56 | |
105 | 57 | |
106 | 58 | object_origins = {} |
109 | 61 | object_origins[item] = module |
110 | 62 | |
111 | 63 | |
112 | class _AutoModule(ModuleType): | |
64 | class module(ModuleType): | |
113 | 65 | """Automatically import objects from the modules.""" |
114 | 66 | |
115 | 67 | def __getattr__(self, name): |
118 | 70 | for extra_name in all_by_module[module.__name__]: |
119 | 71 | setattr(self, extra_name, getattr(module, extra_name)) |
120 | 72 | return getattr(module, name) |
73 | elif name in attribute_modules: | |
74 | __import__('werkzeug.' + name) | |
121 | 75 | return ModuleType.__getattribute__(self, name) |
122 | 76 | |
123 | 77 | |
78 | # keep a reference to this module so that it's not garbage collected | |
124 | 79 | old_module = sys.modules['werkzeug'] |
125 | new_module = sys.modules['werkzeug'] = _AutoModule('werkzeug') | |
126 | new_module.__dict__.update( | |
127 | __file__=__file__, | |
128 | __path__=__path__, | |
129 | __doc__=__doc__, | |
130 | __all__=tuple(object_origins) | |
131 | ) | |
80 | ||
81 | # setup the new module and patch it into the dict of loaded modules | |
82 | new_module = sys.modules['werkzeug'] = module('werkzeug') | |
83 | new_module.__dict__.update({ | |
84 | '__file__': __file__, | |
85 | '__path__': __path__, | |
86 | '__doc__': __doc__, | |
87 | '__all__': tuple(object_origins) | |
88 | }) |
12 | 12 | tell the Python interpreter that `contrib` is a package. |
13 | 13 | |
14 | 14 | |
15 | :copyright: 2007 by Marek Kubica. | |
15 | :copyright: 2007-2008 by Marek Kubica, Marian Sigler, Armin Ronacher, | |
16 | Leif K-Brooks, Ronny Pfannschmid, Thomas Johansson. | |
16 | 17 | :license: BSD, see LICENSE for more details. |
17 | 18 | """ |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | werkzeug.contrib.atom | |
3 | ~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | This module provides a class called `AtomFeed` which can be used | |
6 | to generate Atom feeds. | |
7 | ||
8 | :copyright: Copyright 2007 by Armin Ronacher, Marian Sigler. | |
9 | :license: GNU GPL. | |
10 | """ | |
11 | from datetime import datetime | |
12 | from werkzeug.utils import escape | |
13 | from werkzeug.wrappers import BaseResponse | |
14 | ||
15 | ||
16 | XHTML_NAMESPACE = 'http://www.w3.org/1999/xhtml' | |
17 | ||
18 | ||
19 | def _make_text_block(name, content, content_type=None): | |
20 | """Helper function for the builder that creates an XML text block.""" | |
21 | if content_type == 'xhtml': | |
22 | return u'<%s type="xhtml"><div xmlns="%s">%s</div></%s>\n' % \ | |
23 | (name, XHTML_NAMESPACE, content, name) | |
24 | if not content_type: | |
25 | return u'<%s>%s</%s>\n' % (name, escape(content), name) | |
26 | return u'<%s type="%s">%s</%s>\n' % (name, content_type, | |
27 | escape(content), name) | |
28 | ||
29 | ||
30 | def format_iso8601(obj): | |
31 | """Format a datetime object for iso8601""" | |
32 | return obj.strftime('%Y-%d-%mT%H:%M:%SZ') | |
33 | ||
34 | ||
35 | class AtomFeed(object): | |
36 | """ | |
37 | A helper class that creates ATOM feeds. | |
38 | """ | |
39 | default_generator = ('Werkzeug', None, None) | |
40 | ||
41 | def __init__(self, title=None, entries=None, **kwargs): | |
42 | """ | |
43 | Create an atom feed. | |
44 | ||
45 | :Parameters: | |
46 | title | |
47 | the title of the feed. Required. | |
48 | title_type | |
49 | the type attribute for the title element. One of html, text, | |
50 | xhtml. Default is text. | |
51 | url | |
52 | the url for the feed (not the url *of* the feed) | |
53 | id | |
54 | a globally unique id for the feed. Must be an URI. If not present | |
55 | the `feed_url` is used, but one of both is required. | |
56 | updated | |
57 | the time the feed was modified the last time. Must be a `datetime` | |
58 | object. If not present the latest entry's `updated` is used. | |
59 | feed_url | |
60 | the url to the feed. Should be the URL that was requested. | |
61 | author | |
62 | the author of the feed. Must be either a string (the name) or a | |
63 | dict with name (required) and uri or email (both optional). Can be | |
64 | a list of (may be mixed, too) strings and dicts, too, if there are | |
65 | multiple authors. Required if not every entry has an author | |
66 | element. | |
67 | icon | |
68 | an icon for the feed. | |
69 | logo | |
70 | a logo for the feed. | |
71 | rights | |
72 | copyright information for the feed. | |
73 | rights_type | |
74 | the type attribute for the rights element. One of html, text, | |
75 | xhtml. Default is text. | |
76 | subtitle | |
77 | a short description of the feed. | |
78 | subtitle_type | |
79 | the type attribute for the subtitle element. One of html, text, | |
80 | xhtml. Default is text. | |
81 | links | |
82 | additional links. Must be a list of dictionaries with href | |
83 | (required) and rel, type, hreflang, title, length (all optional) | |
84 | generator | |
85 | the software that generated this feed. This must be a tuple in | |
86 | the form ``(name, url, version)``. If you don't want to specify | |
87 | one of them, set the item to None. | |
88 | entries | |
89 | a list with the entries for the feed. Entries can also be added | |
90 | later with add(). | |
91 | ||
92 | For more information on the elements see | |
93 | http://www.atomenabled.org/developers/syndication/ | |
94 | ||
95 | Everywhere where a list is demanded, any iterable can be used. | |
96 | """ | |
97 | self.title = title | |
98 | self.title_type = kwargs.get('title_type') | |
99 | self.url = kwargs.get('url') | |
100 | self.feed_url = kwargs.get('feed_url', self.url) | |
101 | self.id = kwargs.get('id', self.feed_url) | |
102 | self.updated = kwargs.get('updated') | |
103 | self.author = kwargs.get('author', ()) | |
104 | self.icon = kwargs.get('icon') | |
105 | self.logo = kwargs.get('logo') | |
106 | self.rights = kwargs.get('rights') | |
107 | self.rights_type = kwargs.get('rights_type') | |
108 | self.subtitle = kwargs.get('subtitle') | |
109 | self.subtitle_type = kwargs.get('subtitle_type') | |
110 | self.generator = kwargs.get('generator') | |
111 | if self.generator is None: | |
112 | self.generator = self.default_generator | |
113 | self.links = kwargs.get('links', []) | |
114 | self.entries = entries and list(entries) or [] | |
115 | ||
116 | if not hasattr(self.author, '__iter__') \ | |
117 | or isinstance(self.author, (basestring, dict)): | |
118 | self.author = [self.author] | |
119 | for i, author in enumerate(self.author): | |
120 | if not isinstance(author, dict): | |
121 | self.author[i] = {'name': author} | |
122 | ||
123 | if not self.title: | |
124 | raise ValueError('title is required') | |
125 | if not self.id: | |
126 | raise ValueError('id is required') | |
127 | for author in self.author: | |
128 | if 'name' not in author: | |
129 | raise TypeError('author must contain at least a name') | |
130 | ||
131 | def add(self, *args, **kwargs): | |
132 | """add a new entry to the feed""" | |
133 | if len(args) == 1 and not kwargs and isinstance(args[0], FeedEntry): | |
134 | self.entries.append(args[0]) | |
135 | else: | |
136 | self.entries.append(FeedEntry(*args, **kwargs)) | |
137 | ||
138 | def __repr__(self): | |
139 | return '<%s %r (%d entries)>' % ( | |
140 | self.__class__.__name__, | |
141 | self.title, | |
142 | len(self.entries) | |
143 | ) | |
144 | ||
145 | def generate(self): | |
146 | """Return a generator that yields pieces of XML.""" | |
147 | # atom demands either an author element in every entry or a global one | |
148 | if not self.author: | |
149 | if False in map(lambda e: bool(e.author), self.entries): | |
150 | self.author = ({'name': u'unbekannter Autor'},) | |
151 | ||
152 | if not self.updated: | |
153 | dates = sorted(entry.updated for entry in self.entries) | |
154 | self.updated = dates and dates[-1] or datetime.utcnow() | |
155 | ||
156 | yield u'<?xml version="1.0" encoding="utf-8"?>\n' | |
157 | yield u'<feed xmlns="http://www.w3.org/2005/Atom">\n' | |
158 | yield ' ' + _make_text_block('title', self.title, self.title_type) | |
159 | yield u' <id>%s</id>\n' % escape(self.id) | |
160 | yield u' <updated>%s</updated>\n' % format_iso8601(self.updated) | |
161 | if self.url: | |
162 | yield u' <link href="%s" />\n' % escape(self.url, True) | |
163 | if self.feed_url: | |
164 | yield u' <link href="%s" rel="self" />\n' % \ | |
165 | escape(self.feed_url, True) | |
166 | for link in self.links: | |
167 | yield u' <link %s/>\n' % ''.join('%s="%s" ' % \ | |
168 | (k, escape(link[k], True)) for k in link) | |
169 | for author in self.author: | |
170 | yield u' <author>\n' | |
171 | yield u' <name>%s</name>\n' % escape(author['name']) | |
172 | if 'uri' in author: | |
173 | yield u' <uri>%s</uri>\n' % escape(author['uri']) | |
174 | if 'email' in author: | |
175 | yield ' <email>%s</email>\n' % escape(author['email']) | |
176 | yield ' </author>\n' | |
177 | if self.subtitle: | |
178 | yield ' ' + _make_text_block('subtitle', self.subtitle, | |
179 | self.subtitle_type) | |
180 | if self.icon: | |
181 | yield u' <icon>%s</icon>\n' % escape(self.icon) | |
182 | if self.logo: | |
183 | yield u' <logo>%s</logo>\n' % escape(self.logo) | |
184 | if self.rights: | |
185 | yield ' ' + _make_text_block('rights', self.rights, | |
186 | self.rights_type) | |
187 | generator_name, generator_url, generator_version = self.generator | |
188 | if generator_name or generator_url or generator_version: | |
189 | tmp = [u' <generator'] | |
190 | if generator_url: | |
191 | tmp.append(u' uri="%s"' % escape(generator_url, True)) | |
192 | if generator_version: | |
193 | tmp.append(u' version="%s"' % escape(generator_version, True)) | |
194 | tmp.append(u'>%s</generator>\n' % escape(generator_name)) | |
195 | yield u''.join(tmp) | |
196 | for entry in self.entries: | |
197 | for line in entry.generate(): | |
198 | yield u' ' + line | |
199 | yield u'</feed>\n' | |
200 | ||
201 | def to_string(self): | |
202 | """Convert the feed into a string.""" | |
203 | return u''.join(self.generate()) | |
204 | ||
205 | def get_response(self): | |
206 | """Return a response object for the feed.""" | |
207 | return BaseResponse(self.to_string(), mimetype='application/atom+xml') | |
208 | ||
209 | def __call__(self, environ, start_response): | |
210 | """Use the class as WSGI response object.""" | |
211 | return self.get_response(environ, start_response) | |
212 | ||
213 | def __unicode__(self): | |
214 | return self.to_string() | |
215 | ||
216 | def __str__(self): | |
217 | return self.to_string().encode('utf-8') | |
218 | ||
219 | ||
220 | class FeedEntry(object): | |
221 | """ | |
222 | Represents a single entry in a feed. | |
223 | """ | |
224 | ||
225 | def __init__(self, title=None, content=None, **kwargs): | |
226 | """ | |
227 | Holds an atom feed entry. | |
228 | ||
229 | :Parameters: | |
230 | title | |
231 | the title of the entry. Required. | |
232 | title_type | |
233 | the type attribute for the title element. One of html, text, | |
234 | xhtml. Default is text. | |
235 | content | |
236 | the content of the entry. | |
237 | content_type | |
238 | the type attribute for the content element. One of html, text, | |
239 | xhtml. Default is text. | |
240 | summary | |
241 | a summary of the entry's content. | |
242 | summary_type | |
243 | a type attribute for the summary element. One of html, text, | |
244 | xhtml. Default is text. | |
245 | url | |
246 | the url for the entry. | |
247 | id | |
248 | a globally unique id for the entry. Must be an URI. If not present | |
249 | the URL is used, but one of both is required. | |
250 | updated | |
251 | the time the entry was modified the last time. Must be a | |
252 | `datetime` object. Required. | |
253 | author | |
254 | the author of the entry. Must be either a string (the name) or a | |
255 | dict with name (required) and uri or email (both optional). Can | |
256 | be a list of (may be mixed, too) strings and dicts, too, if there | |
257 | are multiple authors. Required if there is no author for the | |
258 | feed. | |
259 | published | |
260 | the time the entry was initially published. Must be a `datetime` | |
261 | object. | |
262 | rights | |
263 | copyright information for the entry. | |
264 | rights_type | |
265 | the type attribute for the rights element. One of html, text, | |
266 | xhtml. Default is text. | |
267 | links | |
268 | additional links. Must be a list of dictionaries with href | |
269 | (required) and rel, type, hreflang, title, length (all optional) | |
270 | xml_base | |
271 | The xml base (url) for this feed item. If not provided it will | |
272 | default to the item url. | |
273 | ||
274 | For more information on the elements see | |
275 | http://www.atomenabled.org/developers/syndication/ | |
276 | ||
277 | Everywhere where a list is demanded, any iterable can be used. | |
278 | """ | |
279 | self.title = title | |
280 | self.title_type = kwargs.get('title_type', 'html') | |
281 | self.content = content | |
282 | self.content_type = kwargs.get('content_type', 'html') | |
283 | self.url = kwargs.get('url') | |
284 | self.id = kwargs.get('id', self.url) | |
285 | self.updated = kwargs.get('updated') | |
286 | self.summary = kwargs.get('summary') | |
287 | self.summary_type = kwargs.get('summary_type', 'html') | |
288 | self.author = kwargs.get('author') | |
289 | self.published = kwargs.get('published') | |
290 | self.rights = kwargs.get('rights') | |
291 | self.links = kwargs.get('links', []) | |
292 | self.xml_base = kwargs.get('xml_base', self.url) | |
293 | ||
294 | if not hasattr(self.author, '__iter__') \ | |
295 | or isinstance(self.author, (basestring, dict)): | |
296 | self.author = [self.author] | |
297 | for i, author in enumerate(self.author): | |
298 | if not isinstance(author, dict): | |
299 | self.author[i] = {'name': author} | |
300 | ||
301 | if not self.title: | |
302 | raise ValueError('title is required') | |
303 | if not self.id: | |
304 | raise ValueError('id is required') | |
305 | if not self.updated: | |
306 | raise ValueError('updated is required') | |
307 | ||
308 | def __repr__(self): | |
309 | return '<%s %r>' % ( | |
310 | self.__class__.__name__, | |
311 | self.title | |
312 | ) | |
313 | ||
314 | def generate(self): | |
315 | """Yields pieces of ATOM XML.""" | |
316 | base = '' | |
317 | if self.xml_base: | |
318 | base = ' xml:base="%s"' % escape(self.xml_base, True) | |
319 | yield u'<entry%s>\n' % base | |
320 | yield u' ' + _make_text_block('title', self.title, self.title_type) | |
321 | yield u' <id>%s</id>\n' % escape(self.id) | |
322 | yield u' <updated>%s</updated>\n' % format_iso8601(self.updated) | |
323 | if self.published: | |
324 | yield u' <published>%s</published>\n' % \ | |
325 | format_iso8601(self.published) | |
326 | if self.url: | |
327 | yield u' <link href="%s" />\n' % escape(self.url) | |
328 | for author in self.author: | |
329 | yield u' <author>\n' | |
330 | yield u' <name>%s</name>\n' % escape(author['name']) | |
331 | if 'uri' in author: | |
332 | yield u' <uri>%s</uri>\n' % escape(author['uri']) | |
333 | if 'email' in author: | |
334 | yield u' <email>%s</email>\n' % escape(author['email']) | |
335 | yield u' </author>\n' | |
336 | for link in self.links: | |
337 | yield u' <link %s/>\n' % ''.join('%s="%s" ' % \ | |
338 | (k, escape(link[k], True)) for k in link) | |
339 | if self.summary: | |
340 | yield u' ' + _make_text_block('summary', self.summary, | |
341 | self.summary_type) | |
342 | if self.content: | |
343 | yield u' ' + _make_text_block('content', self.content, | |
344 | self.content_type) | |
345 | yield u'</entry>\n' | |
346 | ||
347 | def to_string(self): | |
348 | """Convert the feed item into a unicode object.""" | |
349 | return u''.join(self.generate()) | |
350 | ||
351 | def __unicode__(self): | |
352 | return self.to_string() | |
353 | ||
354 | def __str__(self): | |
355 | return self.to_string().encode('utf-8') |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | werkzeug.contrib.cache | |
3 | ~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Small helper module that provides a simple interface to memcached, a | |
6 | simple django-inspired in-process cache and a file system based cache. | |
7 | ||
8 | The idea is that it's possible to switch caching systems without changing | |
9 | much code in the application. | |
10 | ||
11 | ||
12 | :copyright: 2007-2008 by Armin Ronacher. | |
13 | :license: BSD, see LICENSE for more details. | |
14 | """ | |
15 | try: | |
16 | from hashlib import md5 | |
17 | except ImportError: | |
18 | from md5 import new as md5 | |
19 | from itertools import izip | |
20 | from time import time | |
21 | from cPickle import loads, dumps, load, dump, HIGHEST_PROTOCOL | |
22 | ||
23 | have_memcache = True | |
24 | try: | |
25 | from cmemcache import memcache | |
26 | except ImportError: | |
27 | try: | |
28 | import memcache | |
29 | except ImportError: | |
30 | have_memcache = False | |
31 | ||
32 | ||
33 | class BaseCache(object): | |
34 | """Baseclass for the cache systems.""" | |
35 | ||
36 | def __init__(self, default_timeout=300): | |
37 | self.default_timeout = default_timeout | |
38 | ||
39 | def get(self, key): | |
40 | return None | |
41 | delete = get | |
42 | ||
43 | def get_many(self, *keys): | |
44 | return [self.get(key) for key in keys] | |
45 | ||
46 | def get_dict(self, *keys): | |
47 | return dict(izip(keys, self.get_many(keys))) | |
48 | ||
49 | def set(self, key, value, timeout=None): | |
50 | pass | |
51 | add = set | |
52 | ||
53 | def set_many(self, mapping, timeout=None): | |
54 | for key, value in mapping.iteritems(): | |
55 | self.set(key, value, timeout) | |
56 | ||
57 | def delete_many(self, *keys): | |
58 | for key in keys: | |
59 | self.delete(key) | |
60 | ||
61 | def clear(self): | |
62 | pass | |
63 | ||
64 | ||
65 | class NullCache(BaseCache): | |
66 | """A cache that doesn't cache.""" | |
67 | ||
68 | ||
69 | class SimpleCache(BaseCache): | |
70 | """ | |
71 | Simple memory cache for single process environments. This class exists | |
72 | mainly for the development server and is not 100% thread safe. It tries | |
73 | to use as many atomic operations as possible and no locks for simplicity | |
74 | but it could happen under heavy load that keys are added multiple times. | |
75 | """ | |
76 | ||
77 | def __init__(self, threshold=500, default_timeout=300): | |
78 | BaseCache.__init__(self, default_timeout) | |
79 | self._cache = {} | |
80 | self.clear = self._cache.clear | |
81 | self._threshold = threshold | |
82 | ||
83 | def _prune(self): | |
84 | if len(self._cache) > self._threshold: | |
85 | now = time() | |
86 | for idx, (key, (expires, _)) in enumerate(self._cache.items()): | |
87 | if expires <= now or idx % 3 == 0: | |
88 | self._cache.pop(key, None) | |
89 | ||
90 | def get(self, key): | |
91 | now = time() | |
92 | expires, value = self._cache.get(key, (0, None)) | |
93 | if expires > time(): | |
94 | return loads(value) | |
95 | ||
96 | def set(self, key, value, timeout=None): | |
97 | if timeout is None: | |
98 | timeout = self.default_timeout | |
99 | self._prune() | |
100 | self._cache[key] = (time() + timeout, dumps(value, HIGHEST_PROTOCOL)) | |
101 | ||
102 | def add(self, key, value, timeout=None): | |
103 | if timeout is None: | |
104 | timeout = self.default_timeout | |
105 | if len(self._cache) > self._threshold: | |
106 | self._prune() | |
107 | item = (time() + timeout, dumps(value, HIGHEST_PROTOCOL)) | |
108 | self._cache.setdefault(key, item) | |
109 | ||
110 | def delete(self, key): | |
111 | self._cache.pop(key, None) | |
112 | ||
113 | ||
114 | class MemcachedCache(BaseCache): | |
115 | """A cache that uses memcached as backend.""" | |
116 | ||
117 | def __init__(self, servers, default_timeout=300): | |
118 | BaseClient.__init__(self, default_timeout) | |
119 | self._client = memcache.Client(servers) | |
120 | ||
121 | def get(self, key): | |
122 | return self._client.get(key) | |
123 | ||
124 | def get_many(self, *keys): | |
125 | return self._client.get_multi(*keys) | |
126 | ||
127 | def add(self, key, value, timeout=None): | |
128 | if timeout is None: | |
129 | timeout = self.default_timeout | |
130 | self._client.add(key, value, timeout) | |
131 | ||
132 | def set(self, key, value, timeout=None): | |
133 | if timeout is None: | |
134 | timeout = self.default_timeout | |
135 | self._client.set(key, value, timeout) | |
136 | ||
137 | def set_many(self, mapping, timeout=None): | |
138 | if timeout is None: | |
139 | timeout = self.default_timeout | |
140 | self._client.set_multi(mapping, timeout) | |
141 | ||
142 | def delete(self, key): | |
143 | self._client.delete(key) | |
144 | ||
145 | def delete_many(self, *keys): | |
146 | self._client.delete_multi(keys) | |
147 | ||
148 | def clear(self): | |
149 | self._client.flush_all() | |
150 | ||
151 | ||
152 | class FileSystemCache(BaseCache): | |
153 | """A cache that stores the items on the file system.""" | |
154 | ||
155 | def __init__(self, cache_dir, threshold=500, default_timeout=300): | |
156 | BaseCache.__init__(self, default_timeout) | |
157 | self._path = cache_dir | |
158 | self._threshold = threshold | |
159 | if not os.path.exists(self._path): | |
160 | os.makedirs(self._path) | |
161 | ||
162 | def _prune(self): | |
163 | entries = os.listdir(self._path) | |
164 | if len(entries) > self._threshold: | |
165 | now = time() | |
166 | for idx, key in enumerate(entries): | |
167 | try: | |
168 | f = file(self._get_filename(key)) | |
169 | if pickle.load(f) > now and idx % 3 != 0: | |
170 | f.close() | |
171 | continue | |
172 | except: | |
173 | f.close() | |
174 | self.delete(key) | |
175 | ||
176 | def _get_filename(self, key): | |
177 | hash = md5(key).hexdigest() | |
178 | return os.path.join(self._path, hash) | |
179 | ||
180 | def get(self, key): | |
181 | filename = self._get_filename(key) | |
182 | try: | |
183 | f = file(filename, 'rb') | |
184 | try: | |
185 | if load(f) >= time(): | |
186 | return load(f) | |
187 | finally: | |
188 | f.close() | |
189 | os.remove(filename) | |
190 | except: | |
191 | return None | |
192 | ||
193 | def add(self, key, value, timeout=None): | |
194 | filename = self._get_filename(key) | |
195 | if not os.path.exists(filename): | |
196 | self.set(key, value, timeout) | |
197 | ||
198 | def set(self, key, value, timeout=None): | |
199 | if timeout is None: | |
200 | timeout = self.default_timeout | |
201 | filename = self._get_filename(key) | |
202 | self._prune() | |
203 | try: | |
204 | f = file(filename, 'wb') | |
205 | try: | |
206 | dump(int(time() + timeout), f, 1) | |
207 | dump(value, f, HIGHEST_PROTOCOL) | |
208 | finally: | |
209 | f.close() | |
210 | except (IOError, OSError): | |
211 | pass | |
212 | ||
213 | def delete(self, key): | |
214 | try: | |
215 | os.remove(self._get_filename(key)) | |
216 | except (IOError, OSError): | |
217 | pass | |
218 | ||
219 | def clear(self): | |
220 | for key in os.listdir(self._path): | |
221 | self.delete(key) |
3 | 3 | ~~~~~~~~~~~~~~~~~~~~~~~ |
4 | 4 | |
5 | 5 | This module implements a `IterIO` that converts an iterator into a stream |
6 | object and the other way round. Converting streams into interators | |
6 | object and the other way round. Converting streams into iterators | |
7 | 7 | requires the `greenlet`_ module. |
8 | 8 | |
9 | 9 | |
24 | 24 | constructor on the other hand is not an stream object but an iterator:: |
25 | 25 | |
26 | 26 | def foo(stream): |
27 | stream.write("something") | |
27 | stream.write("some") | |
28 | stream.write("thing") | |
29 | stream.flush() | |
28 | 30 | stream.write("otherthing") |
29 | 31 | iterator = IterIO(foo) |
30 | 32 | print iterator.next() # prints something |
124 | 126 | |
125 | 127 | def __new__(cls, func): |
126 | 128 | if greenlet is None: |
127 | raise RuntimeError('IterI requires greenlets') | |
129 | raise RuntimeError('IterI requires greenlet support') | |
128 | 130 | stream = object.__new__(cls) |
129 | 131 | stream.__init__(greenlet.getcurrent()) |
130 | 132 | |
131 | g = greenlet(lambda: func(stream), stream._parent) | |
133 | def run(): | |
134 | func(stream) | |
135 | stream.flush() | |
136 | ||
137 | g = greenlet(run, stream._parent) | |
132 | 138 | while 1: |
133 | 139 | rv = g.switch() |
134 | 140 | if not rv: |
137 | 143 | |
138 | 144 | def __init__(self, parent): |
139 | 145 | self._parent = parent |
146 | self._buffer = [] | |
140 | 147 | self.closed = False |
141 | 148 | self.pos = 0 |
142 | 149 | |
143 | 150 | def close(self): |
144 | 151 | if not self.closed: |
145 | 152 | self.closed = True |
146 | self._parent.throw(ExecutionStop) | |
147 | 153 | |
148 | 154 | def write(self, s): |
149 | 155 | if self.closed: |
150 | 156 | raise ValueError('I/O operation on closed file') |
151 | 157 | self.pos += len(s) |
152 | self._parent.switch((s,)) | |
158 | self._buffer.append(s) | |
153 | 159 | |
154 | 160 | def writelines(slf, list): |
155 | 161 | self.write(''.join(list)) |
157 | 163 | def flush(self): |
158 | 164 | if self.closed: |
159 | 165 | raise ValueError('I/O operation on closed file') |
166 | data = ''.join(self._buffer) | |
167 | self._buffer = [] | |
168 | self._parent.switch((data,)) | |
160 | 169 | |
161 | 170 | |
162 | 171 | class IterO(IterIO): |
5 | 5 | This module provides some simple shortcuts to make using Werkzeug |
6 | 6 | simpler for small scripts. |
7 | 7 | |
8 | These improvements include predefied Request and Response objects as well | |
9 | as a pre-defined Application object which all can be customized in child | |
10 | classes of course. The Request and Reponse objects handle URL generation | |
11 | as well as sessions via the werkzeug.contrib.sessions and is purely | |
12 | optional. | |
13 | ||
14 | There is also some integration of template engines. The template loaders | |
15 | are of course not neccessary to use the template engines in Werkzeug, but | |
16 | they provide a common interface. Currently supported template engines | |
17 | include Werkzeug's minitmpl an Genshi. Support for other engines can be | |
18 | added in a trivial way. These loaders provide a template interface similar | |
19 | to the one that Django uses. | |
8 | 20 | |
9 | 21 | :copyright: 2007 by Marek Kubica, Armin Ronacher. |
10 | 22 | :license: BSD, see LICENSE for more details. |
12 | 24 | from os import path |
13 | 25 | from werkzeug.wrappers import BaseRequest, BaseResponse |
14 | 26 | from werkzeug.templates import Template |
15 | ||
16 | ||
17 | __all__ = ['Request', 'Response', 'TemplateNotFound', 'TemplateLoader'] | |
27 | from werkzeug.exceptions import HTTPException | |
28 | from werkzeug.routing import RequestRedirect | |
29 | from werkzeug.contrib.sessions import FilesystemSessionStore | |
30 | ||
31 | __all__ = ['Request', 'Response', 'TemplateNotFound', 'TemplateLoader', | |
32 | 'GenshiTemplateLoader', 'Application'] | |
18 | 33 | |
19 | 34 | |
20 | 35 | class Request(BaseRequest): |
21 | 36 | """ |
22 | 37 | A handy subclass of the base request that adds a URL builder. |
23 | """ | |
24 | ||
25 | def __init__(self, environ, url_map): | |
38 | It when supplied a session store, it is also able to handle sessions. | |
39 | """ | |
40 | ||
41 | def __init__(self, environ, url_map, | |
42 | session_store=None, cookie_name=None): | |
43 | # call the parent for initialization | |
26 | 44 | BaseRequest.__init__(self, environ) |
45 | # create an adapter | |
27 | 46 | self.url_adapter = url_map.bind_to_environ(environ) |
47 | # create all stuff for sessions | |
48 | self.session_store = session_store | |
49 | self.cookie_name = cookie_name | |
50 | ||
51 | if session_store is not None and cookie_name is not None: | |
52 | if cookie_name in self.cookies: | |
53 | # get the session out of the storage | |
54 | self.session = session_store.get(self.cookies[cookie_name]) | |
55 | else: | |
56 | # create a new session | |
57 | self.session = session_store.new() | |
28 | 58 | |
29 | 59 | def url_for(self, callback, **values): |
30 | 60 | return self.url_adapter.build(callback, values) |
32 | 62 | |
33 | 63 | class Response(BaseResponse): |
34 | 64 | """ |
35 | A subclass of base response which sets the default mimetype to text/html | |
65 | A subclass of base response which sets the default mimetype to text/html. | |
66 | It the `Request` that came in is using Werkzeug sessions, this class | |
67 | takes care of saving that session. | |
36 | 68 | """ |
37 | 69 | default_mimetype = 'text/html' |
70 | ||
71 | def __call__(self, environ, start_response): | |
72 | # get the request object | |
73 | request = environ['werkzeug.request'] | |
74 | ||
75 | if request.session_store is not None: | |
76 | # save the session if neccessary | |
77 | request.session_store.save_if_modified(request.session) | |
78 | ||
79 | # set the cookie for the browser if it is not there: | |
80 | if request.cookie_name not in request.cookies: | |
81 | self.set_cookie(request.cookie_name, request.session.sid) | |
82 | ||
83 | # go on with normal response business | |
84 | return BaseResponse.__call__(self, environ, start_response) | |
85 | ||
86 | ||
87 | class Processor(object): | |
88 | """ | |
89 | A request and response processor - it is what Django calls a middleware, | |
90 | but Werkzeug also includes straight-foward support for real WSGI | |
91 | middlewares, so another name was chosen. | |
92 | ||
93 | The code of this processor is derived from the example in the Werkzeug | |
94 | trac, called `Request and Response Processor | |
95 | <http://dev.pocoo.org/projects/werkzeug/wiki/RequestResponseProcessor>`_ | |
96 | """ | |
97 | ||
98 | def process_request(self, request): | |
99 | return request | |
100 | ||
101 | def process_response(self, request, response): | |
102 | return response | |
103 | ||
104 | def process_view(self, request, view_func, view_args, view_kwargs): | |
105 | """ | |
106 | process_view() is called just before the Application calls the | |
107 | function specified by view_func. | |
108 | ||
109 | If this returns None, the Application processes the next Processor, | |
110 | and if it returns something else (like a Response instance), that | |
111 | will be returned without any further processing. | |
112 | """ | |
113 | return None | |
114 | ||
115 | def process_exception(self, request, exception): | |
116 | return None | |
117 | ||
118 | ||
119 | class Application(object): | |
120 | """ | |
121 | A generic WSGI application which can be used to start with Werkzeug in an | |
122 | easy, straightforward way. | |
123 | """ | |
124 | ||
125 | def __init__(self, name, url_map, session=False, processors=None): | |
126 | # save the name and the URL-map, as it'll be needed later on | |
127 | self.name = name | |
128 | self.url_map = url_map | |
129 | # save the list of processors if supplied | |
130 | self.processors = processors or [] | |
131 | # create an instance of the storage | |
132 | if session: | |
133 | self.store = session | |
134 | else: | |
135 | self.store = None | |
136 | ||
137 | def __call__(self, environ, start_response): | |
138 | # create a request - with or without session support | |
139 | if self.store is not None: | |
140 | request = Request(environ, self.url_map, | |
141 | session_store=self.store, cookie_name='%s_sid' % self.name) | |
142 | else: | |
143 | request = Request(environ, self.url_map) | |
144 | ||
145 | # apply the request processors | |
146 | for processor in self.processors: | |
147 | request = processor.process_request(request) | |
148 | ||
149 | try: | |
150 | # find the callback to which the URL is mapped | |
151 | callback, args = request.url_adapter.match(request.path) | |
152 | except (HTTPException, RequestRedirect), e: | |
153 | response = e | |
154 | else: | |
155 | # check all view processors | |
156 | for processor in self.processors: | |
157 | action = processor.process_view(request, callback, (), args) | |
158 | if action is not None: | |
159 | # it is overriding the default behaviour, this is | |
160 | # short-circuiting the processing, so it returns here | |
161 | return action(environ, start_response) | |
162 | ||
163 | try: | |
164 | response = callback(request, **args) | |
165 | except Exception, exception: | |
166 | # the callback raised some exception, need to process that | |
167 | for processor in reversed(self.processors): | |
168 | # filter it through the exception processor | |
169 | action = processor.process_exception(request, exception) | |
170 | if action is not None: | |
171 | # the exception processor returned some action | |
172 | return action(environ, start_response) | |
173 | # still not handled by a exception processor, so re-raise | |
174 | raise | |
175 | ||
176 | # apply the response processors | |
177 | for processor in reversed(self.processors): | |
178 | response = processor.process_response(request, response) | |
179 | ||
180 | # return the completely processed response | |
181 | return response(environ, start_response) | |
182 | ||
183 | ||
184 | def config_session(self, store, expiration='session'): | |
185 | """ | |
186 | Configures the setting for cookies. You can also disable cookies by | |
187 | setting store to None. | |
188 | """ | |
189 | self.store = store | |
190 | # expiration=session is the default anyway | |
191 | # TODO: add settings to define the expiration date, the domain, the | |
192 | # path any maybe the secure parameter. | |
38 | 193 | |
39 | 194 | |
40 | 195 | class TemplateNotFound(IOError, LookupError): |
70 | 225 | return Response(self.render_to_string(*args, **kwargs)) |
71 | 226 | |
72 | 227 | def render_to_string(self, *args, **kwargs): |
73 | """Load and render a tempalte into a unicode string.""" | |
228 | """Load and render a template into a unicode string.""" | |
74 | 229 | try: |
75 | 230 | template_name, args = args[0], args[1:] |
76 | 231 | except IndexError: |
77 | 232 | raise TypeError('name of template required') |
78 | 233 | return self.get_template(template_name).render(*args, **kwargs) |
234 | ||
235 | ||
236 | class GenshiTemplateLoader(TemplateLoader): | |
237 | """ | |
238 | A unified interface for loading Genshi templates. Actually a quite thin | |
239 | wrapper for Genshi's TemplateLoader. | |
240 | ||
241 | It sets some defaults that differ from the Genshi loader, most notably | |
242 | auto_reload is active. All imporant options can be passed through to | |
243 | Genshi. | |
244 | The default output type is 'html', but can be adjusted easily by changing | |
245 | the `output_type` attribute. | |
246 | """ | |
247 | def __init__(self, search_path, encoding='utf-8', **kwargs): | |
248 | TemplateLoader.__init__(self, search_path, encoding) | |
249 | # import Genshi here, because we don't want a general Genshi | |
250 | # dependency, only a local one | |
251 | from genshi.template import TemplateLoader as GenshiLoader | |
252 | from genshi.template.loader import TemplateNotFound | |
253 | ||
254 | self.not_found_exception = TemplateNotFound | |
255 | # set auto_reload to True per default | |
256 | reload_template = kwargs.pop('auto_reload', True) | |
257 | # get rid of default_encoding as this template loaders overwrites it | |
258 | # with the value of encoding | |
259 | kwargs.pop('default_encoding', None) | |
260 | ||
261 | # now, all arguments are clean, pass them on | |
262 | self.loader = GenshiLoader(search_path, default_encoding=encoding, | |
263 | auto_reload=reload_template, **kwargs) | |
264 | ||
265 | # the default output is HTML but can be overridden easily | |
266 | self.output_type = 'html' | |
267 | self.encoding = encoding | |
268 | ||
269 | def get_template(self, template_name): | |
270 | """Get the template which is at the given name""" | |
271 | try: | |
272 | return self.loader.load(template_name, encoding=self.encoding) | |
273 | except self.not_found_exception, e: | |
274 | # catch the exception raised by Genshi, convert it into a werkzeug | |
275 | # exception (for the sake of consistency) | |
276 | raise TemplateNotFound(template_name) | |
277 | ||
278 | def render_to_string(self, template_name, context=None): | |
279 | """Load and render a template into an unicode string""" | |
280 | # create an empty context if no context was specified | |
281 | context = context or {} | |
282 | tmpl = self.get_template(template_name) | |
283 | # render the template into a unicode string (None means unicode) | |
284 | return tmpl.\ | |
285 | generate(**context).\ | |
286 | render(self.output_type, encoding=None) |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | inyoka.middlewares.profiler | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | Provides a WSGI Profiler middleware for finding bottlenecks. | |
6 | ||
7 | :copyright: 2006-2007 by Armin Ronacher. | |
8 | :license: BSD, see LICENSE for more details. | |
9 | """ | |
10 | import sys | |
11 | try: | |
12 | try: | |
13 | from cProfile import Profile | |
14 | except ImportError: | |
15 | from profile import Profile | |
16 | from pstats import Stats | |
17 | available = True | |
18 | except ImportError: | |
19 | available = False | |
20 | ||
21 | ||
22 | class MergeStream(object): | |
23 | """ | |
24 | A object that redirects `write` calls to multiple streams. | |
25 | Use this to log to both `sys.stdout` and a file:: | |
26 | ||
27 | f = file('profiler.log') | |
28 | stream = MergeStream(sys.stdout, f) | |
29 | profiler = ProfilerMiddleware(app, stream) | |
30 | """ | |
31 | ||
32 | def __init__(self, *streams): | |
33 | if not streams: | |
34 | raise TypeError('at least one stream must be given') | |
35 | self.streams = streams | |
36 | ||
37 | def write(self, data): | |
38 | for stream in self.streams: | |
39 | stream.write(data) | |
40 | ||
41 | ||
42 | class ProfilerMiddleware(object): | |
43 | """ | |
44 | Simple profiler middleware | |
45 | """ | |
46 | ||
47 | def __init__(self, app, stream=None, | |
48 | sort_by=('time', 'calls'), restrictions=()): | |
49 | if not available: | |
50 | raise RuntimeError('the profiler is not available because ' | |
51 | 'profile or pstat is not installed.') | |
52 | self._app = app | |
53 | self._stream = stream or sys.stdout | |
54 | self._sort_by = sort_by | |
55 | self._restrictions = restrictions | |
56 | ||
57 | def __call__(self, environ, start_response): | |
58 | response_body = [] | |
59 | ||
60 | def catching_start_response(status, headers, exc_info=None): | |
61 | start_response(status, headers, exc_info) | |
62 | return response_body.append | |
63 | ||
64 | def runapp(): | |
65 | appiter = self._app(environ, catching_start_response) | |
66 | response_body.extend(appiter) | |
67 | if hasattr(appiter, 'close'): | |
68 | appiter.close() | |
69 | ||
70 | p = Profile() | |
71 | p.runcall(runapp) | |
72 | body = ''.join(response_body) | |
73 | stats = Stats(p) | |
74 | stats.sort_stats(*self._sort_by) | |
75 | ||
76 | self._stream.write('-' * 80) | |
77 | self._stream.write('\nPATH: %r\n' % environ.get('PATH_INFO')) | |
78 | stats.print_stats(*self._restrictions) | |
79 | self._stream.write('-' * 80 + '\n\n') | |
80 | ||
81 | return [body] | |
82 | ||
83 | ||
84 | def make_action(app_factory, hostname='localhost', port=5000, | |
85 | threaded=False, processes=1, stream=None, | |
86 | sort_by=('time', 'calls'), restrictions=()): | |
87 | """ | |
88 | Return a new callback for werkzeug scripts that starts a local server | |
89 | for profiling. | |
90 | """ | |
91 | def action(hostname=('h', hostname), port=('p', port), | |
92 | threaded=threaded, processes=processes): | |
93 | """Start a new development server.""" | |
94 | from werkzeug.serving import run_simple | |
95 | app = ProfilerMiddleware(app_factory(), stream, sort_by, restrictions) | |
96 | run_simple(hostname, port, app, False, None, threaded, processes) | |
97 | return action |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | werkzeug.contrib.reporterstream | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | This module implements a class that can wrap `wsgi.input` in order to | |
6 | be informed about changes of the stream. This is useful if you want | |
7 | to display a progress bar for the upload. | |
8 | ||
9 | :copyright: 2007 by Armin Ronacher. | |
10 | :license: BSD, see LICENSE for more details. | |
11 | """ | |
12 | ||
13 | ||
14 | class BaseReporterStream(object): | |
15 | """ | |
16 | This class can be used to wrap `wsgi.input` in order to be informed about | |
17 | changes of the stream. | |
18 | ||
19 | Usage:: | |
20 | ||
21 | from random import randrange | |
22 | ||
23 | class ReporterStream(BaseReporterStream): | |
24 | ||
25 | def __init__(self, environ): | |
26 | super(ReporterStream, self).__init__(environ, 1024 * 16) | |
27 | self.transport_id = randrange(0, 100000) | |
28 | ||
29 | def processed(self): | |
30 | s = self.environ['my.session.service'] | |
31 | s.store['upload/%s' % self.transport_id] = (self.pos, self.length) | |
32 | s.flush() | |
33 | ||
34 | ||
35 | And before accessing `request.form` or similar attributes add the stream: | |
36 | ||
37 | stream = ReporterStream(environ) | |
38 | environ['wsgi.input'] = stream | |
39 | """ | |
40 | ||
41 | def __init__(self, environ, threshold): | |
42 | self.threshold = threshold | |
43 | self.length = int(environ.get('CONTENT_LENGTH') or 0) | |
44 | self.pos = 0 | |
45 | self.environ = environ | |
46 | self._stream = environ['wsgi.input'] | |
47 | ||
48 | def processed(self): | |
49 | """Called after pos has changed for threshold or a line was read.""" | |
50 | ||
51 | def read(self, size=None): | |
52 | length = self.length | |
53 | threshold = self.threshold | |
54 | buffer = [] | |
55 | ||
56 | if size is None: | |
57 | while self.pos < length: | |
58 | step = min(threshold, length - self.pos) | |
59 | data = self._stream.read(step) | |
60 | self.pos += step | |
61 | self.processed() | |
62 | buffer.append(data) | |
63 | else: | |
64 | read = 0 | |
65 | while read < size: | |
66 | step = min(threshold, length - self.pos) | |
67 | step = min(step, size) | |
68 | data = self._stream.read(step) | |
69 | self.pos += step | |
70 | read += step | |
71 | self.processed() | |
72 | buffer.append(data) | |
73 | ||
74 | return ''.join(buffer) | |
75 | ||
76 | def readline(self, *args): | |
77 | line = self._stream.readline(*args) | |
78 | self.pos += len(line) | |
79 | self.processed() | |
80 | return line | |
81 | ||
82 | def readlines(self, hint=None): | |
83 | result = [] | |
84 | while self.pos < self.length: | |
85 | result.append(self.readline()) | |
86 | return result |
0 | # -*- coding: utf-8 -*- | |
1 | r""" | |
2 | werkzeug.contrib.securecookie | |
3 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | This module implements a cookie that is not alterable from the client | |
6 | because it adds a checksum the server checks for. You can use it as | |
7 | session replacement if all you have is a user id or something to mark | |
8 | a logged in user. | |
9 | ||
10 | Keep in mind that the data is still readable from the client as a | |
11 | normal cookie is. However you don't have to store and flush the | |
12 | sessions you have at the server. | |
13 | ||
14 | Example usage: | |
15 | ||
16 | >>> from werkzeug.contrib.securecookie import SecureCookie | |
17 | >>> x = SecureCookie({"foo": 42, "baz": (1, 2, 3)}, "deadbeef") | |
18 | ||
19 | Dumping into a string so that one can store it in a cookie: | |
20 | ||
21 | >>> value = x.serialize() | |
22 | ||
23 | Loading from that string again: | |
24 | ||
25 | >>> x = SecureCookie.unserialize(value, "deadbeef") | |
26 | >>> x["baz"] | |
27 | (1, 2, 3) | |
28 | ||
29 | If someone modifies the cookie and the checksum is wrong the unserialize | |
30 | method will fail silently and return a new empty `SecureCookie` object. | |
31 | ||
32 | Keep in mind that the values will be visible in the cookie so do not | |
33 | store data in a cookie you don't want the user to see. | |
34 | ||
35 | Application Integration | |
36 | ======================= | |
37 | ||
38 | If you are using the werkzeug request objects you could integrate the | |
39 | secure cookie into your application like this:: | |
40 | ||
41 | from werkzeug import BaseRequest, cached_property | |
42 | from werkzeug.contrib.securecookie import SecureCookie | |
43 | ||
44 | # don' use this key but a different one. you could just use | |
45 | # os.unrandom(20) to get something random | |
46 | SECRET_KEY = '\xfa\xdd\xb8z\xae\xe0}4\x8b\xea' | |
47 | ||
48 | class Request(BaseRequest): | |
49 | ||
50 | @cached_property | |
51 | def client_session(self): | |
52 | data = self.cookies.get('session_data') | |
53 | if not data: | |
54 | return SecureCookie(secret_key=SECRET_KEY) | |
55 | return SecureCookie.unserialize(data, SECRET_KEY) | |
56 | ||
57 | def application(environ, start_response): | |
58 | request = Request(environ, start_response) | |
59 | ||
60 | # get a response object here | |
61 | response = ... | |
62 | ||
63 | if request.client_session.should_save: | |
64 | session_data = request.client_session.serialize() | |
65 | response.set_cookie('session_data', session_data, | |
66 | httponly=True) | |
67 | return response(environ, start_response) | |
68 | ||
69 | ||
70 | :copyright: 2007 by Armin Ronacher, Thomas Johansson. | |
71 | :license: BSD, see LICENSE for more details. | |
72 | """ | |
73 | try: | |
74 | from hashlib import sha1 | |
75 | except ImportError: | |
76 | from sha import new as sha1 | |
77 | from binascii import Error as BinASCIIError | |
78 | from datetime import datetime | |
79 | from time import time, mktime, gmtime | |
80 | from random import Random | |
81 | from cPickle import loads, dumps, HIGHEST_PROTOCOL | |
82 | from werkzeug import url_quote_plus, url_unquote_plus | |
83 | from werkzeug.contrib.sessions import ModificationTrackingDict, generate_key | |
84 | ||
85 | ||
86 | class UnquoteError(Exception): | |
87 | pass | |
88 | ||
89 | ||
90 | def pickle_quote(value): | |
91 | """Pickle and url encode a value.""" | |
92 | result = None | |
93 | for protocol in xrange(HIGHEST_PROTOCOL + 1): | |
94 | data = ''.join(dumps(value, protocol).encode('base64').splitlines()).strip() | |
95 | if result is None or len(result) > len(data): | |
96 | result = data | |
97 | return result | |
98 | ||
99 | ||
100 | def pickle_unquote(string): | |
101 | """URL decode a string and load it into pickle""" | |
102 | try: | |
103 | return loads(string.decode('base64')) | |
104 | # unfortunately pickle can cause pretty every error here. | |
105 | # if we get one we catch it and convert it into an UnquoteError | |
106 | except Exception, e: | |
107 | raise UnquoteError(str(e)) | |
108 | ||
109 | ||
110 | class SecureCookie(ModificationTrackingDict): | |
111 | """ | |
112 | Represents a secure cookie. You can subclass this class and provide | |
113 | an alternative hash method. The import thing is that the hash method | |
114 | is a function with a similar interface to the hashlib. Required | |
115 | methods are update() and digest(). | |
116 | """ | |
117 | __slots__ = ModificationTrackingDict.__slots__ + ('secret_key', 'new') | |
118 | ||
119 | hash_method = sha1 | |
120 | ||
121 | def __init__(self, data=None, secret_key=None, new=True): | |
122 | ModificationTrackingDict.__init__(self, data or ()) | |
123 | self.secret_key = secret_key | |
124 | self.new = new | |
125 | ||
126 | def __repr__(self): | |
127 | return '<%s %s%s>' % ( | |
128 | self.__class__.__name__, | |
129 | dict.__repr__(self), | |
130 | self.should_save and '*' or '' | |
131 | ) | |
132 | ||
133 | def should_save(self): | |
134 | """True if the session should be saved.""" | |
135 | return self.modified | |
136 | should_save = property(should_save) | |
137 | ||
138 | def new_salt(self, secret_key): | |
139 | """Return a new salt Return value must be 4 bytes long.""" | |
140 | return generate_key()[:4] | |
141 | ||
142 | def serialize(self, expires=None): | |
143 | """ | |
144 | Serialize the secure cookie into a string. | |
145 | ||
146 | If expires is provided, the session will be automatically invalidated | |
147 | after expiration when you unseralize it. This provides better | |
148 | protection against session cookie theft. | |
149 | """ | |
150 | if self.secret_key is None: | |
151 | raise RuntimeError('no secret key defined') | |
152 | if expires: | |
153 | if isinstance(expires, datetime): | |
154 | expires = expires.utctimetuple() | |
155 | elif isinstance(expires, (int, long, float)): | |
156 | expires = gmtime(expires) | |
157 | self['_expires'] = int(mktime(expires)) | |
158 | result = [] | |
159 | salt = self.new_salt(self.secret_key) | |
160 | hash = self.hash_method(self.secret_key + salt) | |
161 | for key, value in self.iteritems(): | |
162 | result.append('%s=%s' % ( | |
163 | url_quote_plus(key), | |
164 | pickle_quote(value) | |
165 | )) | |
166 | hash.update('|' + result[-1]) | |
167 | return '%s%s?%s' % ( | |
168 | salt, | |
169 | hash.digest().encode('base64').strip(), | |
170 | '&'.join(result) | |
171 | ) | |
172 | ||
173 | def unserialize(cls, string, secret_key): | |
174 | """Load the secure cookie from a serialized string.""" | |
175 | if isinstance(string, unicode): | |
176 | string = string.encode('utf-8', 'ignore') | |
177 | salt = string[:4] | |
178 | try: | |
179 | base64_hash, data = string[4:].split('?', 1) | |
180 | except (ValueError, IndexError): | |
181 | items = () | |
182 | else: | |
183 | items = {} | |
184 | hash = sha1(secret_key + salt) | |
185 | for item in data.split('&'): | |
186 | hash.update('|' + item) | |
187 | if not '=' in item: | |
188 | items = None | |
189 | break | |
190 | key, value = item.split('=', 1) | |
191 | items[url_unquote_plus(key)] = value | |
192 | ||
193 | # no parsing error and the hash looks okay, we can now | |
194 | # sercurely unpickle our cookie. | |
195 | try: | |
196 | client_hash = base64_hash.decode('base64') | |
197 | except Exception: | |
198 | items = client_hash = None | |
199 | if items is not None and client_hash == hash.digest(): | |
200 | try: | |
201 | for key, value in items.iteritems(): | |
202 | items[key] = pickle_unquote(value) | |
203 | except UnquoteError: | |
204 | items = () | |
205 | else: | |
206 | if '_expires' in items: | |
207 | if time() > items['_expires']: | |
208 | items = () | |
209 | else: | |
210 | del items['_expires'] | |
211 | else: | |
212 | items = () | |
213 | return cls(items, secret_key, False) | |
214 | unserialize = classmethod(unserialize) | |
215 | ||
216 | def load_cookie(cls, request, key='session', secret_key=None): | |
217 | """ | |
218 | Loads a SecureCookie from a cookie in request. If the cookie is not | |
219 | set, a new SecureCookie instanced is returned. | |
220 | """ | |
221 | data = request.cookies.get(key) | |
222 | if not data: | |
223 | return SecureCookie(secret_key=secret_key) | |
224 | return SecureCookie.unserialize(data, secret_key) | |
225 | load_cookie = classmethod(load_cookie) | |
226 | ||
227 | def save_cookie(self, response, key='session', expires=None, | |
228 | session_expires=None, max_age=None, path='/', domain=None, | |
229 | secure=None, httponly=False, force=False): | |
230 | """Saves the SecureCookie in a cookie on response.""" | |
231 | if force or self.should_save: | |
232 | data = self.serialize(session_expires or expires) | |
233 | response.set_cookie(key, data, expires=expires, max_age=max_age, | |
234 | path=path, domain=domain, secure=secure, | |
235 | httponly=httponly) |
23 | 23 | time of the files. It sessions are stored in the database the new() |
24 | 24 | method should add an expiration timestamp for the session. |
25 | 25 | |
26 | For better flexibility it's recommended to not use the middleware but the | |
27 | store and session object directly in the application dispatching:: | |
28 | ||
29 | session_store = FilesystemSessionStore() | |
30 | ||
31 | def application(environ, start_response): | |
32 | request = Request(environ) | |
33 | sid = request.cookie.get('cookie_name') | |
34 | if sid is None: | |
35 | request.session = session_store.new() | |
36 | else: | |
37 | request.session = session_store.get(sid) | |
38 | response = get_the_response_object(request) | |
39 | if request.session.should_save: | |
40 | session_store.save(request.session) | |
41 | response.set_cookie('cookie_name', request.session.sid) | |
42 | return response(environ, start_response) | |
43 | ||
26 | 44 | |
27 | 45 | :copyright: 2007 by Armin Ronacher. |
28 | 46 | :license: BSD, see LICENSE for more details. |
29 | 47 | """ |
30 | 48 | import re |
49 | import os | |
31 | 50 | from os import path, unlink |
32 | 51 | from time import time |
33 | from random import Random | |
52 | from random import Random, random | |
34 | 53 | try: |
35 | 54 | from hashlib import sha1 |
36 | 55 | except ImportError: |
37 | 56 | from sha import new as sha1 |
38 | 57 | from cPickle import dump, load, HIGHEST_PROTOCOL |
39 | from Cookie import SimpleCookie, Morsel | |
40 | from werkzeug.utils import ClosingIterator | |
58 | from werkzeug.utils import ClosingIterator, dump_cookie, parse_cookie | |
41 | 59 | |
42 | 60 | |
43 | 61 | _sha1_re = re.compile(r'^[a-fA-F0-9]{40}$') |
44 | 62 | |
45 | 63 | |
46 | class Session(dict): | |
47 | """ | |
48 | Subclass of a dict that keeps track of direct object changes. Changes | |
49 | in mutable structures are not tracked, for those you have to set | |
50 | `modified` to `True` by hand. | |
51 | """ | |
52 | ||
53 | __slots__ = ('modified', 'sid', 'new') | |
54 | ||
55 | def __init__(self, data, sid, new=False): | |
56 | dict.__init__(self, data) | |
64 | def _urandom(): | |
65 | if hasattr(os, 'urandom'): | |
66 | return os.urandom(30) | |
67 | return random() | |
68 | ||
69 | ||
70 | def generate_key(salt=None): | |
71 | return sha1('%s%s%s' % (salt, time(), _urandom())).hexdigest() | |
72 | ||
73 | ||
74 | class ModificationTrackingDict(dict): | |
75 | __slots__ = ('modified',) | |
76 | ||
77 | def __init__(self, *args, **kwargs): | |
78 | dict.__init__(self, *args, **kwargs) | |
57 | 79 | self.modified = False |
58 | self.sid = sid | |
59 | self.new = new | |
60 | 80 | |
61 | 81 | def __repr__(self): |
62 | 82 | return '<%s %s%s>' % ( |
63 | 83 | self.__class__.__name__, |
64 | dict.__repr__(self), | |
65 | self.should_save and '*' or '' | |
84 | dict.__repr__(self) | |
66 | 85 | ) |
67 | 86 | |
68 | def should_save(self): | |
69 | """True if the session should be saved.""" | |
70 | return self.modified or self.new | |
71 | should_save = property(should_save) | |
72 | ||
73 | 87 | def copy(self): |
74 | """Create a flat copy of the session.""" | |
75 | result = self.__class__(dict(self), self.sid, self.new) | |
76 | result.modified = self.modified | |
88 | """Create a flat copy of the dict.""" | |
89 | missing = object() | |
90 | result = object.__new__(self.__class__) | |
91 | for name in self.__slots__: | |
92 | val = getattr(self, name, missing) | |
93 | if val is not missing: | |
94 | setattr(result, name, val) | |
77 | 95 | return result |
96 | ||
97 | def __copy__(self): | |
98 | return self.copy() | |
78 | 99 | |
79 | 100 | def call_with_modification(f): |
80 | 101 | def oncall(self, *args, **kw): |
100 | 121 | del call_with_modification |
101 | 122 | |
102 | 123 | |
124 | class Session(ModificationTrackingDict): | |
125 | """ | |
126 | Subclass of a dict that keeps track of direct object changes. Changes | |
127 | in mutable structures are not tracked, for those you have to set | |
128 | `modified` to `True` by hand. | |
129 | """ | |
130 | __slots__ = ModificationTrackingDict.__slots__ + ('sid', 'new') | |
131 | ||
132 | def __init__(self, data, sid, new=False): | |
133 | ModificationTrackingDict.__init__(self, data) | |
134 | self.sid = sid | |
135 | self.new = new | |
136 | ||
137 | def __repr__(self): | |
138 | return '<%s %s%s>' % ( | |
139 | self.__class__.__name__, | |
140 | dict.__repr__(self), | |
141 | self.should_save and '*' or '' | |
142 | ) | |
143 | ||
144 | def should_save(self): | |
145 | """True if the session should be saved.""" | |
146 | return self.modified or self.new | |
147 | should_save = property(should_save) | |
148 | ||
149 | ||
103 | 150 | class SessionStore(object): |
104 | 151 | """ |
105 | 152 | Baseclass for all session stores. The Werkzeug contrib module does not |
118 | 165 | |
119 | 166 | def generate_key(self, salt=None): |
120 | 167 | """Simple function that generates a new session key.""" |
121 | return sha1('%s|%s|%s' % (Random(salt).random(), time(), | |
122 | salt)).hexdigest() | |
168 | return generate_key(salt) | |
123 | 169 | |
124 | 170 | def new(self): |
125 | 171 | """Generate a new session.""" |
196 | 242 | the WSGI environ. It automatically sets cookies and restores sessions. |
197 | 243 | |
198 | 244 | However a middleware is not the preferred solution because it won't be as |
199 | fast as sessions managed by the application itself. | |
245 | fast as sessions managed by the application itself and will put a key into | |
246 | the WSGI environment only relevant for the application which is against | |
247 | the concept of WSGI. | |
200 | 248 | """ |
201 | 249 | |
202 | 250 | def __init__(self, app, store, cookie_name='session_id', |
203 | cookie_age=None, cookie_path=None, cookie_domain=None, | |
204 | cookie_secure=None, environ_key='werkzeug.session'): | |
251 | cookie_age=None, cookie_expires=None, cookie_path='/', | |
252 | cookie_domain=None, cookie_secure=None, | |
253 | cookie_httponly=False, environ_key='werkzeug.session'): | |
254 | """ | |
255 | The cookie parameters are the same as for the `dump_cookie` function | |
256 | just prefixed with "cookie_". Additionally "max_age" is "cookie_age" | |
257 | for backwards compatibility. | |
258 | """ | |
205 | 259 | self.app = app |
206 | 260 | self.store = store |
207 | 261 | self.cookie_name = cookie_name |
208 | 262 | self.cookie_age = cookie_age |
263 | self.cookie_expires = cookie_expires | |
209 | 264 | self.cookie_path = cookie_path |
210 | 265 | self.cookie_domain = cookie_domain |
211 | 266 | self.cookie_secure = cookie_secure |
267 | self.cookie_httponly = cookie_httponly | |
212 | 268 | self.environ_key = environ_key |
213 | 269 | |
214 | 270 | def __call__(self, environ, start_response): |
215 | cookie = SimpleCookie(environ.get('HTTP_COOKIE', '')) | |
216 | morsel = cookie.get(self.cookie_name, None) | |
217 | if morsel is None: | |
271 | cookie = parse_cookie(environ.get('HTTP_COOKIE', '')) | |
272 | sid = cookie.get(self.cookie_name, None) | |
273 | if sid is None: | |
218 | 274 | session = self.store.new() |
219 | 275 | else: |
220 | session = self.store.get(morsel.value) | |
276 | session = self.store.get(sid) | |
221 | 277 | environ[self.environ_key] = session |
222 | 278 | |
223 | 279 | def injecting_start_response(status, headers, exc_info=None): |
224 | 280 | if session.should_save: |
225 | morsel = Morsel() | |
226 | morsel.key = self.cookie_name | |
227 | morsel.coded_value = session.sid | |
228 | if self.cookie_age is not None: | |
229 | morsel['max-age'] = self.cookie_age | |
230 | morsel['expires'] = cookie_date(time() + self.cookie_age) | |
231 | if self.cookie_domain is not None: | |
232 | morsel['domain'] = self.cookie_domain | |
233 | if self.cookie_path is not None: | |
234 | morsel['path'] = self.cookie_path | |
235 | if self.cookie_secure is not None: | |
236 | morsel['secure'] = self.cookie_secure | |
237 | headers.append(tuple(str(morsel).split(':', 1))) | |
281 | self.store.save(session) | |
282 | headers.append(('Set-Cookie', dump_cookie(self.cookie_name, | |
283 | session.sid, self.cookie_age, | |
284 | self.cookie_expires, self.cookie_path, | |
285 | self.cookie_domain, self.cookie_secure, | |
286 | self.cookie_httponly))) | |
238 | 287 | return start_response(status, headers, exc_info) |
239 | 288 | return ClosingIterator(self.app(environ, injecting_start_response), |
240 | 289 | lambda: self.store.save_if_modified(session)) |
0 | """ | |
1 | werkzeug.contrib.testtools | |
2 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | |
3 | ||
4 | This Module implements a extended wrappers for simplified Testing | |
5 | ||
6 | `TestResponse` | |
7 | a response wrapper wich adds various cached attributes for | |
8 | simplified assertions on various contenttypes | |
9 | ||
10 | :copyright: 2007 by Ronny Pfannschmidt. | |
11 | :license: BSD, see LICENSE for more details. | |
12 | """ | |
13 | from werkzeug import Response, cached_property, import_string | |
14 | ||
15 | ||
16 | class ContentAccessors(object): | |
17 | ||
18 | def xml(self): | |
19 | """Get an etree if possible.""" | |
20 | if 'xml' not in self.mimetype: | |
21 | raise AttributeError( | |
22 | 'Not a XML response (Content-Type: %s)' | |
23 | % self.mimetype) | |
24 | for module in ['xml.etree.ElementTree', 'ElementTree', | |
25 | 'elementtree.ElementTree']: | |
26 | etree = import_string(module, silent=True) | |
27 | if etree is not None: | |
28 | return etree.XML(self.body) | |
29 | raise RuntimeError('You must have ElementTree installed ' | |
30 | 'to use TestResponse.xml') | |
31 | xml = cached_property(xml) | |
32 | ||
33 | def lxml(self): | |
34 | """Get an lxml etree if possible.""" | |
35 | if ('html' not in self.mimetype and 'xml' not in self.mimetype): | |
36 | raise AttributeError('Not an HTML/XML response') | |
37 | from lxml import etree | |
38 | try: | |
39 | from lxml.html import fromstring | |
40 | except ImportError: | |
41 | fromstring = etree.HTML | |
42 | if self.mimetype=='text/html': | |
43 | return fromstring(self.data) | |
44 | return etree.XML(self.data) | |
45 | lxml = cached_property(lxml) | |
46 | ||
47 | def json(self): | |
48 | """Get the result of simplejson.loads if possible.""" | |
49 | if 'json' not in self.mimetype: | |
50 | raise AttributeError('Not a JSON response') | |
51 | from simplejson import loads | |
52 | return loads(self.data) | |
53 | json = cached_property(json) | |
54 | ||
55 | ||
56 | class TestResponse(Response, ContentAccessors): | |
57 | """ | |
58 | Pass this to `werkzeug.test.Client` for easier unittesting. | |
59 | """ |
19 | 19 | |
20 | 20 | |
21 | 21 | try: |
22 | system_exceptions = (GeneratorExist,) | |
22 | system_exceptions = (GeneratorExit,) | |
23 | 23 | except NameError: |
24 | 24 | system_exceptions = () |
25 | 25 | system_exceptions += (SystemExit, KeyboardInterrupt) |
114 | 114 | |
115 | 115 | def format_exception(self, exc_info): |
116 | 116 | """Format a text/plain traceback.""" |
117 | return self.create_debug_context({}, exc_info, True).plaintb | |
117 | return self.create_debug_context({ | |
118 | 'wsgi.run_once': True | |
119 | }, exc_info, True).plaintb + '\n' | |
118 | 120 | |
119 | 121 | def create_debug_context(self, environ, exc_info, simple=False): |
120 | 122 | exception_type, exception_value, tb = exc_info |
224 | 226 | self.middleware = middleware |
225 | 227 | self.globals = frame.f_globals |
226 | 228 | code.InteractiveInterpreter.__init__(self, frame.f_locals) |
227 | self.prompt = '>>> ' | |
229 | self.more = False | |
228 | 230 | self.buffer = [] |
229 | 231 | |
230 | 232 | def runsource(self, source): |
233 | if isinstance(source, unicode): | |
234 | source = source.encode('utf-8') | |
235 | source = source.rstrip() + '\n' | |
231 | 236 | ThreadedStream.push() |
232 | prompt = self.prompt | |
237 | prompt = self.more and '... ' or '>>> ' | |
233 | 238 | try: |
234 | 239 | source_to_eval = ''.join(self.buffer + [source]) |
235 | 240 | if code.InteractiveInterpreter.runsource(self, |
236 | 241 | source_to_eval, '<debugger>', 'single'): |
237 | self.prompt = '... ' | |
242 | self.more = True | |
238 | 243 | self.buffer.append(source) |
239 | 244 | else: |
240 | self.prompt = '>>> ' | |
245 | self.more = False | |
241 | 246 | del self.buffer[:] |
242 | 247 | finally: |
243 | source = source.encode('utf-8') | |
244 | 248 | return prompt + source + ThreadedStream.fetch() |
245 | 249 | |
246 | 250 | def runcode(self, code): |
247 | 251 | try: |
248 | 252 | exec code in self.globals, self.locals |
249 | 253 | except: |
250 | self.showtraceback() | |
251 | ||
252 | def showtraceback(self): | |
253 | self.write(self.middleware.format_exception(sys.exc_info())) | |
254 | self.write(self.middleware.format_exception(sys.exc_info())) | |
254 | 255 | |
255 | 256 | def write(self, data): |
256 | 257 | sys.stdout.write(data) |
0 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" | |
1 | "http://www.w3.org/TR/html4/loose.dtd"> | |
2 | <html> | |
3 | <head> | |
4 | <title>$escape(exception_type) in $escape(last_frame['basename']) (Werkzeug Debugger)</title> | |
5 | <link rel="stylesheet" href="__traceback__?resource=style.css&mimetype=text/css" type="text/css"> | |
6 | <script type="text/javascript" src="__traceback__?resource=jquery.js&mimetype=text/javascript"></script> | |
7 | <script type="text/javascript" src="__traceback__?resource=debugger.js&mimetype=text/javascript"></script> | |
8 | </head> | |
9 | <body> | |
10 | <div class="traceback_wrapper"> | |
11 | <h1>$escape(exception_type)</h1> | |
12 | <p class="errormsg">$escape(exception_value)</p> | |
13 | ||
14 | <p class="errorline"> | |
15 | $escape(last_frame['filename']) in | |
16 | $escape(last_frame['function']), | |
17 | line $last_frame['lineno'] | |
18 | </p> | |
19 | ||
20 | <h2 onclick="changeTB()" class="tb">Traceback <span>(toggle raw view)</span></h2> | |
21 | <div id="interactive"> | |
22 | <p class="text">A problem occurred in your Python WSGI application. | |
23 | Here is the sequence of function calls leading up to the error, in the order | |
24 | they occurred. Activate a code line to toggle context lines.</p> | |
25 | ||
26 | <% for num, frame in enumerate(frames) %> | |
27 | <div class="frame" id="frame-$num"> | |
28 | <h3 class="fn"><em>$escape(frame['function'])</em> in <tt>$escape(frame['filename'])</tt></h3> | |
29 | <a class="locals" href="javascript:toggleFrameVars($num)">[inspect]</a> | |
30 | <% if evalex %><a class="eval" href="javascript:toggleInterpreter($num)">[console]</a><% endif %> | |
31 | $code_table(frame) | |
32 | $var_table(frame['vars']) | |
33 | <% if evalex %> | |
34 | <form class="exec_code" action=""> | |
35 | <pre class="output">[console ready]</pre> | |
36 | <input type="hidden" name="tb" value="$tb_uid"> | |
37 | <input type="hidden" name="frame" value="$frame['frame_uid']"> | |
38 | <input type="text" name="cmd" class="input" value=""> | |
39 | </form> | |
40 | <% endif %> | |
41 | </div> | |
42 | <% endfor %> | |
43 | </div> | |
44 | ||
45 | <div id="plain"> | |
46 | <p class="text">Here is the plain Python traceback for copy and paste:</p> | |
47 | <pre class="plain">$escape(plaintb)</pre> | |
48 | <p class="text pastebininfo"> | |
49 | <a href="javascript:pasteIt()">Create a new Paste</a> with | |
50 | this traceback in the lodgeit pastebin. | |
51 | </p> | |
52 | </div> | |
53 | ||
54 | <% if req_vars %> | |
55 | <h2>Request Data</h2> | |
56 | <p class="text">The following list contains all important request variables. | |
57 | Select a header to expand the list.</p> | |
58 | <% for num, (key, info) in enumerate(req_vars) %> | |
59 | <dl> | |
60 | <dt onclick="toggleTableVars($num)">$escape(key)</dt> | |
61 | <dd id="tvar-$num">$var_table(info)</dd> | |
62 | </dl> | |
63 | <% endfor %> | |
64 | <% endif %> | |
65 | </div> | |
66 | ||
67 | <div id="footer"> | |
68 | Brought to you by <span class="arthur">DON'T PANIC</span>, your friendly | |
69 | Werkzeug powered traceback interpreter. | |
70 | </div> | |
71 | </body> | |
72 | </html> | |
73 | ||
74 | <!-- Plain traceback: | |
75 | ||
76 | <%py | |
77 | import re | |
78 | print re.sub('-{2,}', '-', plaintb) | |
79 | %> | |
80 | --> |
0 | <table class="code"> | |
1 | <% for line in lines %> | |
2 | <tr class="$line.mode"> | |
3 | <td class="lineno">$line.lineno</td> | |
4 | <td class="code">$line.code</td> | |
5 | </tr> | |
6 | <% endfor %> | |
7 | </table> |
0 | HISTORY = {}; | |
1 | HISTORY_POSITIONS = {}; | |
2 | ||
3 | function changeTB() { | |
4 | $('#interactive').slideToggle('fast'); | |
5 | $('#plain').slideToggle('fast'); | |
6 | } | |
7 | ||
8 | function toggleFrameVars(num) { | |
9 | $('#frame-' + num + ' .vars').slideToggle('fast'); | |
10 | } | |
11 | ||
12 | function toggleInterpreter(num) { | |
13 | $('#frame-' + num + ' .exec_code').slideToggle('fast', function() { | |
14 | if ($(this).css('display') == 'block') | |
15 | $('input.input', this).focus(); | |
16 | }); | |
17 | } | |
18 | ||
19 | function toggleTableVars(num) { | |
20 | $('#tvar-' + num + ' .vars').slideToggle('fast'); | |
21 | } | |
22 | ||
23 | function getHistory(tb, frame) { | |
24 | var key = tb + '||' + frame; | |
25 | if (key in HISTORY) | |
26 | var h = HISTORY[key]; | |
27 | else { | |
28 | var h = HISTORY[key] = ['']; | |
29 | HISTORY_POSITIONS[key] = 0; | |
30 | } | |
31 | return { | |
32 | history: h, | |
33 | setPos: function(val) { | |
34 | HISTORY_POSITIONS[key] = val; | |
35 | }, | |
36 | getPos: function() { | |
37 | return HISTORY_POSITIONS[key]; | |
38 | }, | |
39 | getCurrent: function() { | |
40 | return h[HISTORY_POSITIONS[key]]; | |
41 | } | |
42 | }; | |
43 | } | |
44 | ||
45 | function addToHistory(tb, frame, value) { | |
46 | var h = getHistory(tb, frame); | |
47 | var tmp = h.history.pop(); | |
48 | h.history.push(value); | |
49 | if (tmp != undefined) | |
50 | h.history.push(tmp); | |
51 | h.setPos(h.history.length - 1); | |
52 | } | |
53 | ||
54 | function backInHistory(tb, frame, input) { | |
55 | var pos, h = getHistory(tb, frame); | |
56 | if ((pos = h.getPos()) > 0) | |
57 | h.setPos(pos - 1); | |
58 | input.value = h.getCurrent(); | |
59 | } | |
60 | ||
61 | function forwardInHistory(tb, frame, input) { | |
62 | var pos, h = getHistory(tb, frame); | |
63 | if ((pos = h.getPos()) < h.history.length - 1) | |
64 | h.setPos(pos + 1); | |
65 | input.value = h.getCurrent(); | |
66 | } | |
67 | ||
68 | function sendCommand(tb, frame, cmd, output) { | |
69 | addToHistory(tb, frame, cmd); | |
70 | $.get('__traceback__', { | |
71 | tb: tb, | |
72 | frame: frame, | |
73 | code: cmd + '\n' | |
74 | }, function(data) { | |
75 | var x = output.append($('<div>').text(data))[0]; | |
76 | x.scrollTop = x.scrollHeight; | |
77 | }); | |
78 | } | |
79 | ||
80 | function pasteIt() { | |
81 | var info = $('#plain p.pastebininfo'); | |
82 | var orig = info.html(); | |
83 | info.html('<em>submitting traceback...</em>'); | |
84 | ||
85 | $.ajax({ | |
86 | type: 'POST', | |
87 | url: '__traceback__?pastetb=yes', | |
88 | data: $('#plain pre.plain').text(), | |
89 | dataType: 'json', | |
90 | error: function() { | |
91 | alert('Submitting paste failed. Make sure you have a\n' + | |
92 | 'working internet connection.'); | |
93 | info.html(orig); | |
94 | }, | |
95 | success: function(result) { | |
96 | info.text('Submitted paste: ').append( | |
97 | $('<a>').attr('href', result.url).text('#' + result.paste_id) | |
98 | ); | |
99 | } | |
100 | }); | |
101 | } | |
102 | ||
103 | $(document).ready(function() { | |
104 | $('.exec_code').hide(); | |
105 | $('.vars').hide(); | |
106 | $('.code .pre').hide(); | |
107 | $('.code .post').hide(); | |
108 | ||
109 | $('.exec_code').submit(function() { | |
110 | sendCommand(this.tb.value, this.frame.value, this.cmd.value, | |
111 | $('.output', this)); | |
112 | this.cmd.value = ''; | |
113 | return false; | |
114 | }); | |
115 | ||
116 | $('.code').click(function() { | |
117 | $('.pre', $(this)).toggle(); | |
118 | $('.post', $(this)).toggle(); | |
119 | }); | |
120 | ||
121 | $('.exec_code input.input').keypress(function(e) { | |
122 | if (e.charCode == 100 && e.ctrlKey) { | |
123 | $('.output', $(this).parent()).text('--- screen cleared ---'); | |
124 | return false; | |
125 | } | |
126 | else if (e.charCode == 0 && (e.keyCode == 38 || e.keyCode == 40)) { | |
127 | var parent = $(this).parent(); | |
128 | var tb = $('input[@name="tb"]', parent).attr('value'); | |
129 | var frame = $('input[@name="frame"]', parent).attr('value'); | |
130 | if (e.keyCode == 38) | |
131 | backInHistory(tb, frame, this); | |
132 | else | |
133 | forwardInHistory(tb, frame, this); | |
134 | return false; | |
135 | } | |
136 | return true; | |
137 | }); | |
138 | }); |
0 | eval(function(p,a,c,k,e,d){e=function(c){return(c<a?"":e(parseInt(c/a)))+((c=c%a)>35?String.fromCharCode(c+29):c.toString(36))};if(!''.replace(/^/,String)){while(c--){d[e(c)]=k[c]||e(c)}k=[function(e){return d[e]}];e=function(){return'\\w+'};c=1};while(c--){if(k[c]){p=p.replace(new RegExp('\\b'+e(c)+'\\b','g'),k[c])}}return p}('7(1C 1w.6=="T"){1w.T=1w.T;B 6=u(a,c){7(1w==q)v 1p 6(a,c);a=a||17;7(6.1t(a))v 1p 6(17)[6.E.27?"27":"2O"](a);7(1C a=="23"){B m=/^[^<]*(<(.|\\s)+>)[^>]*$/.2Q(a);7(m)a=6.3k([m[1]]);J v 1p 6(c).2o(a)}v q.6r(a.1l==2y&&a||(a.3Y||a.I&&a!=1w&&!a.24&&a[0]!=T&&a[0].24)&&6.3M(a)||[a])};7(1C $!="T")6.2S$=$;B $=6;6.E=6.8p={3Y:"1.1.2",8q:u(){v q.I},I:0,2b:u(1T){v 1T==T?6.3M(q):q[1T]},2r:u(a){B L=6(a);L.6p=q;v L},6r:u(a){q.I=0;[].1g.14(q,a);v q},K:u(E,1E){v 6.K(q,E,1E)},2h:u(1c){B 4c=-1;q.K(u(i){7(q==1c)4c=i});v 4c},1I:u(1Y,O,C){B 1c=1Y;7(1Y.1l==3t)7(O==T)v q.I&&6[C||"1I"](q[0],1Y)||T;J{1c={};1c[1Y]=O}v q.K(u(2h){P(B H 1x 1c)6.1I(C?q.1q:q,H,6.H(q,1c[H],C,2h,H))})},1m:u(1Y,O){v q.1I(1Y,O,"30")},2L:u(e){7(1C e=="23")v q.3u().3r(17.8t(e));B t="";6.K(e||q,u(){6.K(q.2I,u(){7(q.24!=8)t+=q.24!=1?q.60:6.E.2L([q])})});v t},2K:u(){B a=6.3k(1A);v q.K(u(){B b=a[0].3l(U);q.11.2X(b,q);22(b.1b)b=b.1b;b.4C(q)})},3r:u(){v q.3j(1A,U,1,u(a){q.4C(a)})},5i:u(){v q.3j(1A,U,-1,u(a){q.2X(a,q.1b)})},5j:u(){v q.3j(1A,12,1,u(a){q.11.2X(a,q)})},5t:u(){v q.3j(1A,12,-1,u(a){q.11.2X(a,q.2e)})},4g:u(){v q.6p||6([])},2o:u(t){v q.2r(6.31(q,u(a){v 6.2o(t,a)}),t)},4Y:u(4N){v q.2r(6.31(q,u(a){B a=a.3l(4N!=T?4N:U);a.$1H=16;v a}))},1D:u(t){v q.2r(6.1t(t)&&6.2q(q,u(2z,2h){v t.14(2z,[2h])})||6.3z(t,q))},2g:u(t){v q.2r(t.1l==3t&&6.3z(t,q,U)||6.2q(q,u(a){v(t.1l==2y||t.3Y)?6.3y(a,t)<0:a!=t}))},1M:u(t){v q.2r(6.2k(q.2b(),t.1l==3t?6(t).2b():t.I!=T&&(!t.1f||t.1f=="8v")?t:[t]))},4l:u(1s){v 1s?6.1D(1s,q).r.I>0:12},1a:u(1a){v 1a==T?(q.I?q[0].O:16):q.1I("O",1a)},4U:u(1a){v 1a==T?(q.I?q[0].2t:16):q.3u().3r(1a)},3j:u(1E,1P,3Z,E){B 4Y=q.I>1;B a=6.3k(1E);7(3Z<0)a.8w();v q.K(u(){B 1c=q;7(1P&&6.1f(q,"1P")&&6.1f(a[0],"3m"))1c=q.5J("20")[0]||q.4C(17.6n("20"));6.K(a,u(){E.14(1c,[4Y?q.3l(U):q])})})}};6.1z=6.E.1z=u(){B 1O=1A[0],a=1;7(1A.I==1){1O=q;a=0}B H;22(H=1A[a++])P(B i 1x H)1O[i]=H[i];v 1O};6.1z({8x:u(){7(6.2S$)$=6.2S$;v 6},1t:u(E){v!!E&&1C E!="23"&&!E.1f&&1C E[0]=="T"&&/u/i.1n(E+"")},4B:u(D){v D.66&&D.5I&&!D.5I.64},1f:u(D,Y){v D.1f&&D.1f.3K()==Y.3K()},K:u(1c,E,1E){7(1c.I==T)P(B i 1x 1c)E.14(1c[i],1E||[i,1c[i]]);J P(B i=0,6q=1c.I;i<6q;i++)7(E.14(1c[i],1E||[i,1c[i]])===12)3O;v 1c},H:u(D,O,C,2h,H){7(6.1t(O))O=O.3n(D,[2h]);B 6s=/z-?2h|7P-?8A|1d|58|8B-?28/i;v O&&O.1l==3Q&&C=="30"&&!6s.1n(H)?O+"4S":O},19:{1M:u(D,c){6.K(c.3o(/\\s+/),u(i,Q){7(!6.19.2V(D.19,Q))D.19+=(D.19?" ":"")+Q})},2f:u(D,c){D.19=c?6.2q(D.19.3o(/\\s+/),u(Q){v!6.19.2V(c,Q)}).6t(" "):""},2V:u(t,c){t=t.19||t;c=c.1R(/([\\.\\\\\\+\\*\\?\\[\\^\\]\\$\\(\\)\\{\\}\\=\\!\\<\\>\\|\\:])/g,"\\\\$1");v t&&1p 4v("(^|\\\\s)"+c+"(\\\\s|$)").1n(t)}},4d:u(e,o,f){P(B i 1x o){e.1q["1N"+i]=e.1q[i];e.1q[i]=o[i]}f.14(e,[]);P(B i 1x o)e.1q[i]=e.1q["1N"+i]},1m:u(e,p){7(p=="28"||p=="3V"){B 1N={},46,3P,d=["7d","8C","8D","8E"];6.K(d,u(){1N["8F"+q]=0;1N["8G"+q+"8H"]=0});6.4d(e,1N,u(){7(6.1m(e,"1h")!="1Z"){46=e.8I;3P=e.8J}J{e=6(e.3l(U)).2o(":4j").5l("2Z").4g().1m({4n:"1G",45:"8K",1h:"2D",7I:"0",8M:"0"}).5z(e.11)[0];B 3d=6.1m(e.11,"45");7(3d==""||3d=="4b")e.11.1q.45="6x";46=e.6y;3P=e.6z;7(3d==""||3d=="4b")e.11.1q.45="4b";e.11.33(e)}});v p=="28"?46:3P}v 6.30(e,p)},30:u(D,H,53){B L;7(H=="1d"&&6.W.1j)v 6.1I(D.1q,"1d");7(H=="4h"||H=="2v")H=6.W.1j?"3T":"2v";7(!53&&D.1q[H])L=D.1q[H];J 7(17.44&&17.44.4W){7(H=="2v"||H=="3T")H="4h";H=H.1R(/([A-Z])/g,"-$1").4m();B Q=17.44.4W(D,16);7(Q)L=Q.55(H);J 7(H=="1h")L="1Z";J 6.4d(D,{1h:"2D"},u(){B c=17.44.4W(q,"");L=c&&c.55(H)||""})}J 7(D.51){B 56=H.1R(/\\-(\\w)/g,u(m,c){v c.3K()});L=D.51[H]||D.51[56]}v L},3k:u(a){B r=[];6.K(a,u(i,1r){7(!1r)v;7(1r.1l==3Q)1r=1r.6C();7(1C 1r=="23"){B s=6.35(1r),1V=17.6n("1V"),2i=[];B 2K=!s.18("<1u")&&[1,"<42>","</42>"]||(!s.18("<6D")||!s.18("<20")||!s.18("<6E"))&&[1,"<1P>","</1P>"]||!s.18("<3m")&&[2,"<1P><20>","</20></1P>"]||(!s.18("<6F")||!s.18("<6G"))&&[3,"<1P><20><3m>","</3m></20></1P>"]||[0,"",""];1V.2t=2K[1]+s+2K[2];22(2K[0]--)1V=1V.1b;7(6.W.1j){7(!s.18("<1P")&&s.18("<20")<0)2i=1V.1b&&1V.1b.2I;J 7(2K[1]=="<1P>"&&s.18("<20")<0)2i=1V.2I;P(B n=2i.I-1;n>=0;--n)7(6.1f(2i[n],"20")&&!2i[n].2I.I)2i[n].11.33(2i[n])}1r=[];P(B i=0,l=1V.2I.I;i<l;i++)1r.1g(1V.2I[i])}7(1r.I===0&&!6.1f(1r,"3w"))v;7(1r[0]==T||6.1f(1r,"3w"))r.1g(1r);J r=6.2k(r,1r)});v r},1I:u(D,Y,O){B 2j=6.4B(D)?{}:{"P":"6J","6L":"19","4h":6.W.1j?"3T":"2v",2v:6.W.1j?"3T":"2v",2t:"2t",19:"19",O:"O",2W:"2W",2Z:"2Z",89:"6N",2Y:"2Y"};7(Y=="1d"&&6.W.1j&&O!=T){D.58=1;v D.1D=D.1D.1R(/4i\\([^\\)]*\\)/6O,"")+(O==1?"":"4i(1d="+O*6g+")")}J 7(Y=="1d"&&6.W.1j)v D.1D?4T(D.1D.6P(/4i\\(1d=(.*)\\)/)[1])/6g:1;7(Y=="1d"&&6.W.3h&&O==1)O=0.6R;7(2j[Y]){7(O!=T)D[2j[Y]]=O;v D[2j[Y]]}J 7(O==T&&6.W.1j&&6.1f(D,"3w")&&(Y=="81"||Y=="80"))v D.6T(Y).60;J 7(D.66){7(O!=T)D.6V(Y,O);7(6.W.1j&&/5E|3e/.1n(Y)&&!6.4B(D))v D.36(Y,2);v D.36(Y)}J{Y=Y.1R(/-([a-z])/6W,u(z,b){v b.3K()});7(O!=T)D[Y]=O;v D[Y]}},35:u(t){v t.1R(/^\\s+|\\s+$/g,"")},3M:u(a){B r=[];7(a.1l!=2y)P(B i=0,2R=a.I;i<2R;i++)r.1g(a[i]);J r=a.3N(0);v r},3y:u(b,a){P(B i=0,2R=a.I;i<2R;i++)7(a[i]==b)v i;v-1},2k:u(2u,3H){B r=[].3N.3n(2u,0);P(B i=0,5b=3H.I;i<5b;i++)7(6.3y(3H[i],r)==-1)2u.1g(3H[i]);v 2u},2q:u(1U,E,4k){7(1C E=="23")E=1p 4w("a","i","v "+E);B 1i=[];P(B i=0,2z=1U.I;i<2z;i++)7(!4k&&E(1U[i],i)||4k&&!E(1U[i],i))1i.1g(1U[i]);v 1i},31:u(1U,E){7(1C E=="23")E=1p 4w("a","v "+E);B 1i=[],r=[];P(B i=0,2z=1U.I;i<2z;i++){B 1a=E(1U[i],i);7(1a!==16&&1a!=T){7(1a.1l!=2y)1a=[1a];1i=1i.6Z(1a)}}B r=1i.I?[1i[0]]:[];5f:P(B i=1,5e=1i.I;i<5e;i++){P(B j=0;j<i;j++)7(1i[i]==r[j])5F 5f;r.1g(1i[i])}v r}});1p u(){B b=7L.71.4m();6.W={2N:/5D/.1n(b),3f:/3f/.1n(b),1j:/1j/.1n(b)&&!/3f/.1n(b),3h:/3h/.1n(b)&&!/(72|5D)/.1n(b)};6.7H=!6.W.1j||17.74=="75"};6.K({5u:"a.11",4z:"6.4z(a)",76:"6.2a(a,2,\'2e\')",7D:"6.2a(a,2,\'5s\')",78:"6.2B(a.11.1b,a)",79:"6.2B(a.1b)"},u(i,n){6.E[i]=u(a){B L=6.31(q,n);7(a&&1C a=="23")L=6.3z(a,L);v q.2r(L)}});6.K({5z:"3r",7b:"5i",2X:"5j",7e:"5t"},u(i,n){6.E[i]=u(){B a=1A;v q.K(u(){P(B j=0,2R=a.I;j<2R;j++)6(a[j])[n](q)})}});6.K({5l:u(1Y){6.1I(q,1Y,"");q.7g(1Y)},7h:u(c){6.19.1M(q,c)},7i:u(c){6.19.2f(q,c)},7k:u(c){6.19[6.19.2V(q,c)?"2f":"1M"](q,c)},2f:u(a){7(!a||6.1D(a,[q]).r.I)q.11.33(q)},3u:u(){22(q.1b)q.33(q.1b)}},u(i,n){6.E[i]=u(){v q.K(n,1A)}});6.K(["5q","5n","5p","5v"],u(i,n){6.E[n]=u(1T,E){v q.1D(":"+n+"("+1T+")",E)}});6.K(["28","3V"],u(i,n){6.E[n]=u(h){v h==T?(q.I?6.1m(q[0],n):16):q.1m(n,h.1l==3t?h:h+"4S")}});6.1z({1s:{"":"m[2]==\'*\'||6.1f(a,m[2])","#":"a.36(\'2J\')==m[2]",":":{5n:"i<m[3]-0",5p:"i>m[3]-0",2a:"m[3]-0==i",5q:"m[3]-0==i",2u:"i==0",2T:"i==r.I-1",5R:"i%2==0",5S:"i%2","2a-3s":"6.2a(a.11.1b,m[3],\'2e\',a)==a","2u-3s":"6.2a(a.11.1b,1,\'2e\')==a","2T-3s":"6.2a(a.11.7n,1,\'5s\')==a","7p-3s":"6.2B(a.11.1b).I==1",5u:"a.1b",3u:"!a.1b",5v:"6.E.2L.14([a]).18(m[3])>=0",3i:\'a.C!="1G"&&6.1m(a,"1h")!="1Z"&&6.1m(a,"4n")!="1G"\',1G:\'a.C=="1G"||6.1m(a,"1h")=="1Z"||6.1m(a,"4n")=="1G"\',7v:"!a.2W",2W:"a.2W",2Z:"a.2Z",2Y:"a.2Y||6.1I(a,\'2Y\')",2L:"a.C==\'2L\'",4j:"a.C==\'4j\'",5x:"a.C==\'5x\'",4G:"a.C==\'4G\'",5y:"a.C==\'5y\'",4R:"a.C==\'4R\'",5A:"a.C==\'5A\'",5B:"a.C==\'5B\'",3x:\'a.C=="3x"||6.1f(a,"3x")\',5C:"/5C|42|7A|3x/i.1n(a.1f)"},".":"6.19.2V(a,m[2])","@":{"=":"z==m[4]","!=":"z!=m[4]","^=":"z&&!z.18(m[4])","$=":"z&&z.2U(z.I - m[4].I,m[4].I)==m[4]","*=":"z&&z.18(m[4])>=0","":"z",4u:u(m){v["",m[1],m[3],m[2],m[5]]},5P:"z=a[m[3]];7(!z||/5E|3e/.1n(m[3]))z=6.1I(a,m[3]);"},"[":"6.2o(m[2],a).I"},5M:[/^\\[ *(@)([a-2m-3C-]*) *([!*$^=]*) *(\'?"?)(.*?)\\4 *\\]/i,/^(\\[)\\s*(.*?(\\[.*?\\])?[^[]*?)\\s*\\]/,/^(:)([a-2m-3C-]*)\\("?\'?(.*?(\\(.*?\\))?[^(]*?)"?\'?\\)/i,/^([:.#]*)([a-2m-3C*-]*)/i],1Q:[/^(\\/?\\.\\.)/,"a.11",/^(>|\\/)/,"6.2B(a.1b)",/^(\\+)/,"6.2a(a,2,\'2e\')",/^(~)/,u(a){B s=6.2B(a.11.1b);v s.3N(6.3y(a,s)+1)}],3z:u(1s,1U,2g){B 1N,Q=[];22(1s&&1s!=1N){1N=1s;B f=6.1D(1s,1U,2g);1s=f.t.1R(/^\\s*,\\s*/,"");Q=2g?1U=f.r:6.2k(Q,f.r)}v Q},2o:u(t,1B){7(1C t!="23")v[t];7(1B&&!1B.24)1B=16;1B=1B||17;7(!t.18("//")){1B=1B.4H;t=t.2U(2,t.I)}J 7(!t.18("/")){1B=1B.4H;t=t.2U(1,t.I);7(t.18("/")>=1)t=t.2U(t.18("/"),t.I)}B L=[1B],2c=[],2T=16;22(t&&2T!=t){B r=[];2T=t;t=6.35(t).1R(/^\\/\\//i,"");B 3B=12;B 1J=/^[\\/>]\\s*([a-2m-9*-]+)/i;B m=1J.2Q(t);7(m){6.K(L,u(){P(B c=q.1b;c;c=c.2e)7(c.24==1&&(6.1f(c,m[1])||m[1]=="*"))r.1g(c)});L=r;t=t.1R(1J,"");7(t.18(" ")==0)5F;3B=U}J{P(B i=0;i<6.1Q.I;i+=2){B 1J=6.1Q[i];B m=1J.2Q(t);7(m){r=L=6.31(L,6.1t(6.1Q[i+1])?6.1Q[i+1]:u(a){v 40(6.1Q[i+1])});t=6.35(t.1R(1J,""));3B=U;3O}}}7(t&&!3B){7(!t.18(",")){7(L[0]==1B)L.4L();6.2k(2c,L);r=L=[1B];t=" "+t.2U(1,t.I)}J{B 34=/^([a-2m-3C-]+)(#)([a-2m-9\\\\*2S-]*)/i;B m=34.2Q(t);7(m){m=[0,m[2],m[3],m[1]]}J{34=/^([#.]?)([a-2m-9\\\\*2S-]*)/i;m=34.2Q(t)}7(m[1]=="#"&&L[L.I-1].4X){B 2l=L[L.I-1].4X(m[2]);7(6.W.1j&&2l&&2l.2J!=m[2])2l=6(\'[@2J="\'+m[2]+\'"]\',L[L.I-1])[0];L=r=2l&&(!m[3]||6.1f(2l,m[3]))?[2l]:[]}J{7(m[1]==".")B 4r=1p 4v("(^|\\\\s)"+m[2]+"(\\\\s|$)");6.K(L,u(){B 3E=m[1]!=""||m[0]==""?"*":m[2];7(6.1f(q,"7J")&&3E=="*")3E="3g";6.2k(r,m[1]!=""&&L.I!=1?6.4x(q,[],m[1],m[2],4r):q.5J(3E))});7(m[1]=="."&&L.I==1)r=6.2q(r,u(e){v 4r.1n(e.19)});7(m[1]=="#"&&L.I==1){B 5K=r;r=[];6.K(5K,u(){7(q.36("2J")==m[2]){r=[q];v 12}})}L=r}t=t.1R(34,"")}}7(t){B 1a=6.1D(t,r);L=r=1a.r;t=6.35(1a.t)}}7(L&&L[0]==1B)L.4L();6.2k(2c,L);v 2c},1D:u(t,r,2g){22(t&&/^[a-z[({<*:.#]/i.1n(t)){B p=6.5M,m;6.K(p,u(i,1J){m=1J.2Q(t);7(m){t=t.7M(m[0].I);7(6.1s[m[1]].4u)m=6.1s[m[1]].4u(m);v 12}});7(m[1]==":"&&m[2]=="2g")r=6.1D(m[3],r,U).r;J 7(m[1]=="."){B 1J=1p 4v("(^|\\\\s)"+m[2]+"(\\\\s|$)");r=6.2q(r,u(e){v 1J.1n(e.19||"")},2g)}J{B f=6.1s[m[1]];7(1C f!="23")f=6.1s[m[1]][m[2]];40("f = u(a,i){"+(6.1s[m[1]].5P||"")+"v "+f+"}");r=6.2q(r,f,2g)}}v{r:r,t:t}},4x:u(o,r,1Q,Y,1J){P(B s=o.1b;s;s=s.2e)7(s.24==1){B 1M=U;7(1Q==".")1M=s.19&&1J.1n(s.19);J 7(1Q=="#")1M=s.36("2J")==Y;7(1M)r.1g(s);7(1Q=="#"&&r.I)3O;7(s.1b)6.4x(s,r,1Q,Y,1J)}v r},4z:u(D){B 4A=[];B Q=D.11;22(Q&&Q!=17){4A.1g(Q);Q=Q.11}v 4A},2a:u(Q,1i,3Z,D){1i=1i||1;B 1T=0;P(;Q;Q=Q[3Z]){7(Q.24==1)1T++;7(1T==1i||1i=="5R"&&1T%2==0&&1T>1&&Q==D||1i=="5S"&&1T%2==1&&Q==D)v Q}},2B:u(n,D){B r=[];P(;n;n=n.2e){7(n.24==1&&(!D||n!=D))r.1g(n)}v r}});6.G={1M:u(S,C,1o,F){7(6.W.1j&&S.3L!=T)S=1w;7(F)1o.F=F;7(!1o.2A)1o.2A=q.2A++;7(!S.$1H)S.$1H={};B 38=S.$1H[C];7(!38){38=S.$1H[C]={};7(S["39"+C])38[0]=S["39"+C]}38[1o.2A]=1o;S["39"+C]=q.5Y;7(!q.1k[C])q.1k[C]=[];q.1k[C].1g(S)},2A:1,1k:{},2f:u(S,C,1o){7(S.$1H){B i,j,k;7(C&&C.C){1o=C.1o;C=C.C}7(C&&S.$1H[C])7(1o)5U S.$1H[C][1o.2A];J P(i 1x S.$1H[C])5U S.$1H[C][i];J P(j 1x S.$1H)q.2f(S,j);P(k 1x S.$1H[C])7(k){k=U;3O}7(!k)S["39"+C]=16}},1S:u(C,F,S){F=6.3M(F||[]);7(!S)6.K(q.1k[C]||[],u(){6.G.1S(C,F,q)});J{B 1o=S["39"+C],1a,E=6.1t(S[C]);7(1o){F.61(q.2j({C:C,1O:S}));7((1a=1o.14(S,F))!==12)q.4F=U}7(E&&1a!==12)S[C]();q.4F=12}},5Y:u(G){7(1C 6=="T"||6.G.4F)v;G=6.G.2j(G||1w.G||{});B 3R;B c=q.$1H[G.C];B 1E=[].3N.3n(1A,1);1E.61(G);P(B j 1x c){1E[0].1o=c[j];1E[0].F=c[j].F;7(c[j].14(q,1E)===12){G.2n();G.2H();3R=12}}7(6.W.1j)G.1O=G.2n=G.2H=G.1o=G.F=16;v 3R},2j:u(G){7(!G.1O&&G.63)G.1O=G.63;7(G.65==T&&G.67!=T){B e=17.4H,b=17.64;G.65=G.67+(e.68||b.68);G.7Y=G.7Z+(e.6c||b.6c)}7(6.W.2N&&G.1O.24==3){B 3a=G;G=6.1z({},3a);G.1O=3a.1O.11;G.2n=u(){v 3a.2n()};G.2H=u(){v 3a.2H()}}7(!G.2n)G.2n=u(){q.3R=12};7(!G.2H)G.2H=u(){q.82=U};v G}};6.E.1z({3U:u(C,F,E){v q.K(u(){6.G.1M(q,C,E||F,F)})},6u:u(C,F,E){v q.K(u(){6.G.1M(q,C,u(G){6(q).6f(G);v(E||F).14(q,1A)},F)})},6f:u(C,E){v q.K(u(){6.G.2f(q,C,E)})},1S:u(C,F){v q.K(u(){6.G.1S(C,F,q)})},3X:u(){B a=1A;v q.6j(u(e){q.4M=q.4M==0?1:0;e.2n();v a[q.4M].14(q,[e])||12})},83:u(f,g){u 4O(e){B p=(e.C=="41"?e.84:e.85)||e.86;22(p&&p!=q)2G{p=p.11}2w(e){p=q};7(p==q)v 12;v(e.C=="41"?f:g).14(q,[e])}v q.41(4O).6k(4O)},27:u(f){7(6.3W)f.14(17,[6]);J{6.3c.1g(u(){v f.14(q,[6])})}v q}});6.1z({3W:12,3c:[],27:u(){7(!6.3W){6.3W=U;7(6.3c){6.K(6.3c,u(){q.14(17)});6.3c=16}7(6.W.3h||6.W.3f)17.87("6o",6.27,12)}}});1p u(){6.K(("88,8a,2O,8b,8d,52,6j,8e,"+"8f,8g,8h,41,6k,8j,42,"+"4R,8k,8l,8m,2C").3o(","),u(i,o){6.E[o]=u(f){v f?q.3U(o,f):q.1S(o)}});7(6.W.3h||6.W.3f)17.8n("6o",6.27,12);J 7(6.W.1j){17.8o("<8r"+"8s 2J=62 8u=U "+"3e=//:><\\/2d>");B 2d=17.4X("62");7(2d)2d.37=u(){7(q.3D!="1X")v;q.11.33(q);6.27()};2d=16}J 7(6.W.2N)6.50=3L(u(){7(17.3D=="8y"||17.3D=="1X"){4p(6.50);6.50=16;6.27()}},10);6.G.1M(1w,"2O",6.27)};7(6.W.1j)6(1w).6u("52",u(){B 1k=6.G.1k;P(B C 1x 1k){B 4Z=1k[C],i=4Z.I;7(i&&C!=\'52\')6w 6.G.2f(4Z[i-1],C);22(--i)}});6.E.1z({6A:u(V,21,M){q.2O(V,21,M,1)},2O:u(V,21,M,1W){7(6.1t(V))v q.3U("2O",V);M=M||u(){};B C="5d";7(21)7(6.1t(21)){M=21;21=16}J{21=6.3g(21);C="5V"}B 4e=q;6.3v({V:V,C:C,F:21,1W:1W,1X:u(2P,15){7(15=="2M"||!1W&&15=="5L")4e.1I("2t",2P.3G).4V().K(M,[2P.3G,15,2P]);J M.14(4e,[2P.3G,15,2P])}});v q},6B:u(){v 6.3g(q)},4V:u(){v q.2o("2d").K(u(){7(q.3e)6.59(q.3e);J 6.4a(q.2L||q.6H||q.2t||"")}).4g()}});7(!1w.3p)3p=u(){v 1p 6I("6K.6M")};6.K("5m,5Q,5O,5W,5N,5H".3o(","),u(i,o){6.E[o]=u(f){v q.3U(o,f)}});6.1z({2b:u(V,F,M,C,1W){7(6.1t(F)){M=F;F=16}v 6.3v({V:V,F:F,2M:M,4t:C,1W:1W})},6Q:u(V,F,M,C){v 6.2b(V,F,M,C,1)},59:u(V,M){v 6.2b(V,16,M,"2d")},6S:u(V,F,M){v 6.2b(V,F,M,"6m")},6U:u(V,F,M,C){7(6.1t(F)){M=F;F={}}v 6.3v({C:"5V",V:V,F:F,2M:M,4t:C})},6X:u(29){6.3q.29=29},6Y:u(5c){6.1z(6.3q,5c)},3q:{1k:U,C:"5d",29:0,5r:"70/x-73-3w-77",5h:U,48:U,F:16},3S:{},3v:u(s){s=6.1z({},6.3q,s);7(s.F){7(s.5h&&1C s.F!="23")s.F=6.3g(s.F);7(s.C.4m()=="2b"){s.V+=((s.V.18("?")>-1)?"&":"?")+s.F;s.F=16}}7(s.1k&&!6.4E++)6.G.1S("5m");B 4y=12;B N=1p 3p();N.7j(s.C,s.V,s.48);7(s.F)N.3A("7l-7m",s.5r);7(s.1W)N.3A("7o-4K-7q",6.3S[s.V]||"7s, 7t 7w 7x 4o:4o:4o 7z");N.3A("X-7B-7C","3p");7(N.7E)N.3A("7F","7G");7(s.5G)s.5G(N);7(s.1k)6.G.1S("5H",[N,s]);B 37=u(4s){7(N&&(N.3D==4||4s=="29")){4y=U;7(3I){4p(3I);3I=16}B 15;2G{15=6.5Z(N)&&4s!="29"?s.1W&&6.69(N,s.V)?"5L":"2M":"2C";7(15!="2C"){B 3F;2G{3F=N.4P("6b-4K")}2w(e){}7(s.1W&&3F)6.3S[s.V]=3F;B F=6.6i(N,s.4t);7(s.2M)s.2M(F,15);7(s.1k)6.G.1S("5N",[N,s])}J 6.3J(s,N,15)}2w(e){15="2C";6.3J(s,N,15,e)}7(s.1k)6.G.1S("5O",[N,s]);7(s.1k&&!--6.4E)6.G.1S("5Q");7(s.1X)s.1X(N,15);7(s.48)N=16}};B 3I=3L(37,13);7(s.29>0)57(u(){7(N){N.7N();7(!4y)37("29")}},s.29);2G{N.7Q(s.F)}2w(e){6.3J(s,N,16,e)}7(!s.48)37();v N},3J:u(s,N,15,e){7(s.2C)s.2C(N,15,e);7(s.1k)6.G.1S("5W",[N,s,e])},4E:0,5Z:u(r){2G{v!r.15&&7V.7W=="4G:"||(r.15>=5X&&r.15<7X)||r.15==6d||6.W.2N&&r.15==T}2w(e){}v 12},69:u(N,V){2G{B 6e=N.4P("6b-4K");v N.15==6d||6e==6.3S[V]||6.W.2N&&N.15==T}2w(e){}v 12},6i:u(r,C){B 4Q=r.4P("8c-C");B F=!C&&4Q&&4Q.18("N")>=0;F=C=="N"||F?r.8i:r.3G;7(C=="2d")6.4a(F);7(C=="6m")40("F = "+F);7(C=="4U")6("<1V>").4U(F).4V();v F},3g:u(a){B s=[];7(a.1l==2y||a.3Y)6.K(a,u(){s.1g(2x(q.Y)+"="+2x(q.O))});J P(B j 1x a)7(a[j]&&a[j].1l==2y)6.K(a[j],u(){s.1g(2x(j)+"="+2x(q))});J s.1g(2x(j)+"="+2x(a[j]));v s.6t("&")},4a:u(F){7(1w.54)1w.54(F);J 7(6.W.2N)1w.57(F,0);J 40.3n(1w,F)}});6.E.1z({1L:u(R,M){B 1G=q.1D(":1G");R?1G.26({28:"1L",3V:"1L",1d:"1L"},R,M):1G.K(u(){q.1q.1h=q.2E?q.2E:"";7(6.1m(q,"1h")=="1Z")q.1q.1h="2D"});v q},1K:u(R,M){B 3i=q.1D(":3i");R?3i.26({28:"1K",3V:"1K",1d:"1K"},R,M):3i.K(u(){q.2E=q.2E||6.1m(q,"1h");7(q.2E=="1Z")q.2E="2D";q.1q.1h="1Z"});v q},5g:6.E.3X,3X:u(E,4I){B 1E=1A;v 6.1t(E)&&6.1t(4I)?q.5g(E,4I):q.K(u(){6(q)[6(q).4l(":1G")?"1L":"1K"].14(6(q),1E)})},7a:u(R,M){v q.26({28:"1L"},R,M)},7c:u(R,M){v q.26({28:"1K"},R,M)},7f:u(R,M){v q.K(u(){B 5k=6(q).4l(":1G")?"1L":"1K";6(q).26({28:5k},R,M)})},7r:u(R,M){v q.26({1d:"1L"},R,M)},7u:u(R,M){v q.26({1d:"1K"},R,M)},7y:u(R,43,M){v q.26({1d:43},R,M)},26:u(H,R,1v,M){v q.1F(u(){q.2F=6.1z({},H);B 1u=6.R(R,1v,M);P(B p 1x H){B e=1p 6.3b(q,1u,p);7(H[p].1l==3Q)e.2s(e.Q(),H[p]);J e[H[p]](H)}})},1F:u(C,E){7(!E){E=C;C="3b"}v q.K(u(){7(!q.1F)q.1F={};7(!q.1F[C])q.1F[C]=[];q.1F[C].1g(E);7(q.1F[C].I==1)E.14(q)})}});6.1z({R:u(R,1v,E){B 1u=R&&R.1l==7K?R:{1X:E||!E&&1v||6.1t(R)&&R,25:R,1v:E&&1v||1v&&1v.1l!=4w&&1v};1u.25=(1u.25&&1u.25.1l==3Q?1u.25:{7R:7S,7T:5X}[1u.25])||7U;1u.1N=1u.1X;1u.1X=u(){6.6a(q,"3b");7(6.1t(1u.1N))1u.1N.14(q)};v 1u},1v:{},1F:{},6a:u(D,C){C=C||"3b";7(D.1F&&D.1F[C]){D.1F[C].4L();B f=D.1F[C][0];7(f)f.14(D)}},3b:u(D,1e,H){B z=q;B y=D.1q;B 4D=6.1m(D,"1h");y.5T="1G";z.a=u(){7(1e.49)1e.49.14(D,[z.2p]);7(H=="1d")6.1I(y,"1d",z.2p);J 7(6l(z.2p))y[H]=6l(z.2p)+"4S";y.1h="2D"};z.6v=u(){v 4T(6.1m(D,H))};z.Q=u(){B r=4T(6.30(D,H));v r&&r>-8z?r:z.6v()};z.2s=u(4f,43){z.4J=(1p 5o()).5w();z.2p=4f;z.a();z.4q=3L(u(){z.49(4f,43)},13)};z.1L=u(){7(!D.1y)D.1y={};D.1y[H]=q.Q();1e.1L=U;z.2s(0,D.1y[H]);7(H!="1d")y[H]="5a"};z.1K=u(){7(!D.1y)D.1y={};D.1y[H]=q.Q();1e.1K=U;z.2s(D.1y[H],0)};z.3X=u(){7(!D.1y)D.1y={};D.1y[H]=q.Q();7(4D=="1Z"){1e.1L=U;7(H!="1d")y[H]="5a";z.2s(0,D.1y[H])}J{1e.1K=U;z.2s(D.1y[H],0)}};z.49=u(32,47){B t=(1p 5o()).5w();7(t>1e.25+z.4J){4p(z.4q);z.4q=16;z.2p=47;z.a();7(D.2F)D.2F[H]=U;B 2c=U;P(B i 1x D.2F)7(D.2F[i]!==U)2c=12;7(2c){y.5T="";y.1h=4D;7(6.1m(D,"1h")=="1Z")y.1h="2D";7(1e.1K)y.1h="1Z";7(1e.1K||1e.1L)P(B p 1x D.2F)7(p=="1d")6.1I(y,p,D.1y[p]);J y[p]=""}7(2c&&6.1t(1e.1X))1e.1X.14(D)}J{B n=t-q.4J;B p=n/1e.25;z.2p=1e.1v&&6.1v[1e.1v]?6.1v[1e.1v](p,n,32,(47-32),1e.25):((-6h.7O(p*6h.8L)/2)+0.5)*(47-32)+32;z.a()}}}})}',62,545,'||||||jQuery|if|||||||||||||||||||this||||function|return||||||var|type|elem|fn|data|event|prop|length|else|each|ret|callback|xml|value|for|cur|speed|element|undefined|true|url|browser||name|||parentNode|false||apply|status|null|document|indexOf|className|val|firstChild|obj|opacity|options|nodeName|push|display|result|msie|global|constructor|css|test|handler|new|style|arg|expr|isFunction|opt|easing|window|in|orig|extend|arguments|context|typeof|filter|args|queue|hidden|events|attr|re|hide|show|add|old|target|table|token|replace|trigger|num|elems|div|ifModified|complete|key|none|tbody|params|while|string|nodeType|duration|animate|ready|height|timeout|nth|get|done|script|nextSibling|remove|not|index|tb|fix|merge|oid|z0|preventDefault|find|now|grep|pushStack|custom|innerHTML|first|cssFloat|catch|encodeURIComponent|Array|el|guid|sibling|error|block|oldblock|curAnim|try|stopPropagation|childNodes|id|wrap|text|success|safari|load|res|exec|al|_|last|substr|has|disabled|insertBefore|selected|checked|curCSS|map|firstNum|removeChild|re2|trim|getAttribute|onreadystatechange|handlers|on|originalEvent|fx|readyList|parPos|src|opera|param|mozilla|visible|domManip|clean|cloneNode|tr|call|split|XMLHttpRequest|ajaxSettings|append|child|String|empty|ajax|form|button|inArray|multiFilter|setRequestHeader|foundToken|9_|readyState|tag|modRes|responseText|second|ival|handleError|toUpperCase|setInterval|makeArray|slice|break|oWidth|Number|returnValue|lastModified|styleFloat|bind|width|isReady|toggle|jquery|dir|eval|mouseover|select|to|defaultView|position|oHeight|lastNum|async|step|globalEval|static|pos|swap|self|from|end|float|alpha|radio|inv|is|toLowerCase|visibility|00|clearInterval|timer|rec|isTimeout|dataType|_resort|RegExp|Function|getAll|requestDone|parents|matched|isXMLDoc|appendChild|oldDisplay|active|triggered|file|documentElement|fn2|startTime|Modified|shift|lastToggle|deep|handleHover|getResponseHeader|ct|submit|px|parseFloat|html|evalScripts|getComputedStyle|getElementById|clone|els|safariTimer|currentStyle|unload|force|execScript|getPropertyValue|newProp|setTimeout|zoom|getScript|1px|sl|settings|GET|rl|check|_toggle|processData|prepend|before|state|removeAttr|ajaxStart|lt|Date|gt|eq|contentType|previousSibling|after|parent|contains|getTime|checkbox|password|appendTo|image|reset|input|webkit|href|continue|beforeSend|ajaxSend|ownerDocument|getElementsByTagName|tmp|notmodified|parse|ajaxSuccess|ajaxComplete|_prefix|ajaxStop|even|odd|overflow|delete|POST|ajaxError|200|handle|httpSuccess|nodeValue|unshift|__ie_init|srcElement|body|pageX|tagName|clientX|scrollLeft|httpNotModified|dequeue|Last|scrollTop|304|xmlRes|unbind|100|Math|httpData|click|mouseout|parseInt|json|createElement|DOMContentLoaded|prevObject|ol|setArray|exclude|join|one|max|do|relative|clientHeight|clientWidth|loadIfModified|serialize|toString|thead|tfoot|td|th|textContent|ActiveXObject|htmlFor|Microsoft|class|XMLHTTP|readOnly|gi|match|getIfModified|9999|getJSON|getAttributeNode|post|setAttribute|ig|ajaxTimeout|ajaxSetup|concat|application|userAgent|compatible|www|compatMode|CSS1Compat|next|urlencoded|siblings|children|slideDown|prependTo|slideUp|Top|insertAfter|slideToggle|removeAttribute|addClass|removeClass|open|toggleClass|Content|Type|lastChild|If|only|Since|fadeIn|Thu|01|fadeOut|enabled|Jan|1970|fadeTo|GMT|textarea|Requested|With|prev|overrideMimeType|Connection|close|boxModel|right|object|Object|navigator|substring|abort|cos|font|send|slow|600|fast|400|location|protocol|300|pageY|clientY|method|action|cancelBubble|hover|fromElement|toElement|relatedTarget|removeEventListener|blur|readonly|focus|resize|content|scroll|dblclick|mousedown|mouseup|mousemove|responseXML|change|keydown|keypress|keyup|addEventListener|write|prototype|size|scr|ipt|createTextNode|defer|FORM|reverse|noConflict|loaded|10000|weight|line|Bottom|Right|Left|padding|border|Width|offsetHeight|offsetWidth|absolute|PI|left'.split('|'),0,{})) |
0 | body { | |
1 | font-family: 'Arial', sans-serif; | |
2 | font-size: 15px; | |
3 | margin: 0; | |
4 | padding: 1.3em; | |
5 | background-color: #07131d; | |
6 | } | |
7 | ||
8 | * { | |
9 | margin: 0; | |
10 | padding: 0; | |
11 | } | |
12 | ||
13 | #footer { | |
14 | margin: 1em; | |
15 | font-size: 13px; | |
16 | letter-spacing: 0.1em; | |
17 | color: #eee; | |
18 | text-align: right; | |
19 | } | |
20 | ||
21 | #footer .arthur { | |
22 | font-weight: bold; | |
23 | } | |
24 | ||
25 | .traceback_wrapper { | |
26 | margin: 1em; | |
27 | border: 1px solid #1b3042; | |
28 | background-color: #F6F6F6; | |
29 | } | |
30 | ||
31 | h1 { | |
32 | background-color: #1b3042; | |
33 | font-size: 24px; | |
34 | color: #FFFFFF; | |
35 | padding: 0.3em; | |
36 | margin: 0 0 0.2em 0; | |
37 | } | |
38 | ||
39 | h2 { | |
40 | background-color: #3F7CA4; | |
41 | font-size: 16px; | |
42 | color: #FFFFFF; | |
43 | padding: 0.3em; | |
44 | margin: 1em 0 0.2em 0; | |
45 | clear: both; | |
46 | } | |
47 | ||
48 | h2.tb { | |
49 | cursor: pointer; | |
50 | } | |
51 | ||
52 | h2.tb span { | |
53 | display: none; | |
54 | color: #d2f2f3; | |
55 | font-weight: normal; | |
56 | } | |
57 | ||
58 | h2.tb:hover span { | |
59 | display: inline; | |
60 | } | |
61 | ||
62 | h3 { | |
63 | font-size: 14px; | |
64 | padding: 0; | |
65 | } | |
66 | ||
67 | h3.fn { | |
68 | margin-top: 0.8em; | |
69 | clear: both; | |
70 | font-weight: normal; | |
71 | } | |
72 | ||
73 | h3.fn em { | |
74 | font-style: normal; | |
75 | font-weight: bold; | |
76 | } | |
77 | ||
78 | h3.fn tt { | |
79 | color: #444; | |
80 | font-size: 0.9em; | |
81 | font-family: 'Arial', sans-serif; | |
82 | } | |
83 | ||
84 | a { | |
85 | color: #333; | |
86 | } | |
87 | ||
88 | a.eval, a.locals { | |
89 | display: block; | |
90 | float: right; | |
91 | margin: -12px 0 0 8px; | |
92 | font-size: 12px; | |
93 | color: #333; | |
94 | text-decoration: none; | |
95 | } | |
96 | ||
97 | a.eval:hover, a.locals:hover { | |
98 | color: black; | |
99 | background-color: #ccc; | |
100 | } | |
101 | ||
102 | p.text { | |
103 | padding: 0.4em 1em 0.4em 1em; | |
104 | } | |
105 | ||
106 | p.errormsg { | |
107 | padding: 0.1em 0.5em 0.1em 0.5em; | |
108 | font-size: 16px; | |
109 | } | |
110 | ||
111 | p.errorline { | |
112 | padding: 0.1em 0.5em 0.1em 2em; | |
113 | font-size: 15px; | |
114 | } | |
115 | ||
116 | div.frame { | |
117 | margin: 0 2em 0 1em; | |
118 | } | |
119 | ||
120 | table.code { | |
121 | margin: 0.4em 0 0 0.5em; | |
122 | background-color: #eee; | |
123 | width: 100%; | |
124 | font-family: monospace; | |
125 | font-size: 13px; | |
126 | border: 1px solid #ccc; | |
127 | border-collapse: collapse; | |
128 | cursor: pointer; | |
129 | } | |
130 | ||
131 | table.code td.lineno { | |
132 | width: 42px; | |
133 | text-align: right; | |
134 | padding: 0 5px 0 0; | |
135 | color: #444; | |
136 | background-color: #ddd; | |
137 | font-weight: bold; | |
138 | border-right: 1px solid #888; | |
139 | } | |
140 | ||
141 | table.code td.code { | |
142 | background-color: #f6f6f6; | |
143 | padding: 1px 0 1px 5px; | |
144 | white-space: pre; | |
145 | } | |
146 | ||
147 | table.code tr.cur td.code { | |
148 | background-color: #fff; | |
149 | border-top: 1px solid #ccc; | |
150 | border-bottom: 1px solid #ccc; | |
151 | white-space: pre; | |
152 | } | |
153 | ||
154 | table.code tr.cur td.lineno { | |
155 | background-color: #bbb; | |
156 | border: 1px solid #999; | |
157 | } | |
158 | ||
159 | pre.plain { | |
160 | margin: 0.5em 1em 1em 1em; | |
161 | padding: 0.5em; | |
162 | border:1px solid #999999; | |
163 | background-color: #FFFFFF; | |
164 | font-family: monospace; | |
165 | font-size: 13px; | |
166 | } | |
167 | ||
168 | form.exec_code { | |
169 | display: block; | |
170 | width: 100%; | |
171 | margin: 0.3em 1em 0.3em 1em; | |
172 | } | |
173 | ||
174 | form.exec_code input.input { | |
175 | width: 100%; | |
176 | height: 1.5em; | |
177 | border: 1px solid #999999; | |
178 | font-family: monospace; | |
179 | font-size: 13px; | |
180 | } | |
181 | ||
182 | form.exec_code pre.output, | |
183 | table.vars td.value { | |
184 | font-family: monospace; | |
185 | font-size: 13px; | |
186 | white-space: pre-wrap; /* css-3 should we be so lucky... */ | |
187 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ | |
188 | white-space: -pre-wrap; /* Opera 4-6 ?? */ | |
189 | white-space: -o-pre-wrap; /* Opera 7 ?? */ | |
190 | word-wrap: break-word; /* Internet Explorer 5.5+ */ | |
191 | _white-space: pre; /* IE only hack to re-specify in addition to word-wrap */ | |
192 | } | |
193 | ||
194 | pre.output { | |
195 | background-color: #222; | |
196 | color: #fff; | |
197 | padding: 4px; | |
198 | border: 1px solid #666; | |
199 | max-height: 350px; | |
200 | overflow: auto; | |
201 | } | |
202 | ||
203 | table.vars { | |
204 | margin: 0.3em 1.5em 0.3em 1.5em; | |
205 | border-collapse: collapse; | |
206 | font-size: 13px; | |
207 | width: 99%; | |
208 | } | |
209 | ||
210 | table.vars td { | |
211 | font-family: 'Bitstream Vera Sans Mono', 'Courier New', monospace; | |
212 | padding: 0.3em; | |
213 | border: 1px solid #ddd; | |
214 | vertical-align: top; | |
215 | background-color: white; | |
216 | } | |
217 | ||
218 | table.vars .name { | |
219 | font-style: italic; | |
220 | } | |
221 | ||
222 | table.vars .value { | |
223 | color: #555; | |
224 | } | |
225 | ||
226 | table.vars th { | |
227 | padding: 0.2em; | |
228 | border: 1px solid #ddd; | |
229 | background-color: #f2f2f2; | |
230 | text-align: left; | |
231 | } | |
232 | ||
233 | #plain { | |
234 | display: none; | |
235 | } | |
236 | ||
237 | dl dt { | |
238 | padding: 0.2em 0 0.2em 1em; | |
239 | font-weight: bold; | |
240 | cursor: pointer; | |
241 | background-color: #ddd; | |
242 | border-top: 1px solid #ccc; | |
243 | } | |
244 | ||
245 | dl dt:hover { | |
246 | background-color: #bbb; | |
247 | color: white; | |
248 | } | |
249 | ||
250 | dl dd { | |
251 | padding: 0 0 0 2em; | |
252 | background-color: #eee; | |
253 | } | |
254 | ||
255 | span.p-kw { | |
256 | font-weight: bold; | |
257 | color: #1d6170; | |
258 | } | |
259 | ||
260 | span.p-cmt { | |
261 | color: #7bbc2e; | |
262 | } | |
263 | ||
264 | span.p-str { | |
265 | color: #ab7219; | |
266 | } | |
267 | ||
268 | span.p-num { | |
269 | color: #3f7719; | |
270 | font-weight: bold; | |
271 | } | |
272 | ||
273 | span.p-op { | |
274 | color: black; | |
275 | } |
0 | <table class="vars"> | |
1 | <% if type == 'empty' %> | |
2 | <tr><th>no data given</th></tr> | |
3 | <% elif type == 'simple' %> | |
4 | <tr><td class="value">$escape(value)</td></tr> | |
5 | <% elif type == 'dict' %> | |
6 | <tr><th>Name</th><th>Value</th></tr> | |
7 | <% for key, item in value %> | |
8 | <tr><td class="name">$escape(key)</td><td class="value">$escape(item)</td></tr> | |
9 | <% endfor %> | |
10 | <% elif type == 'list' %> | |
11 | <% for item in value %> | |
12 | <tr><td class="value">$escape(item)</td></tr> | |
13 | <% endfor %> | |
14 | <% endif %> | |
15 | </table> |
210 | 210 | variables = tb.tb_frame.f_locals |
211 | 211 | |
212 | 212 | # get filename |
213 | fn = tb.tb_frame.f_globals.get('__file__') | |
214 | if not fn: | |
215 | fn = os.path.realpath(inspect.getsourcefile(tb) or | |
216 | inspect.getfile(tb)) | |
217 | if fn[-4:] in ('.pyc', '.pyo'): | |
218 | fn = fn[:-1] | |
213 | if simple: | |
214 | fn = tb.tb_frame.f_code.co_filename | |
215 | else: | |
216 | fn = tb.tb_frame.f_globals.get('__file__') | |
217 | if not fn: | |
218 | fn = os.path.realpath(inspect.getsourcefile(tb) or | |
219 | inspect.getfile(tb)) | |
220 | if fn[-4:] in ('.pyc', '.pyo'): | |
221 | fn = fn[:-1] | |
219 | 222 | |
220 | 223 | # module name |
221 | 224 | modname = tb.tb_frame.f_globals.get('__name__') |
228 | 231 | pre_context, post_context = [], [] |
229 | 232 | context_line = raw_context_line = context_lineno = None |
230 | 233 | try: |
231 | if not loader is None: | |
232 | source = loader.get_source(modname) | |
234 | if not loader is None and hasattr(loader, 'get_source'): | |
235 | source = loader.get_source(modname) or '' | |
233 | 236 | else: |
234 | 237 | source = file(fn).read() |
235 | 238 | except: |
2 | 2 | werkzeug.exceptions |
3 | 3 | ~~~~~~~~~~~~~~~~~~~ |
4 | 4 | |
5 | This module implements exceptions for the most important HTTP status | |
6 | codes. Each exception is a small WSGI application you can return | |
7 | in views. Simple usage example would look like this:: | |
8 | ||
5 | This module implements a number of Python exceptions you can raise from | |
6 | within your views to trigger a standard non 200 response. | |
7 | ||
8 | ||
9 | Usage Example | |
10 | ------------- | |
11 | ||
12 | :: | |
13 | ||
14 | from werkzeug import BaseRequest, responder | |
9 | 15 | from werkzeug.exceptions import HTTPException, NotFound |
10 | 16 | |
17 | def view(request): | |
18 | raise NotFound() | |
19 | ||
20 | @responder | |
11 | 21 | def application(environ, start_response): |
12 | request = Request(environ) | |
22 | request = BaseRequest(environ) | |
13 | 23 | try: |
14 | response = view_func(request) | |
15 | except NotFound: | |
16 | response = get_not_found_response(request) | |
24 | return view(request) | |
17 | 25 | except HTTPException, e: |
18 | response = e | |
19 | return response(environ, start_response) | |
20 | ||
21 | This module does only implement error classes for status codes from | |
22 | 400 onwards. Everything below that is not a real error and should be | |
23 | returned as normal response in the python layer too because of that. | |
24 | ||
25 | Unused exception such as 402 don't have their predefined subclasses | |
26 | but you can easily fill the gap by doing that yourself. | |
27 | ||
28 | If you're looking for redirection helpers have a look into the utils | |
29 | module which implements a `redirect` function that generates a simple | |
30 | redirect response. | |
31 | ||
32 | ||
33 | :copyright: 2007 by Armin Ronacher. | |
26 | return e | |
27 | ||
28 | ||
29 | As you can see from this example those exceptions are callable WSGI | |
30 | applications. Because of Python 2.3 / 2.4 compatibility those do not | |
31 | extend from the response objects but only from the python exception | |
32 | class. | |
33 | ||
34 | As a matter of fact they are not Werkzeug response objects. However you | |
35 | can get a response object by calling ``get_response()`` on a HTTP | |
36 | exception. | |
37 | ||
38 | Keep in mind that you have to pass an environment to ``get_response()`` | |
39 | because some errors fetch additional information from the WSGI | |
40 | environment. | |
41 | ||
42 | If you want to hook in a different exception page to say, an 404 status | |
43 | code, you can add a second except for a specific subclass of an error:: | |
44 | ||
45 | @responder | |
46 | def application(environ, start_response): | |
47 | request = BaseRequest(environ) | |
48 | try: | |
49 | return view(request) | |
50 | except NotFound, e: | |
51 | return not_found(request) | |
52 | except HTTPException, e: | |
53 | return e | |
54 | ||
55 | ||
56 | :copyright: 2007-2008 by Armin Ronacher. | |
34 | 57 | :license: BSD, see LICENSE for more details. |
35 | 58 | """ |
36 | 59 | from werkzeug.utils import escape |
60 | from werkzeug.wrappers import BaseResponse | |
37 | 61 | from werkzeug.http import HTTP_STATUS_CODES |
38 | 62 | |
39 | 63 | |
40 | 64 | class HTTPException(Exception): |
41 | 65 | """ |
42 | Baseclass for all HTTP exceptions. | |
66 | Baseclass for all HTTP exceptions. This exception can be called as WSGI | |
67 | application to render a default error page or you can catch the subclasses | |
68 | of it independently and render nicer error messages. | |
43 | 69 | """ |
44 | 70 | |
45 | 71 | code = None |
46 | 72 | description = None |
73 | ||
74 | def __init__(self, description=None): | |
75 | Exception.__init__(self, '%d %s' % (self.code, self.name)) | |
76 | if description is not None: | |
77 | self.description = description | |
47 | 78 | |
48 | 79 | def name(self): |
49 | 80 | """The status name.""" |
55 | 86 | return self.description |
56 | 87 | |
57 | 88 | def get_body(self, environ): |
89 | """Get the HTML body.""" | |
58 | 90 | return ( |
59 | 91 | '<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n' |
60 | 92 | '<title>%(code)s %(name)s</title>\n' |
67 | 99 | } |
68 | 100 | |
69 | 101 | def get_headers(self, environ): |
102 | """Get a list of headers.""" | |
70 | 103 | return [('Content-Type', 'text/html')] |
71 | 104 | |
105 | def get_response(self, environ): | |
106 | """Get a response object.""" | |
107 | headers = self.get_headers(environ) | |
108 | return BaseResponse(self.get_body(environ), self.code, headers) | |
109 | ||
72 | 110 | def __call__(self, environ, start_response): |
73 | status = '%d %s' % (self.code, self.name) | |
74 | start_response(status, self.get_headers(environ)) | |
75 | if environ['REQUEST_METHOD'] == 'HEAD': | |
76 | return () | |
77 | return [self.get_body(environ)] | |
111 | """Call the exception as WSGI application.""" | |
112 | response = self.get_response(environ) | |
113 | return response(environ, start_response) | |
114 | ||
115 | ||
116 | class _ProxyException(HTTPException): | |
117 | """ | |
118 | An http exception that expands renders a WSGI application on error. | |
119 | """ | |
120 | ||
121 | def __init__(self, response): | |
122 | Exception.__init__(self, 'proxy exception for %r' % response) | |
123 | self.response = response | |
124 | ||
125 | def get_response(self, environ): | |
126 | return self.response | |
78 | 127 | |
79 | 128 | |
80 | 129 | class BadRequest(HTTPException): |
130 | """ | |
131 | *400* `Bad Request` | |
132 | ||
133 | Raise if the browser send something to the application the application | |
134 | or server cannot handle. | |
135 | """ | |
81 | 136 | code = 400 |
82 | 137 | description = ( |
83 | 138 | '<p>The browser (or proxy) sent a request that this server could ' |
86 | 141 | |
87 | 142 | |
88 | 143 | class Unauthorized(HTTPException): |
144 | """ | |
145 | *401* `Unauthorized` | |
146 | ||
147 | Raise if the user is not authorized. Also used if you want to use HTTP | |
148 | basic auth. | |
149 | """ | |
89 | 150 | code = 401 |
90 | 151 | description = ( |
91 | 152 | '<p>The server could not verify that you are authorized to access ' |
98 | 159 | |
99 | 160 | |
100 | 161 | class Forbidden(HTTPException): |
162 | """ | |
163 | *403* `Forbidden` | |
164 | ||
165 | Raise if the user doesn't have the permission for the requested resource | |
166 | but was authenticated. | |
167 | """ | |
101 | 168 | code = 403 |
102 | 169 | description = ( |
103 | 170 | '<p>You don\'t have the permission to access the requested resource. ' |
106 | 173 | |
107 | 174 | |
108 | 175 | class NotFound(HTTPException): |
176 | """ | |
177 | *404* `Not Found` | |
178 | ||
179 | Raise if a resource does not exist and never existed. | |
180 | """ | |
109 | 181 | code = 404 |
110 | 182 | description = ( |
111 | 183 | '<p>The requested URL was not found on the server.</p>' |
115 | 187 | |
116 | 188 | |
117 | 189 | class MethodNotAllowed(HTTPException): |
190 | """ | |
191 | *405* `Method Not Allowed` | |
192 | ||
193 | Raise if the server used a method the resource does not handle. For | |
194 | example `POST` if the resource is view only. Especially useful for REST. | |
195 | ||
196 | The first argument for this exception should be a list of allowed methods. | |
197 | Strictly speaking the response would be invalid if you don't provide valid | |
198 | methods in the header which you can do with that list. | |
199 | """ | |
118 | 200 | code = 405 |
201 | ||
202 | def __init__(self, valid_methods=None, description=None): | |
203 | """ | |
204 | takes an optional list of valid http methods | |
205 | starting with werkzeug 0.3 the list will be mandatory | |
206 | """ | |
207 | HTTPException.__init__(self, description) | |
208 | self.valid_methods = valid_methods | |
209 | ||
210 | def get_headers(self, environ): | |
211 | headers = HTTPException.get_headers(self, environ) | |
212 | if self.valid_methods: | |
213 | headers.append(('Allow', ', '.join(self.valid_methods))) | |
214 | return headers | |
119 | 215 | |
120 | 216 | def get_description(self, environ): |
121 | 217 | m = escape(environ.get('REQUEST_METHOD', 'GET')) |
122 | return '<p>The method %s is not allowed for the requeste URL.</p>' % m | |
218 | return '<p>The method %s is not allowed for the requested URL.</p>' % m | |
219 | ||
220 | ||
221 | class NotAcceptable(HTTPException): | |
222 | """ | |
223 | *406* `Not Acceptable` | |
224 | ||
225 | Raise if the server cant return any content conforming to the | |
226 | `Accept` headers of the client. | |
227 | """ | |
228 | code = 406 | |
229 | ||
230 | description = ( | |
231 | '<p>The resource identified by the request is only capable of ' | |
232 | 'generating response entities which have content characteristics ' | |
233 | 'not acceptable according to the accept headers sent in the ' | |
234 | 'request.</p>' | |
235 | ) | |
123 | 236 | |
124 | 237 | |
125 | 238 | class RequestTimeout(HTTPException): |
239 | """ | |
240 | *408* `Request Timeout` | |
241 | ||
242 | Raise to signalize a timeout. | |
243 | """ | |
126 | 244 | code = 408 |
127 | 245 | description = ( |
128 | 246 | '<p>The server closed the network connection because the browser ' |
131 | 249 | |
132 | 250 | |
133 | 251 | class Gone(HTTPException): |
252 | """ | |
253 | *410* `Gone` | |
254 | ||
255 | Raise if a resource existed previously and went away without new location. | |
256 | """ | |
134 | 257 | code = 410 |
135 | 258 | description = ( |
136 | 259 | '<p>The requested URL is no longer available on this server and ' |
140 | 263 | |
141 | 264 | |
142 | 265 | class LengthRequired(HTTPException): |
266 | """ | |
267 | *411* `Length Required` | |
268 | ||
269 | Raise if the browser submitted data but no ``Content-Length`` header which | |
270 | is required for the kind of processing the server does. | |
271 | """ | |
143 | 272 | code = 411 |
144 | 273 | description = ( |
145 | 274 | '<p>A request with this method requires a valid <code>Content-' |
148 | 277 | |
149 | 278 | |
150 | 279 | class PreconditionFailed(HTTPException): |
280 | """ | |
281 | *412* `Precondition Failed` | |
282 | ||
283 | Status code used in combination with ``If-Match``, ``If-None-Match``, or | |
284 | ``If-Unmodified-Since``. | |
285 | """ | |
151 | 286 | code = 412 |
152 | 287 | description = ( |
153 | 288 | '<p>The precondition on the request for the URL failed positive ' |
156 | 291 | |
157 | 292 | |
158 | 293 | class RequestEntityTooLarge(HTTPException): |
294 | """ | |
295 | *413* `Request Entity Too Large` | |
296 | ||
297 | The status code one should return if the data submitted exceeded a given | |
298 | limit. | |
299 | """ | |
159 | 300 | code = 413 |
160 | 301 | description = ( |
161 | 302 | '<p>The data value transmitted exceed the capacity limit.</p>' |
163 | 304 | |
164 | 305 | |
165 | 306 | class RequestURITooLarge(HTTPException): |
307 | """ | |
308 | *414* `Request URI Too Large` | |
309 | ||
310 | Like *413* but for too long URLs. | |
311 | """ | |
166 | 312 | code = 414 |
167 | 313 | description = ( |
168 | 314 | '<p>The length of the requested URL exceeds the capacity limit ' |
171 | 317 | |
172 | 318 | |
173 | 319 | class UnsupportedMediaType(HTTPException): |
320 | """ | |
321 | *415* `Unsupported Media Type` | |
322 | ||
323 | The status code returned if the server is unable to handle the media type | |
324 | the client transmitted. | |
325 | """ | |
174 | 326 | code = 415 |
175 | 327 | description = ( |
176 | 328 | '<p>The server does not support the media type transmitted in ' |
179 | 331 | |
180 | 332 | |
181 | 333 | class InternalServerError(HTTPException): |
334 | """ | |
335 | *500* `Internal Server Error` | |
336 | ||
337 | Raise if an internal server error occoured. This is a good fallback if an | |
338 | unknown error occoured in the dispatcher. | |
339 | """ | |
182 | 340 | code = 500 |
183 | 341 | description = ( |
184 | 342 | '<p>The server encountered an internal error and was unable to ' |
188 | 346 | |
189 | 347 | |
190 | 348 | class NotImplemented(HTTPException): |
349 | """ | |
350 | *501* `Not Implemented` | |
351 | ||
352 | Raise if the application does not support the action requested by the | |
353 | browser. | |
354 | """ | |
191 | 355 | code = 501 |
192 | 356 | description = ( |
193 | 357 | '<p>The server does not support the action requested by the ' |
196 | 360 | |
197 | 361 | |
198 | 362 | class BadGateway(HTTPException): |
363 | """ | |
364 | *502* `Bad Gateway` | |
365 | ||
366 | If you do proxing in your application you should return this status code | |
367 | if you received an invalid response from the upstream server it accessed | |
368 | in attempting to fulfill the request. | |
369 | """ | |
199 | 370 | code = 502 |
200 | 371 | description = ( |
201 | 372 | '<p>The proxy server received an invalid response from an upstream ' |
204 | 375 | |
205 | 376 | |
206 | 377 | class ServiceUnavailable(HTTPException): |
378 | """ | |
379 | *503* `Service Unavailable` | |
380 | ||
381 | Status code you should return if a service is temporarily unavailable. | |
382 | """ | |
207 | 383 | code = 503 |
208 | 384 | description = ( |
209 | '<p>The serer is temporarily unabel to service your request due to ' | |
385 | '<p>The server is temporarily unable to service your request due to ' | |
210 | 386 | 'maintenance downtime or capacity problems. Please try again ' |
211 | 387 | 'later.</p>' |
212 | 388 | ) |
389 | ||
390 | ||
391 | def _find_exceptions(): | |
392 | rv = {} | |
393 | for name, obj in globals().iteritems(): | |
394 | try: | |
395 | if getattr(obj, 'code', None) is not None: | |
396 | rv[obj.code] = obj | |
397 | except TypeError: | |
398 | continue | |
399 | return rv | |
400 | default_exceptions = _find_exceptions() | |
401 | del _find_exceptions | |
402 | ||
403 | ||
404 | class Aborter(object): | |
405 | """ | |
406 | When passed a dict of code -> exception items it can be used as | |
407 | callable that raises exceptions. If the first argument to the | |
408 | callable is a integer it will be looked up in the mapping, if it's | |
409 | a WSGI application it will be raised in a proxy exception. | |
410 | ||
411 | The rest of the arguments are forwarded to the exception constructor. | |
412 | """ | |
413 | ||
414 | def __init__(self, mapping=None, extra=None): | |
415 | if mapping is None: | |
416 | mapping = default_exceptions | |
417 | self.mapping = dict(mapping) | |
418 | if extra is not None: | |
419 | self.mapping.update(extra) | |
420 | ||
421 | def __call__(self, code, *args, **kwargs): | |
422 | if not args and not kwargs and not isinstance(code, (int, long)): | |
423 | raise _ProxyException(code) | |
424 | if code not in self.mapping: | |
425 | raise LookupError('no exception for %r' % code) | |
426 | raise self.mapping[code](*args, **kwargs) | |
427 | ||
428 | abort = Aborter() |
2 | 2 | werkzeug.http |
3 | 3 | ~~~~~~~~~~~~~ |
4 | 4 | |
5 | Various WSGI related helper functions and classes. | |
6 | ||
7 | ||
8 | :copyright: 2007 by Armin Ronacher. | |
5 | Werkzeug comes with a bunch of utilties that help Werkzeug to deal with | |
6 | HTTP data. Most of the classes and functions provided by this module are | |
7 | used by the wrappers, but they are useful on their own too, especially if | |
8 | the response and request objects are not used. | |
9 | ||
10 | This covers some of the more HTTP centric features of WSGI, some other | |
11 | utilities such as cookie handling are documented in the `werkzeug.utils` | |
12 | module. | |
13 | ||
14 | ||
15 | :copyright: 2007-2008 by Armin Ronacher. | |
9 | 16 | :license: BSD, see LICENSE for more details. |
10 | 17 | """ |
18 | import re | |
19 | import rfc822 | |
20 | from datetime import datetime | |
21 | try: | |
22 | from hashlib import md5 | |
23 | except ImportError: | |
24 | from md5 import new as md5 | |
11 | 25 | try: |
12 | 26 | set = set |
27 | frozenset = frozenset | |
13 | 28 | except NameError: |
14 | from sets import Set as set | |
15 | import re | |
29 | from sets import Set as set, ImmutableSet as frozenset | |
16 | 30 | |
17 | 31 | |
18 | 32 | _accept_re = re.compile(r'([^\s;,]+)(?:[^,]*?;\s*q=(\d*(?:\.\d+)?))?') |
19 | _token_chars = set("!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" | |
20 | '^_`abcdefghijklmnopqrstuvwxyz|~') | |
33 | _token_chars = frozenset("!#$%&'*+-.0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" | |
34 | '^_`abcdefghijklmnopqrstuvwxyz|~') | |
21 | 35 | _token = '[%s]' % ''.join(_token_chars).replace('-', '\\-') |
22 | _cachecontrol_re = re.compile(r'(%s+)(?:=(?:(%s+|".*?")))?' % (_token, _token)) | |
36 | _cachecontrol_re = re.compile(r'(%s+)(?:=(?:(%s+|".*?")))?' % | |
37 | (_token, _token)) | |
38 | _etag_re = re.compile(r'([Ww]/)?(?:"(.*?)"|(.*?))(?:\s*,\s*|$)') | |
23 | 39 | |
24 | 40 | |
25 | 41 | HTTP_STATUS_CODES = { |
34 | 50 | 205: 'Reset Content', |
35 | 51 | 206: 'Partial Content', |
36 | 52 | 207: 'Multi Status', |
53 | 226: 'IM Used', # see RFC 3229 | |
37 | 54 | 300: 'Multiple Choices', |
38 | 55 | 301: 'Moved Permanently', |
39 | 56 | 302: 'Found', |
43 | 60 | 307: 'Temporary Redirect', |
44 | 61 | 400: 'Bad Request', |
45 | 62 | 401: 'Unauthorized', |
46 | 402: 'Payment Required', # unused | |
63 | 402: 'Payment Required', # unused | |
47 | 64 | 403: 'Forbidden', |
48 | 65 | 404: 'Not Found', |
49 | 66 | 405: 'Method Not Allowed', |
63 | 80 | 423: 'Locked', |
64 | 81 | 424: 'Failed Dependency', |
65 | 82 | 426: 'Upgrade Required', |
66 | 449: 'Retry With', # propritary MS extension | |
83 | 449: 'Retry With', # propritary MS extension | |
67 | 84 | 500: 'Internal Server Error', |
68 | 85 | 501: 'Not Implemented', |
69 | 86 | 502: 'Bad Gateway', |
77 | 94 | |
78 | 95 | class Accept(list): |
79 | 96 | """ |
80 | Subclass of a list for easier access to the accept values. Sorted | |
81 | by quality, best first. | |
97 | An `Accept` object is just a list subclass for lists of | |
98 | ``(value, quality)`` tuples. It is automatically sorted by quality. | |
82 | 99 | """ |
83 | 100 | |
84 | 101 | def __init__(self, values=()): |
93 | 110 | list.__init__(self, [(a, b) for b, a in values]) |
94 | 111 | |
95 | 112 | def __getitem__(self, key): |
113 | """ | |
114 | Beside index lookup (getting item n) you can also pass it a string to | |
115 | get the quality for the item. If the item is not in the list, the | |
116 | returned quality is ``0``. | |
117 | """ | |
96 | 118 | if isinstance(key, basestring): |
97 | 119 | for value in self: |
98 | 120 | if value[0] == key: |
110 | 132 | ) |
111 | 133 | |
112 | 134 | def index(self, key): |
113 | """Get the position of en entry or raise IndexError.""" | |
135 | """Get the position of en entry or raise `IndexError`.""" | |
114 | 136 | rv = self.find(key) |
115 | 137 | if rv < 0: |
116 | 138 | raise IndexError(key) |
126 | 148 | return list.find(self, key) |
127 | 149 | |
128 | 150 | def values(self): |
129 | """Just the values, not the qualities.""" | |
151 | """Return a list of the values, not the qualities.""" | |
130 | 152 | return [x[1] for x in self] |
131 | 153 | |
132 | 154 | def itervalues(self): |
140 | 162 | best = property(best) |
141 | 163 | |
142 | 164 | |
165 | class HeaderSet(object): | |
166 | """ | |
167 | Similar to the `ETags` class this implements a set like structure. | |
168 | Unlike `ETags` this is case insensitive and used for vary, allow, and | |
169 | content-language headers. | |
170 | ||
171 | If not constructed using the `parse_set_header` function the instanciation | |
172 | works like this: | |
173 | ||
174 | >>> hs = HeaderSet(['foo', 'bar', 'baz']) | |
175 | >>> hs | |
176 | HeaderSet(['foo', 'bar', 'baz']) | |
177 | """ | |
178 | ||
179 | def __init__(self, headers=None, on_update=None): | |
180 | self._headers = list(headers or ()) | |
181 | self._set = set([x.lower() for x in self._headers]) | |
182 | self.on_update = on_update | |
183 | ||
184 | def add(self, header): | |
185 | """Add a new header to the set.""" | |
186 | self.update((header,)) | |
187 | ||
188 | def remove(self, header): | |
189 | """ | |
190 | Remove a layer from the set. This raises an `IndexError` if the | |
191 | header is not in the set. | |
192 | """ | |
193 | key = header.lower() | |
194 | if key not in self._set: | |
195 | raise IndexError(header) | |
196 | self._set.remove(key) | |
197 | for idx, key in enumerate(self._headers): | |
198 | if key.lower() == header: | |
199 | del self._headers[idx] | |
200 | break | |
201 | if self.on_update is not None: | |
202 | self.on_update(self) | |
203 | ||
204 | def update(self, iterable): | |
205 | """Add all the headers from the iterable to the set.""" | |
206 | inserted_any = False | |
207 | for header in iterable: | |
208 | key = header.lower() | |
209 | if key not in self._set: | |
210 | self._headers.append(header) | |
211 | self._set.add(key) | |
212 | inserted_any = True | |
213 | if inserted_any and self.on_update is not None: | |
214 | self.on_update(self) | |
215 | ||
216 | def discard(self, header): | |
217 | """Like remove but ignores errors.""" | |
218 | try: | |
219 | return self.remove(header) | |
220 | except IndexError: | |
221 | pass | |
222 | ||
223 | def find(self, header): | |
224 | """Return the index of the header in the set or return -1 if not found.""" | |
225 | header = header.lower() | |
226 | for idx, item in enumerate(self._headers): | |
227 | if item.lower() == header: | |
228 | return idx | |
229 | return -1 | |
230 | ||
231 | def index(self, header): | |
232 | """Return the index of the headerin the set or raise an `IndexError`.""" | |
233 | rv = self.find(header) | |
234 | if rv < 0: | |
235 | raise IndexError(header) | |
236 | return rv | |
237 | ||
238 | def clear(self): | |
239 | """Clear the set.""" | |
240 | self._set.clear() | |
241 | del self._headers[:] | |
242 | if self.on_update is not None: | |
243 | self.on_update(self) | |
244 | ||
245 | def as_set(self, preserve_casing=False): | |
246 | """ | |
247 | Return the set as real python set structure. When calling this all | |
248 | the items are converted to lowercase and the ordering is lost. | |
249 | ||
250 | If `preserve_casing` is `True` the items in the set returned will | |
251 | have the original case like in the `HeaderSet`, otherwise they will | |
252 | be lowercase. | |
253 | """ | |
254 | if preserve_casing: | |
255 | return set(self._headers) | |
256 | return set(self._set) | |
257 | ||
258 | def to_header(self): | |
259 | """Convert the header set into an HTTP header string.""" | |
260 | return ', '.join(self._headers) | |
261 | ||
262 | def __getitem__(self, idx): | |
263 | return self._headers[idx] | |
264 | ||
265 | def __delitem__(self, idx): | |
266 | rv = self._headers.pop(idx) | |
267 | self._set.remove(rv.lower()) | |
268 | if self.on_update is not None: | |
269 | self.on_update(self) | |
270 | ||
271 | def __setitem__(self, idx, value): | |
272 | old = self._headers[idx] | |
273 | self._set.remove(old.lower()) | |
274 | self._headers[idx] = value | |
275 | self._set.add(value.lower()) | |
276 | if self.on_update is not None: | |
277 | self.on_update(self) | |
278 | ||
279 | def __contains__(self, header): | |
280 | return header.lower() in self._set | |
281 | ||
282 | def __len__(self): | |
283 | return len(self._set) | |
284 | ||
285 | def __iter__(self): | |
286 | return iter(self._headers) | |
287 | ||
288 | def __nonzero__(self): | |
289 | return bool(self._set) | |
290 | ||
291 | def __str__(self): | |
292 | return self.to_header() | |
293 | ||
294 | def __repr__(self): | |
295 | return '%s(%r)' % ( | |
296 | self.__class__.__name__, | |
297 | self._headers | |
298 | ) | |
299 | ||
300 | ||
143 | 301 | class CacheControl(dict): |
144 | 302 | """ |
145 | Wrapper around a dict for cache control headers. | |
303 | Subclass of a dict that stores values for a Cache-Control header. It has | |
304 | accesors for all the cache-control directives specified in RFC 2616. The | |
305 | class does not differentiate between request and response directives. | |
306 | ||
307 | Because the cache-control directives in the HTTP header use dashes the | |
308 | python descriptors use underscores for that. | |
309 | ||
310 | To get a header of the `CacheControl` object again you can convert the | |
311 | object into a string or call the `to_header()` function. If you plan | |
312 | to subclass it and add your own items have a look at the sourcecode for | |
313 | that class. | |
314 | ||
315 | The following attributes are exposed: | |
316 | ||
317 | `no_cache`, `no_store`, `max_age`, `max_stale`, `min_fresh`, | |
318 | `no_transform`, `only_if_cached`, `public`, `private`, `must_revalidate`, | |
319 | `proxy_revalidate`, and `s_maxage` | |
146 | 320 | """ |
147 | 321 | |
148 | 322 | def cache_property(key, default, type): |
249 | 423 | dict.__repr__(self) |
250 | 424 | ) |
251 | 425 | |
252 | # make cache property a staticmethod so that subclasses of | |
426 | # make cache_property a staticmethod so that subclasses of | |
253 | 427 | # `CacheControl` can use it for new properties. |
254 | 428 | cache_property = staticmethod(cache_property) |
255 | 429 | del calls_update |
256 | 430 | |
257 | 431 | |
432 | class ETags(object): | |
433 | """ | |
434 | A set that can be used to check if one etag is present in a collection | |
435 | of etags. | |
436 | """ | |
437 | ||
438 | def __init__(self, strong_etags=None, weak_etags=None, star_tag=False): | |
439 | self._strong = frozenset(not star_tag and strong_etags or ()) | |
440 | self._weak = frozenset(weak_etags or ()) | |
441 | self.star_tag = star_tag | |
442 | ||
443 | def as_set(self, include_weak=False): | |
444 | """ | |
445 | Convert the `ETags` object into a python set. Per default all the | |
446 | weak etags are not part of this set. | |
447 | """ | |
448 | rv = set(self._strong) | |
449 | if include_weak: | |
450 | rv.update(self._weak) | |
451 | return rv | |
452 | ||
453 | def is_weak(self, etag): | |
454 | """Check if an etag is weak.""" | |
455 | return etag in self._weak | |
456 | ||
457 | def contains_weak(self, etag): | |
458 | """Check if an etag is part of the set including weak and strong tags.""" | |
459 | return self.is_weak(etag) or self.contains(etag) | |
460 | ||
461 | def contains(self, etag): | |
462 | """Check if an etag is part of the set ignoring weak tags.""" | |
463 | if self.star_tag: | |
464 | return True | |
465 | return etag in self._strong | |
466 | ||
467 | def contains_raw(self, etag): | |
468 | """ | |
469 | When passed a quoted tag it will check if this tag is part of the set. | |
470 | If the tag is weak it is checked against weak and strong tags, otherwise | |
471 | weak only. | |
472 | """ | |
473 | etag, weak = unquote_etag(etag) | |
474 | if weak: | |
475 | return self.contains_weak(etag) | |
476 | return self.contains(etag) | |
477 | ||
478 | def to_header(self): | |
479 | """Convert the etags set into a HTTP header string.""" | |
480 | if self.star_tag: | |
481 | return '*' | |
482 | return ', '.join(['"%s"' % item for item in self.as_set(True)]) | |
483 | ||
484 | def __call__(self, etag=None, data=None, include_weak=False): | |
485 | if [etag, data].count(None) != 1: | |
486 | raise TypeError('either tag or data required, but at least one') | |
487 | if etag is None: | |
488 | etag = generate_etag(data) | |
489 | if include_weak: | |
490 | if etag in self._weak: | |
491 | return True | |
492 | return etag in self._strong | |
493 | ||
494 | def __nonzero__(self): | |
495 | return bool(self.star_tag or self._strong) | |
496 | ||
497 | def __str__(self): | |
498 | return self.to_header() | |
499 | ||
500 | def __iter__(self): | |
501 | return iter(self._strong) | |
502 | ||
503 | def __contains__(self, etag): | |
504 | return self.contains(etag) | |
505 | ||
506 | def __repr__(self): | |
507 | return '<%s %r>' % (self.__class__.__name__, str(self)) | |
508 | ||
509 | ||
258 | 510 | def parse_accept_header(value): |
259 | 511 | """ |
260 | 512 | Parses an HTTP Accept-* header. This does not implement a complete valid |
263 | 515 | Returns a new `Accept` object (basicly a list of ``(value, quality)`` |
264 | 516 | tuples sorted by the quality with some additional accessor methods) |
265 | 517 | """ |
518 | if not value: | |
519 | return Accept(None) | |
266 | 520 | result = [] |
267 | 521 | for match in _accept_re.finditer(value): |
268 | 522 | quality = match.group(2) |
274 | 528 | return Accept(result) |
275 | 529 | |
276 | 530 | |
277 | def parse_cache_control_header(value): | |
531 | def parse_cache_control_header(value, on_update=None): | |
278 | 532 | """ |
279 | 533 | Parse a cache control header. The RFC differs between response and |
280 | 534 | request cache control, this method does not. It's your responsibility |
281 | 535 | to not use the wrong control statements. |
282 | 536 | """ |
537 | if not value: | |
538 | return CacheControl(None, on_update) | |
283 | 539 | result = {} |
284 | 540 | for match in _cachecontrol_re.finditer(value): |
285 | 541 | name, value = match.group(1, 2) |
286 | 542 | if value and value[0] == value[-1] == '"': |
287 | 543 | value = value[1:-1] |
288 | 544 | result[name] = value |
289 | return CacheControl(result) | |
545 | return CacheControl(result, on_update) | |
546 | ||
547 | ||
548 | def parse_set_header(value, on_update=None): | |
549 | """ | |
550 | Parse a set like header and return a `HeaderSet` object. The return | |
551 | value is an object that treats the items case insensitive and keeps the | |
552 | order of the items. | |
553 | """ | |
554 | if not value: | |
555 | return HeaderSet(None, on_update) | |
556 | return HeaderSet([x.strip() for x in value.split(',')], on_update) | |
557 | ||
558 | ||
559 | def quote_etag(etag, weak=False): | |
560 | """Quote an etag.""" | |
561 | if '"' in etag: | |
562 | raise ValueError('invalid etag') | |
563 | etag = '"%s"' % etag | |
564 | if weak: | |
565 | etag = 'w/' + etag | |
566 | return etag | |
567 | ||
568 | ||
569 | def unquote_etag(etag): | |
570 | """Unquote a single etag. Return a ``(etag, weak)`` tuple.""" | |
571 | if not etag: | |
572 | return None, None | |
573 | etag = etag.strip() | |
574 | weak = False | |
575 | if etag[:2] in ('w/', 'W/'): | |
576 | weak = True | |
577 | etag = etag[2:] | |
578 | if etag[:1] == etag[-1:] == '"': | |
579 | etag = etag[1:-1] | |
580 | return etag, weak | |
581 | ||
582 | ||
583 | def parse_etags(value): | |
584 | """Parse and etag header. Returns an `ETags` object.""" | |
585 | if not value: | |
586 | return ETags() | |
587 | strong = [] | |
588 | weak = [] | |
589 | end = len(value) | |
590 | pos = 0 | |
591 | while pos < end: | |
592 | match = _etag_re.match(value, pos) | |
593 | if match is None: | |
594 | break | |
595 | is_weak, quoted, raw = match.groups() | |
596 | if raw == '*': | |
597 | return ETags(star_tag=True) | |
598 | elif quoted: | |
599 | raw = quoted | |
600 | if is_weak: | |
601 | weak.append(raw) | |
602 | else: | |
603 | strong.append(raw) | |
604 | pos = match.end() | |
605 | return ETags(strong, weak) | |
606 | ||
607 | ||
608 | def generate_etag(data): | |
609 | """Generate an etag for some data.""" | |
610 | return md5(data).hexdigest() | |
611 | ||
612 | ||
613 | def parse_date(value): | |
614 | """ | |
615 | Parse one of the following date formats into a datetime object: | |
616 | ||
617 | .. sourcecode:: text | |
618 | ||
619 | Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 | |
620 | Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 | |
621 | Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format | |
622 | ||
623 | If parsing fails the return value is `None`. | |
624 | """ | |
625 | if value: | |
626 | t = rfc822.parsedate_tz(value) | |
627 | if t is not None: | |
628 | return datetime.utcfromtimestamp(rfc822.mktime_tz(t)) | |
629 | ||
630 | ||
631 | def is_resource_modified(environ, etag=None, data=None, last_modified=None): | |
632 | """Convenience method for conditional requests.""" | |
633 | if etag is None and data is not None: | |
634 | etag = generate_etag(data) | |
635 | elif data is not None: | |
636 | raise TypeError('both data and etag given') | |
637 | if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): | |
638 | return False | |
639 | ||
640 | unmodified = False | |
641 | if isinstance(last_modified, basestring): | |
642 | last_modified = parse_date(last_modified) | |
643 | modified_since = parse_date(environ.get('HTTP_IF_MODIFIED_SINCE')) | |
644 | ||
645 | if modified_since and last_modified and last_modified <= modified_since: | |
646 | unmodified = True | |
647 | if etag: | |
648 | if_none_match = parse_etags(environ.get('HTTP_IF_NONE_MATCH')) | |
649 | if if_none_match: | |
650 | unmodified = if_none_match.contains_raw(etag) | |
651 | ||
652 | return not unmodified |
2 | 2 | werkzeug.local |
3 | 3 | ~~~~~~~~~~~~~~ |
4 | 4 | |
5 | Special class to manage request local objects as globals. This is a | |
6 | wrapper around `py.magic.greenlet.getcurrent` if available and | |
7 | `threading.currentThread`. | |
8 | ||
9 | Use it like this:: | |
10 | ||
11 | from werkzeug import Local, LocalManager, ClosingIterator | |
5 | Sooner or later you have some things you want to have in every single view | |
6 | or helper function or whatever. In PHP the way to go are global | |
7 | variables. However that is not possible in WSGI applications without a | |
8 | major drawback: As soon as you operate on the global namespace your | |
9 | application is not thread safe any longer. | |
10 | ||
11 | The python standard library comes with a utility called "thread locals". | |
12 | A thread local is a global object where you can put stuff on and get back | |
13 | later in a thread safe way. That means whenever you set or get an object | |
14 | to / from a thread local object the thread local object checks in which | |
15 | thread you are and delivers the correct value. | |
16 | ||
17 | This however has a few disadvantages. For example beside threads there | |
18 | are other ways to handle concurrency in Python. A very popular approach | |
19 | are greenlets. Also, whether every request gets its own thread is not | |
20 | guaranteed in WSGI. It could be that a request is reusing a thread from | |
21 | before and data is left in the thread local object. | |
22 | ||
23 | ||
24 | Nutshell | |
25 | -------- | |
26 | ||
27 | Here a simple example how you can use werkzeug.local:: | |
28 | ||
29 | from werkzeug import Local, LocalManager | |
12 | 30 | |
13 | 31 | local = Local() |
14 | 32 | local_manager = LocalManager([local]) |
15 | 33 | |
16 | def view(request): | |
17 | return Response('...') | |
18 | ||
19 | 34 | def application(environ, start_response): |
20 | request = Request(environ) | |
21 | local.request = request | |
22 | response = view(request) | |
23 | return ClosingIterator(response(environ, start_response), | |
24 | local_manager.cleanup) | |
25 | ||
26 | Additionally you can use the `make_middleware` middleware factory to | |
27 | accomplish the same:: | |
28 | ||
29 | from werkzeug import Local, LocalManager, ClosingIterator | |
30 | ||
31 | local = Local() | |
32 | local_manager = LocalManager([local]) | |
33 | ||
34 | def view(request): | |
35 | return Response('...') | |
36 | ||
37 | def application(environ, start_response): | |
38 | request = Request(environ) | |
39 | local.request = request | |
40 | return view(request)(environ, start_response) | |
35 | local.request = request = Request(environ) | |
36 | ... | |
41 | 37 | |
42 | 38 | application = local_manager.make_middleware(application) |
43 | 39 | |
44 | :copyright: 2007 by Armin Ronacher. | |
40 | Now what this code does is binding request to `local.request`. Every | |
41 | other piece of code executed after this assignment in the same context can | |
42 | safely access local.request and will get the same request object. The | |
43 | `make_middleware` method on the local manager ensures that everything is | |
44 | cleaned up after the request. | |
45 | ||
46 | The same context means the same greenlet (if you're using greenlets) in | |
47 | the same thread and same process. | |
48 | ||
49 | If a request object is not yet set on the local object and you try to | |
50 | access it you will get an `AttributeError`. You can use `getattr` to avoid | |
51 | that:: | |
52 | ||
53 | def get_request(): | |
54 | return getattr(local, 'request', None) | |
55 | ||
56 | This will try to get the request or return `None` if the request is not | |
57 | (yet?) available. | |
58 | ||
59 | Note that local objects cannot manage themselves, for that you need a local | |
60 | manager. You can pass a local manager multiple locals or add additionals | |
61 | later by appending them to `manager.locals` and everytime the manager | |
62 | cleans up it will clean up all the data left in the locals for this | |
63 | context. | |
64 | ||
65 | ||
66 | :copyright: 2007-2008 by Armin Ronacher. | |
45 | 67 | :license: BSD, see LICENSE for more details. |
46 | 68 | """ |
47 | 69 | try: |
49 | 71 | get_current_greenlet = greenlet.getcurrent |
50 | 72 | del greenlet |
51 | 73 | except (RuntimeError, ImportError): |
52 | get_current_greenlet = lambda: None | |
74 | get_current_greenlet = int | |
53 | 75 | try: |
54 | from thread import get_ident as get_current_thread | |
55 | from threading import Lock | |
76 | from thread import get_ident as get_current_thread, allocate_lock | |
56 | 77 | except ImportError: |
57 | from dummy_thread import get_ident as get_current_thread | |
58 | from dummy_threading import Lock | |
59 | from werkzeug.utils import ClosingIterator | |
60 | ||
61 | ||
62 | def get_ident(): | |
63 | """ | |
64 | Return a unique number for the current greenlet in the current thread. | |
65 | """ | |
66 | return hash((get_current_thread(), get_current_greenlet())) | |
78 | from dummy_thread import get_ident as get_current_thread, allocate_lock | |
79 | from werkzeug.utils import ClosingIterator, _patch_wrapper | |
80 | ||
81 | ||
82 | # get the best ident function. if greenlets are not installed we can | |
83 | # savely just use the builtin thread function and save a python methodcall | |
84 | # and the cost of caculating a hash. | |
85 | if get_current_greenlet is int: | |
86 | get_ident = get_current_thread | |
87 | else: | |
88 | get_ident = lambda: hash((get_current_thread(), get_current_greenlet())) | |
67 | 89 | |
68 | 90 | |
69 | 91 | class Local(object): |
92 | __slots__ = ('__storage__', '__lock__') | |
70 | 93 | |
71 | 94 | def __init__(self): |
72 | self.__dict__.update( | |
73 | __storage={}, | |
74 | __lock=Lock() | |
75 | ) | |
95 | object.__setattr__(self, '__storage__', {}) | |
96 | object.__setattr__(self, '__lock__', allocate_lock()) | |
76 | 97 | |
77 | 98 | def __iter__(self): |
78 | return self.__dict__['__storage'].iteritems() | |
99 | return self.__storage__.iteritems() | |
79 | 100 | |
80 | 101 | def __call__(self, proxy): |
81 | 102 | """Create a proxy for a name.""" |
82 | 103 | return LocalProxy(self, proxy) |
83 | 104 | |
84 | 105 | def __getattr__(self, name): |
85 | self.__dict__['__lock'].acquire() | |
86 | try: | |
87 | ident = get_ident() | |
88 | if ident not in self.__dict__['__storage']: | |
89 | raise AttributeError(name) | |
106 | self.__lock__.acquire() | |
107 | try: | |
90 | 108 | try: |
91 | return self.__dict__['__storage'][ident][name] | |
109 | return self.__storage__[get_ident()][name] | |
92 | 110 | except KeyError: |
93 | 111 | raise AttributeError(name) |
94 | 112 | finally: |
95 | self.__dict__['__lock'].release() | |
113 | self.__lock__.release() | |
96 | 114 | |
97 | 115 | def __setattr__(self, name, value): |
98 | self.__dict__['__lock'].acquire() | |
116 | self.__lock__.acquire() | |
99 | 117 | try: |
100 | 118 | ident = get_ident() |
101 | storage = self.__dict__['__storage'] | |
119 | storage = self.__storage__ | |
102 | 120 | if ident in storage: |
103 | 121 | storage[ident][name] = value |
104 | 122 | else: |
105 | 123 | storage[ident] = {name: value} |
106 | 124 | finally: |
107 | self.__dict__['__lock'].release() | |
125 | self.__lock__.release() | |
108 | 126 | |
109 | 127 | def __delattr__(self, name): |
110 | self.__dict__['__lock'].acquire() | |
111 | try: | |
112 | ident = get_ident() | |
113 | if ident not in self.__dict__['__storage']: | |
114 | raise AttributeError(name) | |
128 | self.__lock__.acquire() | |
129 | try: | |
115 | 130 | try: |
116 | del self.__dict__['__storage'][ident][name] | |
131 | del self.__storage__[get_ident()][name] | |
117 | 132 | except KeyError: |
118 | 133 | raise AttributeError(name) |
119 | 134 | finally: |
120 | self.__dict__['__lock'].release() | |
135 | self.__lock__.release() | |
121 | 136 | |
122 | 137 | |
123 | 138 | class LocalManager(object): |
124 | 139 | """ |
125 | Manages local objects. | |
140 | Local objects cannot manage themselves. For that you need a local manager. | |
141 | You can pass a local manager multiple locals or add them later by | |
142 | appending them to `manager.locals`. Everytime the manager cleans up it, | |
143 | will clean up all the data left in the locals for this context. | |
126 | 144 | """ |
127 | 145 | |
128 | 146 | def __init__(self, locals=None): |
135 | 153 | self.locals = [locals] |
136 | 154 | |
137 | 155 | def get_ident(self): |
138 | """Returns the current identifier for this context.""" | |
156 | """ | |
157 | Return the context identifier the local objects use internally for | |
158 | this context. You cannot override this method to change the behavior | |
159 | but use it to link other context local objects (such as SQLAlchemy's | |
160 | scoped sessions) to the Werkzeug locals. | |
161 | """ | |
139 | 162 | return get_ident() |
140 | 163 | |
141 | 164 | def cleanup(self): |
142 | 165 | """ |
143 | Call this at the request end to clean up all data stored for | |
144 | the current greenlet / thread. | |
166 | Manually clean up the data in the locals for this context. Call this | |
167 | at the end of the request or use `make_middleware()`. | |
145 | 168 | """ |
146 | 169 | ident = self.get_ident() |
147 | 170 | for local in self.locals: |
148 | d = local.__dict__ | |
149 | d['__lock'].acquire() | |
150 | try: | |
151 | d['__storage'].pop(ident, None) | |
152 | finally: | |
153 | d['__lock'].release() | |
171 | local.__storage__.pop(ident, None) | |
154 | 172 | |
155 | 173 | def make_middleware(self, app): |
156 | 174 | """ |
158 | 176 | request end. |
159 | 177 | """ |
160 | 178 | def application(environ, start_response): |
161 | return ClosingIterator(app(environ, start_response), | |
162 | self.cleanup) | |
179 | return ClosingIterator(app(environ, start_response), self.cleanup) | |
163 | 180 | return application |
164 | 181 | |
165 | 182 | def middleware(self, func): |
175 | 192 | will have all the arguments copied from the inner application |
176 | 193 | (name, docstring, module). |
177 | 194 | """ |
178 | new_func = self.make_middleware(func) | |
179 | try: | |
180 | new_func.__name__ = func.__name__ | |
181 | new_func.__doc__ = func.__doc__ | |
182 | new_func.__module__ = func.__module__ | |
183 | except: | |
184 | pass | |
185 | return new_func | |
195 | return _patch_wrapper(func, self.make_middleware(func)) | |
186 | 196 | |
187 | 197 | def __repr__(self): |
188 | 198 | return '<%s storages: %d>' % ( |
199 | 209 | |
200 | 210 | Example usage:: |
201 | 211 | |
202 | from werkzeug import Local, LocalProxy | |
212 | from werkzeug import Local | |
203 | 213 | l = Local() |
204 | request = LocalProxy(l, "request") | |
205 | user = LocalProxy(l, "user") | |
214 | request = l('request') | |
215 | user = l('user') | |
206 | 216 | |
207 | 217 | Whenever something is bound to l.user / l.request the proxy objects |
208 | 218 | will forward all operations. If no object is bound a `RuntimeError` |
2 | 2 | werkzeug.routing |
3 | 3 | ~~~~~~~~~~~~~~~~ |
4 | 4 | |
5 | An extensible URL mapper. | |
6 | ||
7 | Map creation:: | |
8 | ||
9 | >>> m = Map([ | |
10 | ... # Static URLs | |
11 | ... Rule('/', endpoint='static/index'), | |
12 | ... Rule('/about', endpoint='static/about'), | |
13 | ... Rule('/help', endpoint='static/help'), | |
14 | ... # Knowledge Base | |
15 | ... Subdomain('kb', [ | |
16 | ... Rule('/', endpoint='kb/index'), | |
17 | ... Rule('/browse/', endpoint='kb/browse'), | |
18 | ... Rule('/browse/<int:id>/', endpoint='kb/browse'), | |
19 | ... Rule('/browse/<int:id>/<int:page>', endpoint='kb/browse') | |
20 | ... ]) | |
21 | ... ], default_subdomain='www') | |
22 | ||
23 | URL building:: | |
24 | ||
25 | >>> c = m.bind('example.com', '/') | |
26 | >>> c.build("kb/browse", dict(id=42)) | |
27 | 'http://kb.example.com/browse/42/' | |
28 | >>> c.build("kb/browse", dict()) | |
29 | 'http://kb.example.com/browse/' | |
30 | >>> c.build("kb/browse", dict(id=42, page=3)) | |
31 | 'http://kb.example.com/browse/42/3' | |
32 | >>> c.build("static/about") | |
33 | u'/about' | |
34 | >>> c.build("static/about", subdomain="kb") | |
35 | 'http://www.example.com/about' | |
36 | >>> c.build("static/index", force_external=True) | |
37 | 'http://www.example.com/' | |
38 | ||
39 | URL matching:: | |
40 | ||
41 | >>> c = m.bind('example.com', '/') | |
42 | >>> c.match("/") | |
43 | ('static/index', {}) | |
44 | >>> c.match("/about") | |
45 | ('static/about', {}) | |
46 | >>> c = m.bind('example.com', '/', 'kb') | |
47 | >>> c.match("/", subdomain="kb") | |
48 | ('kb/index', {}) | |
49 | >>> c.match("/browse/42/23", subdomain="kb") | |
50 | ('kb/browse', {'id': 42, 'page': 23}) | |
51 | ||
52 | Exceptions:: | |
53 | ||
54 | >>> m.match("/browse/42", subdomain="kb") | |
55 | Traceback (most recent call last): | |
56 | ... | |
57 | werkzeug.routing.RequestRedirect: http://kb.example.com/browse/42/ | |
58 | >>> m.match("/missing", subdomain="kb") | |
59 | Traceback (most recent call last): | |
60 | ... | |
61 | werkzeug.routing.NotFound: /missing | |
62 | >>> m.match("/missing", subdomain="kb") | |
63 | ||
64 | ||
65 | :copyright: 2007 by Armin Ronacher, Leif K-Brooks. | |
5 | When it comes to combining multiple controller or view functions (however | |
6 | you want to call them) you need a dispatcher. A simple way would be | |
7 | applying regular expression tests on the ``PATH_INFO`` and call registered | |
8 | callback functions that return the value then. | |
9 | ||
10 | This module implements a much more powerful system than simple regular | |
11 | expression matching because it can also convert values in the URLs and | |
12 | build URLs. | |
13 | ||
14 | Here a simple example that creates an URL map for an application with | |
15 | two subdomains (www and kb) and some URL rules:: | |
16 | ||
17 | m = Map([ | |
18 | # Static URLs | |
19 | Rule('/', endpoint='static/index'), | |
20 | Rule('/about', endpoint='static/about'), | |
21 | Rule('/help', endpoint='static/help'), | |
22 | ||
23 | # Knowledge Base | |
24 | Subdomain('kb', [ | |
25 | Rule('/', endpoint='kb/index'), | |
26 | Rule('/browse/', endpoint='kb/browse'), | |
27 | Rule('/browse/<int:id>/', endpoint='kb/browse'), | |
28 | Rule('/browse/<int:id>/<int:page>', endpoint='kb/browse') | |
29 | ]) | |
30 | ], default_subdomain='www') | |
31 | ||
32 | If the application doesn't use subdomains it's perfectly fine to not set | |
33 | the default subdomain and use the `Subdomain` rule factory. The endpoint | |
34 | in the rules can be anything, for example import paths or unique | |
35 | identitifers. The WSGI application can use those endpoints to get the | |
36 | handler for that URL. It doesn't have to be a string at all but it's | |
37 | recommended. | |
38 | ||
39 | Now it's possible to create a URL adapter for one of the subdomains and | |
40 | build URLs: | |
41 | ||
42 | >>> c = m.bind('example.com') | |
43 | >>> c.build("kb/browse", dict(id=42)) | |
44 | 'http://kb.example.com/browse/42/' | |
45 | >>> c.build("kb/browse", dict()) | |
46 | 'http://kb.example.com/browse/' | |
47 | >>> c.build("kb/browse", dict(id=42, page=3)) | |
48 | 'http://kb.example.com/browse/42/3' | |
49 | >>> c.build("static/about") | |
50 | u'/about' | |
51 | >>> c.build("static/about", subdomain="kb") | |
52 | 'http://www.example.com/about' | |
53 | >>> c.build("static/index", force_external=True) | |
54 | 'http://www.example.com/' | |
55 | ||
56 | The first argument to bind is the server name *without* the subdomain. | |
57 | Per default it will assume that the script is mounted on the root, but | |
58 | often that's not the case so you can provide the real mount point as | |
59 | second argument: | |
60 | ||
61 | >>> c = m.bind('example.com', '/applications/example') | |
62 | ||
63 | The third argument can be the subdomain, if not given the default | |
64 | subdomain is used. For more details about binding have a look at the | |
65 | documentation of the `MapAdapter`. | |
66 | ||
67 | And here is how you can match URLs: | |
68 | ||
69 | >>> c = m.bind('example.com') | |
70 | >>> c.match("/") | |
71 | ('static/index', {}) | |
72 | >>> c.match("/about") | |
73 | ('static/about', {}) | |
74 | >>> c = m.bind('example.com', '/', 'kb') | |
75 | >>> c.match("/") | |
76 | ('kb/index', {}) | |
77 | >>> c.match("/browse/42/23") | |
78 | ('kb/browse', {'id': 42, 'page': 23}) | |
79 | ||
80 | If matching fails you get a `NotFound` exception, if the rule thinks | |
81 | it's a good idea to redirect (for example because the URL was defined | |
82 | to have a slash at the end but the request was missing that slash) it | |
83 | will raise a `RequestRedirect` exception. Both are subclasses of the | |
84 | `HTTPException` so you can use those errors as responses in the | |
85 | application. | |
86 | ||
87 | If matching succeeded but the URL rule was incompatible to the given | |
88 | method (for example there were only rules for `GET` and `HEAD` and | |
89 | routing system tried to match a `POST` request) a `MethodNotAllowed` | |
90 | method is raised. | |
91 | ||
92 | ||
93 | :copyright: 2007-2008 by Armin Ronacher, Leif K-Brooks, | |
94 | Thomas Johansson. | |
66 | 95 | :license: BSD, see LICENSE for more details. |
67 | 96 | """ |
68 | 97 | import sys |
69 | 98 | import re |
70 | 99 | from urlparse import urljoin |
71 | from urllib import quote | |
72 | ||
73 | from werkzeug.utils import url_encode, redirect, format_string | |
74 | from werkzeug.exceptions import NotFound | |
100 | from itertools import izip | |
101 | ||
102 | from werkzeug.utils import url_encode, url_quote, redirect, format_string | |
103 | from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed | |
75 | 104 | try: |
76 | 105 | set |
77 | 106 | except NameError: |
96 | 125 | Parse a rule and return it as generator. Each iteration yields tuples in the |
97 | 126 | form ``(converter, arguments, variable)``. If the converter is `None` it's a |
98 | 127 | static url part, otherwise it's a dynamic one. |
128 | ||
129 | :internal: | |
99 | 130 | """ |
100 | 131 | pos = 0 |
101 | 132 | end = len(rule) |
126 | 157 | """ |
127 | 158 | Create a new converter for the given arguments or raise |
128 | 159 | exception if the converter does not exist. |
160 | ||
161 | :internal: | |
129 | 162 | """ |
130 | 163 | if not name in map.converters: |
131 | 164 | raise LookupError('the converter %r does not exist' % name) |
132 | 165 | if args: |
133 | 166 | storage = type('_Storage', (), {'__getitem__': lambda s, x: x})() |
134 | args, kwargs = eval('(lambda *a, **kw: (a, kw))(%s)' % args, {}, storage) | |
167 | args, kwargs = eval(u'(lambda *a, **kw: (a, kw))(%s)' % args, {}, storage) | |
135 | 168 | else: |
136 | 169 | args = () |
137 | 170 | kwargs = {} |
142 | 175 | """ |
143 | 176 | Special exceptions that require the application to redirect, notifies him |
144 | 177 | about missing urls etc. |
145 | """ | |
146 | ||
147 | ||
148 | class RequestRedirect(RoutingException): | |
178 | ||
179 | :internal: | |
180 | """ | |
181 | ||
182 | ||
183 | class RequestRedirect(HTTPException, RoutingException): | |
149 | 184 | """ |
150 | 185 | Raise if the map requests a redirect. This is for example the case if |
151 | 186 | `strict_slashes` are activated and an url that requires a leading slash. |
152 | 187 | |
153 | 188 | The attribute `new_url` contains the absolute desitination url. |
154 | 189 | """ |
190 | code = 301 | |
155 | 191 | |
156 | 192 | def __init__(self, new_url): |
193 | RoutingException.__init__(self, new_url) | |
157 | 194 | self.new_url = new_url |
158 | RoutingException.__init__(self, new_url) | |
159 | ||
160 | def __call__(self, environ, start_response): | |
161 | return redirect(self.new_url)(environ, start_response) | |
195 | ||
196 | def get_response(self, environ): | |
197 | return redirect(self.new_url, 301) | |
162 | 198 | |
163 | 199 | |
164 | 200 | class RequestSlash(RoutingException): |
173 | 209 | values provided. |
174 | 210 | """ |
175 | 211 | |
176 | def __init__(self, endpoint, values): | |
177 | LookupError.__init__(self, endpoint) | |
212 | def __init__(self, endpoint, values, method): | |
213 | LookupError.__init__(self, endpoint, values, method) | |
178 | 214 | self.endpoint = endpoint |
179 | 215 | self.values = values |
216 | self.method = method | |
180 | 217 | |
181 | 218 | |
182 | 219 | class ValidationError(ValueError): |
183 | 220 | """ |
184 | Validation error. | |
221 | Validation error. If a rule converter raises this exception the rule | |
222 | does not match the current URL and the next URL is tried. | |
185 | 223 | """ |
186 | 224 | |
187 | 225 | |
188 | 226 | class RuleFactory(object): |
189 | 227 | """ |
190 | An object that produces Rules when given a Map. | |
228 | As soon as you have more complex URL setups it's a good idea to use rule | |
229 | factories to avoid repetitive tasks. Some of them are builtin, others can | |
230 | be added by subclassing `RuleFactory` and overriding `get_rules`. | |
191 | 231 | """ |
192 | 232 | |
193 | 233 | def get_rules(self, map): |
234 | """ | |
235 | Subclasses of `RuleFactory` have to override this method and return | |
236 | an iterable of rules. | |
237 | """ | |
194 | 238 | raise NotImplementedError() |
195 | 239 | |
196 | 240 | |
197 | 241 | class Subdomain(RuleFactory): |
198 | 242 | """ |
199 | Collects rules for a given subdomain. | |
243 | All URLs provided by this factory have the subdomain set to a | |
244 | specific domain. For example if you want to use the subdomain for | |
245 | the current language this can be a good setup:: | |
246 | ||
247 | url_map = Map([ | |
248 | Rule('/', endpoint='#select_language'), | |
249 | Subdomain('<string(length=2):lang_code>', [ | |
250 | Rule('/', endpoint='index'), | |
251 | Rule('/about', endpoint='about'), | |
252 | Rule('/help', endpoint='help') | |
253 | ]) | |
254 | ]) | |
255 | ||
256 | All the rules except of the ``'#select_language'`` endpoint will now | |
257 | listen on a two letter long subdomain that helds the language code | |
258 | for the current request. | |
200 | 259 | """ |
201 | 260 | |
202 | 261 | def __init__(self, subdomain, rules): |
212 | 271 | |
213 | 272 | class Submount(RuleFactory): |
214 | 273 | """ |
215 | Collects rules for a given path. | |
274 | Like `Subdomain` but prefixes the URL rule with a given string:: | |
275 | ||
276 | url_map = Map([ | |
277 | Rule('/', endpoint='index'), | |
278 | Submount('/blog', [ | |
279 | Rule('/', endpoint='blog/index'), | |
280 | Rule('/entry/<entry_slug>', endpoint='blog/show') | |
281 | ]) | |
282 | ]) | |
283 | ||
284 | Now the rule ``'blog/show'`` matches ``/blog/entry/<entry_slug>``. | |
216 | 285 | """ |
217 | 286 | |
218 | 287 | def __init__(self, path, rules): |
228 | 297 | |
229 | 298 | class EndpointPrefix(RuleFactory): |
230 | 299 | """ |
231 | Prefixes all endpoints with a given string. | |
300 | Prefixes all endpoints (which must be strings for this factory) with | |
301 | another string. This can be useful for sub applications:: | |
302 | ||
303 | url_map = Map([ | |
304 | Rule('/', endpoint='index'), | |
305 | EndpointPrefix('blog/', [Submount('/blog', [ | |
306 | Rule('/', endpoint='index'), | |
307 | Rule('/entry/<entry_slug>', endpoint='show') | |
308 | ])]) | |
309 | ]) | |
232 | 310 | """ |
233 | 311 | |
234 | 312 | def __init__(self, prefix, rules): |
246 | 324 | """ |
247 | 325 | Returns copies of the rules wrapped and expands string templates in |
248 | 326 | the endpoint, rule, defaults or subdomain sections. |
327 | ||
328 | Here a small example for such a rule template:: | |
329 | ||
330 | from werkzeug.routing import Map, Rule, RuleTemplate | |
331 | ||
332 | resource = RuleTemplate([ | |
333 | Rule('/$name/', endpoint='$name.list'), | |
334 | Rule('/$name/<int:id>', endpoint='$name.show') | |
335 | ]) | |
336 | ||
337 | url_map = Map([resource(name='user'), resource(name='page')]) | |
338 | ||
339 | When a rule template is called the keyword arguments are used to | |
340 | replace the placeholders in all the string parameters. | |
249 | 341 | """ |
250 | 342 | |
251 | 343 | def __init__(self, rules): |
259 | 351 | """ |
260 | 352 | A factory that fills in template variables into rules. Used by |
261 | 353 | `RuleTemplate` internally. |
354 | ||
355 | :internal: | |
262 | 356 | """ |
263 | 357 | |
264 | 358 | def __init__(self, rules, context): |
293 | 387 | |
294 | 388 | class Rule(RuleFactory): |
295 | 389 | """ |
296 | Represents one url pattern. | |
390 | A Rule represents one URL pattern. There are some options for `Rule` that | |
391 | change the way it behaves and are passed to the `Rule` constructor. Note | |
392 | that beside the rule-string all arguments *must* be keyword arguments in | |
393 | order to not break the application on Werkzeug upgrades. | |
394 | ||
395 | `string` | |
396 | Rule strings basically are just normal URL paths with placeholders in | |
397 | the format ``<converter(arguments):name>`` where the converter and the | |
398 | arguments are optional. If no converter is defined the `default` | |
399 | converter is used which means `string` in the normal configuration. | |
400 | ||
401 | URL rules that end with a slash are branch URLs, others are leaves. | |
402 | If you have `strict_slashes` enabled (which is the default), all | |
403 | branch URLs that are matched without a trailing slash will trigger a | |
404 | redirect to the same URL with the missing slash appended. | |
405 | ||
406 | The converters are defined on the `Map`. | |
407 | ||
408 | `endpoint` | |
409 | The endpoint for this rule. This can be anything. A reference to a | |
410 | function, a string, a number etc. The preferred way is using a string | |
411 | as because the endpoint is used for URL generation. | |
412 | ||
413 | `defaults` | |
414 | An optional dict with defaults for other rules with the same endpoint. | |
415 | This is a bit tricky but useful if you want to have unique URLs:: | |
416 | ||
417 | url_map = Map([ | |
418 | Rule('/all/', defaults={'page': 1}, endpoint='all_entries'), | |
419 | Rule('/all/page/<int:page>', endpoint='all_entries') | |
420 | ]) | |
421 | ||
422 | If a user now visits ``http://example.com/all/page/1`` he will be | |
423 | redirected to ``http://example.com/all/``. If `redirect_defaults` is | |
424 | disabled on the `Map` instance this will only affect the URL | |
425 | generation. | |
426 | ||
427 | `subdomain` | |
428 | The subdomain rule string for this rule. If not specified the rule | |
429 | only matches for the `default_subdomain` of the map. If the map is | |
430 | not bound to a subdomain this feature is disabled. | |
431 | ||
432 | Can be useful if you want to have user profiles on different subdomains | |
433 | and all subdomains are forwarded to your application:: | |
434 | ||
435 | url_map = Map([ | |
436 | Rule('/', subdomain='<username>', endpoint='user/homepage'), | |
437 | Rule('/stats', subdomain='<username>', endpoint='user/stats') | |
438 | ]) | |
439 | ||
440 | `methods` | |
441 | A sequence of http methods this rule applies to. If not specified, all | |
442 | methods are allowed. For example this can be useful if you want different | |
443 | endpoints for `POST` and `GET`. If methods are defined and the path | |
444 | matches but the method matched against is not in this list or in the | |
445 | list of another rule for that path the error raised is of the type | |
446 | `MethodNotAllowed` rather than `NotFound`. | |
447 | ||
448 | `strict_slashes` | |
449 | Override the `Map` setting for `strict_slashes` only for this rule. If | |
450 | not specified the `Map` setting is used. | |
451 | ||
452 | `build_only` | |
453 | Set this to true and the rule will never match but will create a URL | |
454 | that can be build. This is useful if you have resources on a subdomain | |
455 | or folder that are not handled by the WSGI application (like static data) | |
297 | 456 | """ |
298 | 457 | |
299 | 458 | def __init__(self, string, defaults=None, subdomain=None, methods=None, |
311 | 470 | if methods is None: |
312 | 471 | self.methods = None |
313 | 472 | else: |
314 | self.methods = m = [] | |
315 | for method in methods: | |
316 | m.append(method.upper()) | |
317 | self.methods.sort(lambda a, b: cmp(len(b), len(a))) | |
473 | self.methods = set([x.upper() for x in methods]) | |
318 | 474 | self.endpoint = endpoint |
319 | 475 | self.greediness = 0 |
320 | 476 | |
325 | 481 | self.arguments = set() |
326 | 482 | self._converters = {} |
327 | 483 | self._regex = None |
484 | self._weights = [] | |
485 | ||
486 | def empty(self): | |
487 | """ | |
488 | Return an unbound copy of this rule. This can be useful if you want | |
489 | to reuse an already bound URL for another map. | |
490 | """ | |
491 | return Rule(self.rule, self.defaults, self.subdomain, self.methods, | |
492 | self.build_only, self.endpoint, self.strict_slashes) | |
328 | 493 | |
329 | 494 | def get_rules(self, map): |
330 | 495 | yield self |
333 | 498 | """ |
334 | 499 | Bind the url to a map and create a regular expression based on |
335 | 500 | the information from the rule itself and the defaults from the map. |
501 | ||
502 | :internal: | |
336 | 503 | """ |
337 | 504 | if self.map is not None: |
338 | 505 | raise RuntimeError('url rule %r already bound to map %r' % |
351 | 518 | if converter is None: |
352 | 519 | regex_parts.append(re.escape(variable)) |
353 | 520 | self._trace.append((False, variable)) |
521 | self._weights.append(len(variable)) | |
354 | 522 | else: |
355 | 523 | convobj = get_converter(map, converter, arguments) |
356 | 524 | regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex)) |
357 | 525 | self._converters[variable] = convobj |
358 | 526 | self._trace.append((True, variable)) |
527 | self._weights.append(convobj.weight) | |
359 | 528 | self.arguments.add(str(variable)) |
360 | 529 | if convobj.is_greedy: |
361 | 530 | self.greediness += 1 |
362 | 531 | if not self.is_leaf: |
363 | 532 | self._trace.append((False, '/')) |
364 | 533 | |
365 | if self.methods is None: | |
366 | method_re = '[^>]*' | |
367 | else: | |
368 | method_re = '|'.join([re.escape(x) for x in self.methods]) | |
369 | ||
370 | 534 | if not self.build_only: |
371 | regex = r'^%s%s\(%s\)$' % ( | |
535 | regex = r'^%s%s$' % ( | |
372 | 536 | u''.join(regex_parts), |
373 | 537 | (not self.is_leaf or not self.strict_slashes) and \ |
374 | '(?<!/)(?P<__suffix__>/?)' or '', | |
375 | method_re | |
538 | '(?<!/)(?P<__suffix__>/?)' or '' | |
376 | 539 | ) |
377 | 540 | self._regex = re.compile(regex, re.UNICODE) |
378 | 541 | |
383 | 546 | |
384 | 547 | If the rule matches a dict with the converted values is returned, |
385 | 548 | otherwise the return value is `None`. |
549 | ||
550 | :internal: | |
386 | 551 | """ |
387 | 552 | if not self.build_only: |
388 | 553 | m = self._regex.search(path) |
415 | 580 | """ |
416 | 581 | Assembles the relative url for that rule and the subdomain. |
417 | 582 | If building doesn't work for some reasons `None` is returned. |
583 | ||
584 | :internal: | |
418 | 585 | """ |
419 | 586 | tmp = [] |
420 | 587 | processed = set(self.arguments) |
438 | 605 | return subdomain, url |
439 | 606 | |
440 | 607 | def provides_defaults_for(self, rule): |
441 | """Check if this rule has defaults for a given rule.""" | |
608 | """ | |
609 | Check if this rule has defaults for a given rule. | |
610 | ||
611 | :internal: | |
612 | """ | |
442 | 613 | return not self.build_only and self.defaults is not None and \ |
443 | 614 | self.endpoint == rule.endpoint and self != rule and \ |
444 | 615 | self.arguments == rule.arguments |
445 | 616 | |
446 | 617 | def suitable_for(self, values, method): |
447 | """Check if the dict of values has enough data for url generation.""" | |
618 | """ | |
619 | Check if the dict of values has enough data for url generation. | |
620 | ||
621 | :internal: | |
622 | """ | |
448 | 623 | if self.methods is not None and method not in self.methods: |
449 | 624 | return False |
450 | 625 | |
464 | 639 | return True |
465 | 640 | |
466 | 641 | def match_compare(self, other): |
467 | """Compare this object with another one for matching""" | |
642 | """ | |
643 | Compare this object with another one for matching. | |
644 | ||
645 | :internal: | |
646 | """ | |
647 | for sw, ow in izip(self._weights, other._weights): | |
648 | if sw > ow: | |
649 | return -1 | |
650 | elif sw < ow: | |
651 | return 1 | |
652 | if len(self._weights) > len(other._weights): | |
653 | return -1 | |
654 | if len(self._weights) < len(other._weights): | |
655 | return 1 | |
468 | 656 | if not other.arguments and self.arguments: |
469 | 657 | return 1 |
470 | 658 | elif other.arguments and not self.arguments: |
484 | 672 | return 1 |
485 | 673 | |
486 | 674 | def build_compare(self, other): |
487 | """Compare this object with another one for building.""" | |
675 | """ | |
676 | Compare this object with another one for building. | |
677 | ||
678 | :internal: | |
679 | """ | |
488 | 680 | if not other.arguments and self.arguments: |
489 | 681 | return -1 |
490 | 682 | elif other.arguments and not self.arguments: |
546 | 738 | """ |
547 | 739 | regex = '[^/]+' |
548 | 740 | is_greedy = False |
741 | weight = 100 | |
549 | 742 | |
550 | 743 | def __init__(self, map): |
551 | 744 | self.map = map |
554 | 747 | return value |
555 | 748 | |
556 | 749 | def to_url(self, value): |
557 | return quote(unicode(value).encode(self.map.charset)) | |
750 | return url_quote(value, self.map.charset) | |
558 | 751 | |
559 | 752 | |
560 | 753 | class UnicodeConverter(BaseConverter): |
561 | 754 | """ |
562 | The default converter for all URL parts. Matches one string without a | |
563 | slash in the part. Can also check for the length of that string. | |
755 | This converter is the default converter and accepts any string but | |
756 | only one one path segment. Thus the string can not include a slash. | |
757 | ||
758 | Supported arguments: | |
759 | ||
760 | - `minlength` - the minimum length of the string. must be greater | |
761 | than 1. | |
762 | - `maxlength` - the maximum length of the string. | |
763 | - `length` - the exact length of that string. | |
564 | 764 | """ |
565 | 765 | |
566 | 766 | def __init__(self, map, minlength=1, maxlength=None, length=None): |
579 | 779 | self.regex = '[^/]' + length |
580 | 780 | |
581 | 781 | |
782 | class AnyConverter(BaseConverter): | |
783 | """ | |
784 | Matches one of the items provided. Items can either be Python | |
785 | identifiers or unicode strings:: | |
786 | ||
787 | Rule('/<any(about, help, imprint, u"class"):page_name>') | |
788 | """ | |
789 | ||
790 | def __init__(self, map, *items): | |
791 | BaseConverter.__init__(self, map) | |
792 | self.regex = '(?:%s)' % '|'.join([re.escape(x) for x in items]) | |
793 | ||
794 | ||
582 | 795 | class PathConverter(BaseConverter): |
583 | 796 | """ |
584 | Matches a whole path (including slashes) | |
585 | """ | |
586 | regex = '[^/].*' | |
797 | Matches one of the items provided. Items can either be python | |
798 | identifiers or unicode strings:: | |
799 | ||
800 | Rule('/<any(about, help, imprint, u"class"):page_name>') | |
801 | """ | |
802 | regex = '[^/].*?' | |
587 | 803 | is_greedy = True |
804 | weight = 50 | |
588 | 805 | |
589 | 806 | |
590 | 807 | class NumberConverter(BaseConverter): |
591 | 808 | """ |
592 | 809 | Baseclass for `IntegerConverter` and `FloatConverter`. |
810 | ||
811 | :internal: | |
593 | 812 | """ |
594 | 813 | |
595 | 814 | def __init__(self, map, fixed_digits=0, min=None, max=None): |
616 | 835 | |
617 | 836 | class IntegerConverter(NumberConverter): |
618 | 837 | """ |
619 | Only accepts integers. | |
838 | This converter only accepts integer values:: | |
839 | ||
840 | Rule('/page/<int:page>') | |
841 | ||
842 | Supported arguments: | |
843 | ||
844 | - `fixed_digits` - the number of fixed digits in the URL. If you | |
845 | set this to ``4`` for example, the application will only match | |
846 | if the url looks like ``/0001/``. The default is | |
847 | variable length. | |
848 | - `min` - the minimal value. | |
849 | - `max` - the maximal value. | |
620 | 850 | """ |
621 | 851 | regex = r'\d+' |
622 | 852 | num_convert = int |
624 | 854 | |
625 | 855 | class FloatConverter(NumberConverter): |
626 | 856 | """ |
627 | Only accepts floats and integers. | |
857 | This converter only accepts floating point values:: | |
858 | ||
859 | Rule('/probability/<float:probability>') | |
860 | ||
861 | Supported arguments: | |
862 | ||
863 | - `min` - the minimal value. | |
864 | - `max` - the maximal value. | |
628 | 865 | """ |
629 | 866 | regex = r'\d+\.\d+' |
630 | 867 | num_convert = float |
635 | 872 | |
636 | 873 | class Map(object): |
637 | 874 | """ |
638 | The base class for all the url maps. | |
875 | The map class stores all the URL rules and some configuration | |
876 | parameters. Some of the configuration values are only stored on the | |
877 | `Map` instance since those affect all rules, others are just defaults | |
878 | and can be overridden for each rule. Note that you have to specify all | |
879 | arguments beside the `rules` as keywords arguments! | |
639 | 880 | """ |
640 | 881 | |
641 | 882 | def __init__(self, rules=None, default_subdomain='', charset='utf-8', |
642 | 883 | strict_slashes=True, redirect_defaults=True, |
643 | 884 | converters=None): |
644 | 885 | """ |
645 | `rules` | |
646 | sequence of url rules for this map. | |
647 | ||
648 | `default_subdomain` | |
649 | The default subdomain for rules without a subdomain defined. | |
650 | ||
651 | `charset` | |
652 | charset of the url. defaults to ``"utf-8"`` | |
653 | ||
654 | `strict_slashes` | |
655 | Take care of trailing slashes. | |
656 | ||
657 | `redirect_defaults` | |
658 | This will redirect to the default rule if it wasn't visited | |
659 | that way. This helps creating unique urls. | |
660 | ||
661 | `converters` | |
662 | A dict of converters that adds additional converters to the | |
663 | list of converters. If you redefine one converter this will | |
664 | override the original one. | |
886 | Initializes the new URL map. | |
887 | ||
888 | :param rules: sequence of url rules for this map. | |
889 | :param default_subdomain: The default subdomain for rules without a | |
890 | subdomain defined. | |
891 | :param charset: charset of the url. defaults to ``"utf-8"`` | |
892 | :param strict_slashes: Take care of trailing slashes. | |
893 | :param redirect_defaults: This will redirect to the default rule if it | |
894 | wasn't visited that way. This helps creating | |
895 | unique URLs. | |
896 | :param converters: A dict of converters that adds additional converters | |
897 | to the list of converters. If you redefine one | |
898 | converter this will override the original one. | |
665 | 899 | """ |
666 | 900 | self._rules = [] |
667 | 901 | self._rules_by_endpoint = {} |
718 | 952 | return self.add(rule) |
719 | 953 | |
720 | 954 | def bind(self, server_name, script_name=None, subdomain=None, |
721 | url_scheme='http', default_method='GET'): | |
722 | """ | |
723 | Return a new map adapter for this request. | |
955 | url_scheme='http', default_method='GET', path_info=None): | |
956 | """ | |
957 | Return a new `MapAdapter` with the details specified to the call. | |
958 | Note that `script_name` will default to ``'/'`` if not further | |
959 | specified or `None`. The `server_name` at least is a requirement | |
960 | because the HTTP RFC requires absolute URLs for redirects and so all | |
961 | redirect exceptions raised by Werkzeug will contain the full canonical | |
962 | URL. | |
963 | ||
964 | If no path_info is passed to match() it will use the default path | |
965 | info passed to bind. While this doesn't really make sense for | |
966 | manual bind calls, it's useful if you bind a map to a WSGI | |
967 | environment which already contains the path info. | |
968 | ||
969 | `subdomain` will default to the `default_subdomain` for this map if | |
970 | no defined. If there is no `default_subdomain` you cannot use the | |
971 | subdomain feature. | |
724 | 972 | """ |
725 | 973 | if subdomain is None: |
726 | 974 | subdomain = self.default_subdomain |
727 | 975 | if script_name is None: |
728 | 976 | script_name = '/' |
729 | 977 | return MapAdapter(self, server_name, script_name, subdomain, |
730 | url_scheme, default_method) | |
978 | url_scheme, path_info, default_method) | |
731 | 979 | |
732 | 980 | def bind_to_environ(self, environ, server_name=None, subdomain=None, |
733 | 981 | calculate_subdomain=False): |
734 | 982 | """ |
735 | Like `bind` but the required information are pulled from the | |
736 | WSGI environment provided where possible. For some information | |
737 | this won't work (subdomains), if you want that feature you have | |
738 | to provide the subdomain with the `subdomain` variable. | |
983 | Like `bind` but you can pass it an WSGI environment and it will fetch | |
984 | the information from that directory. Note that because of limitations | |
985 | in the protocol there is no way to get the current subdomain and real | |
986 | `server_name` from the environment. If you don't provide it, Werkzeug | |
987 | will use `SERVER_NAME` and `SERVER_PORT` (or `HTTP_HOST` if provided) | |
988 | as used `server_name` with disabled subdomain feature. | |
739 | 989 | |
740 | 990 | If `subdomain` is `None` but an environment and a server name is |
741 | 991 | provided it will calculate the current subdomain automatically. |
742 | 992 | Example: `server_name` is ``'example.com'`` and the `SERVER_NAME` |
743 | 993 | in the wsgi `environ` is ``'staging.dev.example.com'`` the calculated |
744 | 994 | subdomain will be ``'staging.dev'``. |
745 | """ | |
995 | ||
996 | If the object passed as environ as an environ attribute, the value of | |
997 | this attribute is used instead. This allows you to pass request | |
998 | objects. Additionally `PATH_INFO` added as a default ot the | |
999 | `MapAdapter` so that you don't have to pass the path info to the | |
1000 | match method. | |
1001 | """ | |
1002 | if hasattr(environ, 'environ'): | |
1003 | environ = environ.environ | |
746 | 1004 | if server_name is None: |
747 | 1005 | if 'HTTP_HOST' in environ: |
748 | 1006 | server_name = environ['HTTP_HOST'] |
756 | 1014 | real_server_name = server_name.split(':', 1)[0].split('.') |
757 | 1015 | offset = -len(real_server_name) |
758 | 1016 | if cur_server_name[offset:] != real_server_name: |
759 | raise ValueError('the server name provided (%r) does not match the ' | |
760 | 'server name from the WSGI environment (%r)' % | |
1017 | raise ValueError('the server name provided (%r) does not ' | |
1018 | 'match the server name from the WSGI ' | |
1019 | 'environment (%r)' % | |
761 | 1020 | (environ['SERVER_NAME'], server_name)) |
762 | 1021 | subdomain = '.'.join(filter(None, cur_server_name[:offset])) |
763 | return Map.bind(self, server_name, environ.get('SCRIPT_NAME'), subdomain, | |
764 | environ['wsgi.url_scheme'], environ['REQUEST_METHOD']) | |
1022 | return Map.bind(self, server_name, environ.get('SCRIPT_NAME'), | |
1023 | subdomain, environ['wsgi.url_scheme'], | |
1024 | environ['REQUEST_METHOD'], environ.get('PATH_INFO')) | |
765 | 1025 | |
766 | 1026 | def update(self): |
767 | 1027 | """ |
782 | 1042 | """ |
783 | 1043 | |
784 | 1044 | def __init__(self, map, server_name, script_name, subdomain, |
785 | url_scheme, default_method): | |
1045 | url_scheme, path_info, default_method): | |
786 | 1046 | self.map = map |
787 | 1047 | self.server_name = server_name |
788 | 1048 | if not script_name.endswith('/'): |
790 | 1050 | self.script_name = script_name |
791 | 1051 | self.subdomain = subdomain |
792 | 1052 | self.url_scheme = url_scheme |
1053 | self.path_info = path_info or u'' | |
793 | 1054 | self.default_method = default_method |
794 | 1055 | |
795 | def dispatch(self, view_func, path_info, method=None): | |
1056 | def dispatch(self, view_func, path_info=None, method=None, | |
1057 | catch_http_exceptions=False): | |
796 | 1058 | """ |
797 | 1059 | Does the complete dispatching process. `view_func` is called with |
798 | 1060 | the endpoint and a dict with the values for the view. It should |
799 | 1061 | look up the view function, call it, and return a response object |
800 | or WSGI application. http exceptions are not catched. | |
1062 | or WSGI application. http exceptions are not catched by default | |
1063 | so that applications can display nicer error messages by just | |
1064 | catching them by hand. If you want to stick with the default | |
1065 | error messages you can pass it ``catch_http_exceptions=True`` and | |
1066 | it will catch the http exceptions. | |
1067 | ||
1068 | Here a small example for the dispatch usage:: | |
1069 | ||
1070 | from werkzeug import Request, Response, responder | |
1071 | from werkzeug.routing import Map, Rule | |
1072 | ||
1073 | def on_index(request): | |
1074 | return Response('Hello from the index') | |
1075 | ||
1076 | url_map = Map([Rule('/', endpoint='index')]) | |
1077 | views = {'index': on_index} | |
1078 | ||
1079 | @responder | |
1080 | def application(environ, start_response): | |
1081 | request = Request(environ) | |
1082 | urls = url_map.bind_to_environ(environ) | |
1083 | return urls.dispatch(lambda e, v: views[e](request, **v), | |
1084 | catch_http_exceptions=True) | |
801 | 1085 | """ |
802 | 1086 | try: |
803 | 1087 | endpoint, args = self.match(path_info, method) |
804 | 1088 | except RequestRedirect, e: |
805 | return e | |
806 | return view_func(endpoint, args) | |
807 | ||
808 | def match(self, path_info, method=None): | |
809 | """ | |
810 | Match a given path_info, script_name and subdomain against the | |
811 | known rules. If the subdomain is not given it defaults to the | |
812 | default subdomain of the map which is usally `www`. Thus if you | |
813 | don't define it anywhere you can safely ignore it. | |
1089 | return e.get_response() | |
1090 | try: | |
1091 | return view_func(endpoint, args) | |
1092 | except HTTPException, e: | |
1093 | if catch_http_exceptions: | |
1094 | return e.get_response() | |
1095 | raise | |
1096 | ||
1097 | def match(self, path_info=None, method=None): | |
1098 | """ | |
1099 | The usage is simple: you just pass the match method the current path | |
1100 | info as well as the method (which defaults to `GET`). The following | |
1101 | things can then happen: | |
1102 | ||
1103 | - you receive a `NotFound` exception that indicates that no URL is | |
1104 | matching. A `NotFound` exception is also a WSGI application you | |
1105 | can call to get a default page not found page (happens to be the | |
1106 | same object as `werkzeug.exceptions.NotFound`) | |
1107 | ||
1108 | - you receive a `MethodNotAllowed` exception that indicates that there | |
1109 | is a match for this URL but non for the current request method. | |
1110 | This is useful for RESTful applications. | |
1111 | ||
1112 | - you receive a `RequestRedirect` exception with a `new_url` | |
1113 | attribute. This exception is used to notify you about a request | |
1114 | Werkzeug requests by your WSGI application. This is for example the | |
1115 | case if you request ``/foo`` although the correct URL is ``/foo/`` | |
1116 | You can use the `RequestRedirect` instance as response-like object | |
1117 | similar to all other subclasses of `HTTPException`. | |
1118 | ||
1119 | - you get a tuple in the form ``(endpoint, arguments)`` when there is | |
1120 | a match. | |
1121 | ||
1122 | If the path info is not passed to the match method the default path | |
1123 | info of the map is used (defaults to the root URL if not defined | |
1124 | explicitly). | |
1125 | ||
1126 | All of the exceptions raised are subclasses of `HTTPException` so they | |
1127 | can be used as WSGI responses. The will all render generic error or | |
1128 | redirect pages. | |
1129 | ||
1130 | Here is a small example for matching: | |
1131 | ||
1132 | >>> from werkzeug.routing import Map, Rule | |
1133 | >>> m = Map([ | |
1134 | ... Rule('/', endpoint='index'), | |
1135 | ... Rule('/downloads/', endpoint='downloads/index'), | |
1136 | ... Rule('/downloads/<int:id>', endpoint='downloads/show') | |
1137 | ... ]) | |
1138 | >>> urls = m.bind("example.com", "/") | |
1139 | >>> urls.match("/", "GET") | |
1140 | ('index', {}) | |
1141 | >>> urls.match("/downloads/42") | |
1142 | ('downloads/show', {'id': 42}) | |
1143 | ||
1144 | And here is what happens on redirect and missing URLs: | |
1145 | ||
1146 | >>> urls.match("/downloads") | |
1147 | Traceback (most recent call last): | |
1148 | ... | |
1149 | werkzeug.routing.RequestRedirect: http://example.com/downloads/ | |
1150 | >>> urls.match("/missing") | |
1151 | Traceback (most recent call last): | |
1152 | ... | |
1153 | werkzeug.routing.NotFound: /missing | |
814 | 1154 | """ |
815 | 1155 | self.map.update() |
1156 | if path_info is None: | |
1157 | path_info = self.path_info | |
816 | 1158 | if not isinstance(path_info, unicode): |
817 | 1159 | path_info = path_info.decode(self.map.charset, 'ignore') |
818 | path = u'%s|/%s(%s)' % ( | |
819 | self.subdomain, | |
820 | path_info.lstrip('/'), | |
821 | (method or self.default_method).upper() | |
822 | ) | |
1160 | method = (method or self.default_method).upper() | |
1161 | path = u'%s|/%s' % (self.subdomain, path_info.lstrip('/')) | |
1162 | have_match_for = set() | |
823 | 1163 | for rule in self.map._rules: |
824 | 1164 | try: |
825 | 1165 | rv = rule.match(path) |
832 | 1172 | path_info.lstrip('/') |
833 | 1173 | ))) |
834 | 1174 | if rv is None: |
1175 | continue | |
1176 | if rule.methods is not None and method not in rule.methods: | |
1177 | have_match_for.update(rule.methods) | |
835 | 1178 | continue |
836 | 1179 | if self.map.redirect_defaults: |
837 | 1180 | for r in self.map._rules_by_endpoint[rule.endpoint]: |
847 | 1190 | path.lstrip('/') |
848 | 1191 | ))) |
849 | 1192 | return rule.endpoint, rv |
1193 | if have_match_for: | |
1194 | raise MethodNotAllowed(valid_methods=list(have_match_for)) | |
850 | 1195 | raise NotFound() |
851 | 1196 | |
1197 | def test(self, path_info=None, method=None): | |
1198 | """ | |
1199 | Test if a rule would match. Works like `match` but returns `True` if | |
1200 | the URL matches, or `False` if it does not exist. | |
1201 | """ | |
1202 | try: | |
1203 | self.match(path_info, method) | |
1204 | except RequestRedirect: | |
1205 | pass | |
1206 | except NotFound: | |
1207 | return False | |
1208 | return True | |
1209 | ||
852 | 1210 | def build(self, endpoint, values=None, method=None, force_external=False): |
853 | 1211 | """ |
854 | Build a new url hostname relative to the current one. If you | |
855 | reference a resource on another subdomain the hostname is added | |
856 | automatically. You can force external urls by setting | |
857 | `force_external` to `True`. | |
1212 | Building URLs works pretty much the other way round. Instead of | |
1213 | `match` you call `build` and pass it the endpoint and a dict of | |
1214 | arguments for the placeholders. | |
1215 | ||
1216 | The `build` function also accepts an argument called `force_external` | |
1217 | which, if you set it to `True` will force external URLs. Per default | |
1218 | external URLs (include the server name) will only be used if the | |
1219 | target URL is on a | |
1220 | different subdomain. | |
1221 | ||
1222 | With the same map as in the example above this code generates some | |
1223 | target URLs: | |
1224 | ||
1225 | >>> urls.build("index", {}) | |
1226 | '/' | |
1227 | >>> urls.build("downloads/show", {'id': 42}) | |
1228 | '/downloads/42' | |
1229 | >>> urls.build("downloads/show", {'id': 42}, force_external=True) | |
1230 | 'http://example.com/downloads/42' | |
1231 | ||
1232 | Because URLs cannot contain non ASCII data you will always get | |
1233 | bytestrings back. Non ASCII characters are urlencoded with the | |
1234 | charset defined on the map instance. | |
1235 | ||
1236 | Additional values are converted to unicode and appended to the URL as | |
1237 | URL querystring parameters: | |
1238 | ||
1239 | >>> urls.build("index", {'q': 'My Searchstring'}) | |
1240 | '/?q=My+Searchstring' | |
1241 | ||
1242 | If a rule does not exist when building a `BuildError` exception is | |
1243 | raised. | |
1244 | ||
1245 | The build method accepts an argument called `method` which allows you | |
1246 | to specify the method you want to have an URL builded for if you have | |
1247 | different methods for the same endpoint specified. | |
858 | 1248 | """ |
859 | 1249 | self.map.update() |
860 | 1250 | method = method or self.default_method |
869 | 1259 | if rv is not None: |
870 | 1260 | break |
871 | 1261 | else: |
872 | raise BuildError(endpoint, values) | |
1262 | raise BuildError(endpoint, values, method) | |
873 | 1263 | subdomain, path = rv |
874 | 1264 | if not force_external and subdomain == self.subdomain: |
875 | 1265 | return str(urljoin(self.script_name, path.lstrip('/'))) |
882 | 1272 | )) |
883 | 1273 | |
884 | 1274 | |
1275 | #: the default converter mapping for the map. | |
885 | 1276 | DEFAULT_CONVERTERS = { |
886 | 1277 | 'default': UnicodeConverter, |
887 | 1278 | 'string': UnicodeConverter, |
1279 | 'any': AnyConverter, | |
888 | 1280 | 'path': PathConverter, |
889 | 1281 | 'int': IntegerConverter, |
890 | 1282 | 'float': FloatConverter |
0 | 0 | # -*- coding: utf-8 -*- |
1 | """ | |
1 | r''' | |
2 | 2 | werkzeug.script |
3 | 3 | ~~~~~~~~~~~~~~~ |
4 | 4 | |
5 | This module provides classes that simplifies the creation of shell | |
6 | scripts. The `Script` class is very basic and does not contain any | |
7 | default actions. The `ManagementScript` class however includes some | |
8 | common actions such as running a WSGI server and starting a python | |
9 | shell. | |
10 | ||
11 | This module is quite magical because it uses frame introspection to | |
12 | locate the action callbacks. You should only use it for small | |
13 | manage scripts and similar things. | |
14 | ||
15 | ||
16 | :copyright: 2007 by Armin Ronacher. | |
5 | Most of the time you have recurring tasks while writing an application | |
6 | such as starting up an interactive python interpreter with some prefilled | |
7 | imports, starting the development server, initializing the database or | |
8 | something similar. | |
9 | ||
10 | For that purpose werkzeug provides the `werkzeug.script` module which | |
11 | helps you writing such scripts. | |
12 | ||
13 | ||
14 | Basic Usage | |
15 | ----------- | |
16 | ||
17 | The following snippet is roughly the same in every werkzeug script:: | |
18 | ||
19 | #!/usr/bin/env python | |
20 | # -*- coding: utf-8 -*- | |
21 | from werkzeug import script | |
22 | ||
23 | # actions go here | |
24 | ||
25 | if __name__ == '__main__': | |
26 | script.run() | |
27 | ||
28 | Starting this script now does nothing because no actions are defined. | |
29 | An action is a function in the same module starting with ``"action_"`` | |
30 | which takes a number of arguments where every argument has a default. The | |
31 | type of the default value specifies the type of the argument. | |
32 | ||
33 | Arguments can then be passed by position or using ``--name=value`` from | |
34 | the shell. | |
35 | ||
36 | Because a runserver and shell command is pretty common there are two | |
37 | factory functions that create such commands:: | |
38 | ||
39 | def make_app(): | |
40 | from yourapplication import YourApplication | |
41 | return YourApplication(...) | |
42 | ||
43 | action_runserver = script.make_runserver(make_app, use_reloader=True) | |
44 | action_shell = script.make_shell(lambda: {'app': make_app()}) | |
45 | ||
46 | ||
47 | Using The Scripts | |
48 | ----------------- | |
49 | ||
50 | The script from above can be used like this from the shell now: | |
51 | ||
52 | .. sourcecode:: text | |
53 | ||
54 | $ ./manage.py --help | |
55 | $ ./manage.py runserver localhost 8080 --debugger --no-reloader | |
56 | $ ./manage.py runserver -p 4000 | |
57 | $ ./manage.py shell | |
58 | ||
59 | As you can see it's possible to pass parameters as positional arguments | |
60 | or as named parameters, pretty much like Python function calls. | |
61 | ||
62 | ||
63 | :copyright: 2007-2008 by Armin Ronacher, Thomas Johansson. | |
17 | 64 | :license: BSD, see LICENSE for more details. |
18 | """ | |
65 | ''' | |
19 | 66 | import sys |
20 | 67 | import inspect |
21 | 68 | import getopt |
69 | from os.path import basename | |
22 | 70 | try: |
23 | 71 | set = set |
24 | 72 | except NameError: |
41 | 89 | } |
42 | 90 | |
43 | 91 | |
44 | def run(namespace=None, action_prefix='action_'): | |
92 | def run(namespace=None, action_prefix='action_', args=None): | |
45 | 93 | """ |
46 | 94 | Run the script. Participating actions are looked up in the callers |
47 | 95 | namespace if no namespace is given, otherwise in the dict provided. |
51 | 99 | """ |
52 | 100 | if namespace is None: |
53 | 101 | namespace = sys._getframe(1).f_locals |
54 | actions = {} | |
55 | for key, value in namespace.iteritems(): | |
56 | if key.startswith(action_prefix): | |
57 | actions[key[len(action_prefix):]] = analyse_action(value) | |
58 | ||
59 | args = sys.argv[1:] | |
102 | actions = find_actions(namespace, action_prefix) | |
103 | ||
104 | if args is None: | |
105 | args = sys.argv[1:] | |
60 | 106 | if not args or args[0] in ('-h', '--help'): |
61 | 107 | return print_usage(actions) |
62 | 108 | elif args[0] not in actions: |
72 | 118 | real_arg = arg.replace('-', '_') |
73 | 119 | converter = converters[option_type] |
74 | 120 | if shortcut: |
75 | formatstring += shortcut + ':' | |
121 | formatstring += shortcut | |
122 | if not isinstance(default, bool): | |
123 | formatstring += ':' | |
76 | 124 | key_to_arg['-' + shortcut] = real_arg |
77 | long_options.append(arg + '=') | |
125 | long_options.append(isinstance(default, bool) and arg or arg + '=') | |
78 | 126 | key_to_arg['--' + arg] = real_arg |
79 | 127 | key_to_arg[idx] = real_arg |
80 | 128 | conv[real_arg] = converter |
101 | 149 | arg = key_to_arg[key] |
102 | 150 | if arg in specified_arguments: |
103 | 151 | fail('Argument \'%s\' is specified twice' % arg) |
152 | if arg.startswith('no_'): | |
153 | value = 'no' | |
154 | elif not value: | |
155 | value = 'yes' | |
104 | 156 | try: |
105 | 157 | arguments[arg] = conv[arg](value) |
106 | 158 | except ValueError: |
107 | 159 | fail('Invalid value for \'%s\': %s' % (key, value)) |
108 | 160 | |
161 | newargs = {} | |
162 | for k, v in arguments.iteritems(): | |
163 | newargs[k.startswith('no_') and k[3:] or k] = v | |
164 | arguments = newargs | |
109 | 165 | return func(**arguments) |
110 | 166 | |
111 | 167 | |
115 | 171 | sys.exit(code) |
116 | 172 | |
117 | 173 | |
174 | def find_actions(namespace, action_prefix): | |
175 | """Find all the actions in the namespace.""" | |
176 | actions = {} | |
177 | for key, value in namespace.iteritems(): | |
178 | if key.startswith(action_prefix): | |
179 | actions[key[len(action_prefix):]] = analyse_action(value) | |
180 | return actions | |
181 | ||
182 | ||
118 | 183 | def print_usage(actions): |
119 | 184 | """Print the usage information. (Help screen)""" |
120 | 185 | actions = actions.items() |
121 | 186 | actions.sort() |
122 | print 'usage: %s <action> [<options>]' % sys.argv[0] | |
123 | print ' %s --help' % sys.argv[0] | |
187 | print 'usage: %s <action> [<options>]' % basename(sys.argv[0]) | |
188 | print ' %s --help' % basename(sys.argv[0]) | |
124 | 189 | |
125 | 190 | print 'actions:' |
126 | 191 | for name, (func, doc, arguments) in actions: |
129 | 194 | print ' %s' % line |
130 | 195 | if arguments: |
131 | 196 | |
132 | for arg, shortcut, default, type in arguments: | |
133 | print ' %-30s%-10s%s' % ( | |
134 | (shortcut and '-%s, ' % shortcut or '') + '--' + arg, | |
135 | type, | |
136 | default | |
137 | ) | |
197 | for arg, shortcut, default, argtype in arguments: | |
198 | if isinstance(default, bool): | |
199 | print ' %s' % ( | |
200 | (shortcut and '-%s, ' % shortcut or '') + '--' + arg | |
201 | ) | |
202 | else: | |
203 | print ' %-30s%-10s%s' % ( | |
204 | (shortcut and '-%s, ' % shortcut or '') + '--' + arg, | |
205 | argtype, default | |
206 | ) | |
138 | 207 | |
139 | 208 | |
140 | 209 | |
157 | 226 | else: |
158 | 227 | shortcut, default = definition |
159 | 228 | argument_type = argument_types[type(default)] |
229 | if isinstance(default, bool) and default is True: | |
230 | arg = 'no-' + arg | |
160 | 231 | arguments.append((arg.replace('_', '-'), shortcut, |
161 | 232 | default, argument_type)) |
162 | 233 | return func, description, arguments |
169 | 240 | """ |
170 | 241 | if banner is None: |
171 | 242 | banner = 'Interactive Werkzeug Shell' |
172 | def action(use_ipython=use_ipython): | |
243 | def action(ipython=use_ipython): | |
173 | 244 | """Start a new interactive python session.""" |
174 | 245 | namespace = init_func() |
175 | if use_ipython: | |
246 | if ipython: | |
176 | 247 | try: |
177 | 248 | import IPython |
178 | 249 | except ImportError: |
193 | 264 | Returns an action callback that spawns a new wsgiref server. |
194 | 265 | """ |
195 | 266 | def action(hostname=('h', hostname), port=('p', port), |
196 | use_reloader=use_reloader, use_debugger=use_debugger, | |
197 | use_evalex=use_evalex, threaded=threaded, processes=processes): | |
267 | reloader=use_reloader, debugger=use_debugger, | |
268 | evalex=use_evalex, threaded=threaded, processes=processes): | |
198 | 269 | """Start a new development server.""" |
199 | 270 | from werkzeug.serving import run_simple |
200 | 271 | app = app_factory() |
201 | if use_debugger: | |
272 | if debugger: | |
202 | 273 | from werkzeug.debug import DebuggedApplication |
203 | app = DebuggedApplication(app, use_evalex) | |
204 | run_simple(hostname, port, app, use_reloader, None, threaded, | |
274 | app = DebuggedApplication(app, evalex) | |
275 | run_simple(hostname, port, app, reloader, None, 1, threaded, | |
205 | 276 | processes) |
206 | 277 | return action |
2 | 2 | werkzeug.serving |
3 | 3 | ~~~~~~~~~~~~~~~~ |
4 | 4 | |
5 | This module wraps the `wsgiref` module so that it reloads code | |
6 | automatically. Works with any WSGI application but it won't help in | |
7 | non `wsgiref` environments. Use it only for development. | |
8 | ||
9 | Usage:: | |
10 | ||
11 | from werkzeug.serving import run_simple | |
5 | There are many ways to serve a WSGI application. While you're developing | |
6 | it you usually don't want a full blown webserver like Apache but a simple | |
7 | standalone one. With Python 2.5 onwards there is the `wsgiref`_ server in | |
8 | the standard library. If you're using older versions of Python you can | |
9 | download the package from the cheeseshop. | |
10 | ||
11 | However there are some caveats. Sourcecode won't reload itself when | |
12 | changed and each time you kill the server using ``^C`` you get an | |
13 | `KeyboardInterrupt` error. While the latter is easy to solve the first | |
14 | one can be a pain in the ass in some situations. | |
15 | ||
16 | Because of that Werkzeug ships a small wrapper over `wsgiref` that spawns | |
17 | the WSGI application in a subprocess and automatically reloads the | |
18 | application if a module was changed. | |
19 | ||
20 | The easiest way is creating a small ``start-myproject.py`` that runs the | |
21 | application:: | |
22 | ||
23 | #!/usr/bin/env python | |
24 | # -*- coding: utf-8 -*- | |
12 | 25 | from myproject import make_app |
13 | run_simple('localhost', 8080, make_app()) | |
14 | ||
15 | :copyright: 2007 by Armin Ronacher. | |
26 | from werkzeug import run_simple | |
27 | ||
28 | app = make_app(...) | |
29 | run_simple('localhost', 8080, app, use_reloader=True) | |
30 | ||
31 | You can also pass it a `extra_files` keyword argument with a list of | |
32 | additional files (like configuration files) you want to observe. | |
33 | ||
34 | For bigger applications you should consider using `werkzeug.script` | |
35 | instead of a simple start file. | |
36 | ||
37 | .. _wsgiref: http://cheeseshop.python.org/pypi/wsgiref | |
38 | ||
39 | ||
40 | :copyright: 2007-2008 by Armin Ronacher. | |
16 | 41 | :license: BSD, see LICENSE for more details. |
17 | 42 | """ |
18 | 43 | import os |
20 | 45 | import sys |
21 | 46 | import time |
22 | 47 | import thread |
23 | from wsgiref.simple_server import ServerHandler, WSGIRequestHandler, \ | |
24 | WSGIServer | |
48 | from itertools import chain | |
49 | from werkzeug.utils import _log | |
50 | try: | |
51 | from wsgiref.simple_server import ServerHandler, WSGIRequestHandler, \ | |
52 | WSGIServer | |
53 | have_wsgiref = True | |
54 | except ImportError: | |
55 | have_wsgiref = False | |
25 | 56 | from SocketServer import ThreadingMixIn, ForkingMixIn |
26 | 57 | |
27 | 58 | |
28 | class BaseRequestHandler(WSGIRequestHandler): | |
29 | """ | |
30 | Subclass of the normal request handler that thinks it is | |
31 | threaded or something like that. The default wsgiref handler | |
32 | has wrong information so we need this class. | |
33 | """ | |
34 | multithreaded = False | |
35 | multiprocess = False | |
36 | _handler_class = None | |
37 | ||
38 | def get_handler(self): | |
39 | handler = self._handler_class | |
40 | if handler is None: | |
41 | class handler(ServerHandler): | |
42 | wsgi_multithread = self.multithreaded | |
43 | wsgi_multiprocess = self.multiprocess | |
44 | self._handler_class = handler | |
45 | ||
46 | rv = handler(self.rfile, self.wfile, self.get_stderr(), | |
47 | self.get_environ()) | |
48 | rv.request_handler = self | |
49 | return rv | |
50 | ||
51 | def handle(self): | |
52 | self.raw_requestline = self.rfile.readline() | |
53 | if self.parse_request(): | |
54 | self.get_handler().run(self.server.get_app()) | |
55 | ||
56 | ||
57 | def make_server(host, port, app=None, threaded=False, processes=1): | |
59 | if have_wsgiref: | |
60 | class BaseRequestHandler(WSGIRequestHandler): | |
61 | """ | |
62 | Subclass of the normal request handler that thinks it is | |
63 | threaded or something like that. The default wsgiref handler | |
64 | has wrong information so we need this class. | |
65 | """ | |
66 | multithreaded = False | |
67 | multiprocess = False | |
68 | _handler_class = None | |
69 | ||
70 | def get_handler(self): | |
71 | handler = self._handler_class | |
72 | if handler is None: | |
73 | class handler(ServerHandler): | |
74 | wsgi_multithread = self.multithreaded | |
75 | wsgi_multiprocess = self.multiprocess | |
76 | self._handler_class = handler | |
77 | ||
78 | rv = handler(self.rfile, self.wfile, self.get_stderr(), | |
79 | self.get_environ()) | |
80 | rv.request_handler = self | |
81 | return rv | |
82 | ||
83 | def handle(self): | |
84 | self.raw_requestline = self.rfile.readline() | |
85 | if self.parse_request(): | |
86 | self.get_handler().run(self.server.get_app()) | |
87 | ||
88 | def log_request(self, code='-', size='-'): | |
89 | _log('info', '%s -- [%s] %s %s', | |
90 | self.address_string(), | |
91 | self.requestline, | |
92 | code, | |
93 | size | |
94 | ) | |
95 | ||
96 | def log_error(self, format, *args): | |
97 | _log('error', 'Error: %s', format % args) | |
98 | ||
99 | def log_message(self, format, *args): | |
100 | _log('info', format, args) | |
101 | ||
102 | ||
103 | def make_server(host, port, app=None, threaded=False, processes=1, | |
104 | request_handler=None): | |
58 | 105 | """ |
59 | 106 | Create a new wsgiref server that is either threaded, or forks |
60 | 107 | or just processes one request after another. |
61 | 108 | """ |
109 | if not have_wsgiref: | |
110 | raise RuntimeError('All the Werkzeug serving features require ' | |
111 | 'an installed wsgiref library.') | |
112 | request_handler = request_handler or BaseRequestHandler | |
62 | 113 | if threaded and processes > 1: |
63 | 114 | raise ValueError("cannot have a multithreaded and " |
64 | 115 | "multi process server.") |
65 | 116 | elif threaded: |
66 | class handler(BaseRequestHandler): | |
117 | class request_handler(request_handler): | |
67 | 118 | multithreaded = True |
68 | 119 | class server(ThreadingMixIn, WSGIServer): |
69 | 120 | pass |
70 | 121 | elif processes > 1: |
71 | class handler(BaseRequestHandler): | |
122 | class request_handler(request_handler): | |
72 | 123 | multiprocess = True |
73 | 124 | max_children = processes - 1 |
74 | 125 | class server(ForkingMixIn, WSGIServer): |
75 | 126 | pass |
76 | 127 | else: |
77 | handler = BaseRequestHandler | |
78 | 128 | server = WSGIServer |
79 | srv = server((host, port), handler) | |
129 | srv = server((host, port), request_handler) | |
80 | 130 | srv.set_app(app) |
81 | 131 | return srv |
82 | 132 | |
83 | 133 | |
84 | def reloader_loop(extra_files): | |
134 | def reloader_loop(extra_files=None, interval=1): | |
85 | 135 | """When this function is run from the main thread, it will force other |
86 | 136 | threads to exit when any modules currently loaded change. |
87 | 137 | |
138 | Copyright notice. This function is based on the autoreload.py from | |
139 | the CherryPy trac which originated from WSGIKit which is now dead. | |
140 | ||
88 | 141 | :param extra_files: a list of additional files it should watch. |
89 | 142 | """ |
143 | def iter_module_files(): | |
144 | for module in sys.modules.values(): | |
145 | filename = getattr(module, '__file__', None) | |
146 | if filename: | |
147 | while not os.path.isfile(filename): | |
148 | filename = os.path.dirname(filename) | |
149 | if not filename: | |
150 | break | |
151 | else: | |
152 | if filename[-4:] in ('.pyc', '.pyo'): | |
153 | filename = filename[:-1] | |
154 | yield filename | |
155 | ||
90 | 156 | mtimes = {} |
91 | while True: | |
92 | for filename in filter(None, [getattr(module, '__file__', None) | |
93 | for module in sys.modules.values()] + | |
94 | extra_files): | |
95 | while not os.path.isfile(filename): | |
96 | filename = os.path.dirname(filename) | |
97 | if not filename: | |
98 | break | |
99 | if not filename: | |
100 | continue | |
101 | ||
102 | if filename[-4:] in ('.pyc', '.pyo'): | |
103 | filename = filename[:-1] | |
104 | ||
157 | while 1: | |
158 | for filename in chain(iter_module_files(), extra_files or ()): | |
105 | 159 | try: |
106 | 160 | mtime = os.stat(filename).st_mtime |
107 | 161 | except OSError: |
108 | 162 | continue |
109 | 163 | |
110 | if filename not in mtimes: | |
164 | old_time = mtimes.get(filename) | |
165 | if old_time is None: | |
111 | 166 | mtimes[filename] = mtime |
112 | 167 | continue |
113 | if mtime > mtimes[filename]: | |
168 | elif mtime > old_time: | |
169 | _log('info', ' * Detected change in %r, reloading' % filename) | |
114 | 170 | sys.exit(3) |
115 | time.sleep(1) | |
171 | time.sleep(interval) | |
116 | 172 | |
117 | 173 | |
118 | 174 | def restart_with_reloader(): |
121 | 177 | but running the reloader thread. |
122 | 178 | """ |
123 | 179 | while 1: |
124 | print '* Restarting with reloader...' | |
180 | _log('info', ' * Restarting with reloader...') | |
125 | 181 | args = [sys.executable] + sys.argv |
126 | 182 | if sys.platform == 'win32': |
127 | 183 | args = ['"%s"' % arg for arg in args] |
132 | 188 | return exit_code |
133 | 189 | |
134 | 190 | |
135 | def run_with_reloader(main_func, extra_watch): | |
191 | def run_with_reloader(main_func, extra_files=None, interval=1): | |
136 | 192 | """ |
137 | 193 | Run the given function in an independent python interpreter. |
138 | 194 | """ |
139 | 195 | if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': |
140 | 196 | thread.start_new_thread(main_func, ()) |
141 | 197 | try: |
142 | reloader_loop(extra_watch) | |
198 | reloader_loop(extra_files, interval) | |
143 | 199 | except KeyboardInterrupt: |
144 | 200 | return |
145 | 201 | try: |
149 | 205 | |
150 | 206 | |
151 | 207 | def run_simple(hostname, port, application, use_reloader=False, |
152 | extra_files=None, threaded=False, processes=1): | |
153 | """ | |
154 | Start an application using wsgiref and with an optional reloader. | |
208 | extra_files=None, reloader_interval=1, threaded=False, | |
209 | processes=1, request_handler=None): | |
210 | """ | |
211 | Start an application using wsgiref and with an optional reloader. This | |
212 | wraps `wsgiref` to fix the wrong default reporting of the multithreaded | |
213 | WSGI variable and adds optional multithreading and fork support. | |
214 | ||
215 | :param hostname: The host for the application. eg: ``'localhost'`` | |
216 | :param port: The port for the server. eg: ``8080`` | |
217 | :param application: the WSGI application to execute | |
218 | :param use_reloader: should the server automatically restart the python | |
219 | process if modules were changed? | |
220 | :param extra_files: a list of files the reloader should listen for | |
221 | additionally to the modules. For example configuration | |
222 | files. | |
223 | :param reloader_interval: the interval for the reloader in seconds. | |
224 | :param threaded: should the process handle each request in a separate | |
225 | thread? | |
226 | :param processes: number of processes to spawn. | |
227 | :param request_handler: optional parameter that can be used to replace | |
228 | the default wsgiref request handler. Have a look | |
229 | at the `werkzeug.serving` sourcecode for more | |
230 | details. | |
155 | 231 | """ |
156 | 232 | def inner(): |
157 | 233 | srv = make_server(hostname, port, application, threaded, |
158 | processes) | |
234 | processes, request_handler) | |
159 | 235 | try: |
160 | 236 | srv.serve_forever() |
161 | 237 | except KeyboardInterrupt: |
162 | 238 | pass |
163 | 239 | |
164 | 240 | if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': |
165 | print '* Running on http://%s:%d/' % (hostname or '0.0.0.0', port) | |
241 | display_hostname = hostname or '127.0.0.1' | |
242 | _log('info', ' * Running on http://%s:%d/', display_hostname, port) | |
166 | 243 | if use_reloader: |
167 | # Create and destroy a socket so that any exceptions are raised before we | |
168 | # spawn a separate Python interpreter and loose this ability. | |
244 | # Create and destroy a socket so that any exceptions are raised before | |
245 | # we spawn a separate Python interpreter and loose this ability. | |
169 | 246 | test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
170 | 247 | test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
171 | 248 | test_socket.bind((hostname, port)) |
172 | 249 | test_socket.close() |
173 | run_with_reloader(inner, extra_files or []) | |
250 | run_with_reloader(inner, extra_files, reloader_interval) | |
174 | 251 | else: |
175 | 252 | inner() |
2 | 2 | werkzeug.templates |
3 | 3 | ~~~~~~~~~~~~~~~~~~ |
4 | 4 | |
5 | A very simple Python "Template Engine". In fact it just recognizes | |
6 | PHP like blocks and executes the code in them:: | |
5 | This template engine recognizes ASP/PHP like blocks and executes the code | |
6 | in them:: | |
7 | 7 | |
8 | 8 | t = Template('<% for u in users %>${u['username']}\n<% endfor %>') |
9 | 9 | t.render(users=[{'username': 'John'}, |
29 | 29 | Syntax Elements |
30 | 30 | --------------- |
31 | 31 | |
32 | Printing Variables:: | |
32 | Printing Variables: | |
33 | ||
34 | .. sourcecode:: text | |
33 | 35 | |
34 | 36 | $variable |
35 | 37 | $variable.attribute[item](some, function)(calls) |
38 | 40 | Keep in mind that the print statement adds a newline after the call or |
39 | 41 | a whitespace if it ends with a comma. |
40 | 42 | |
41 | For Loops:: | |
43 | For Loops: | |
44 | ||
45 | .. sourcecode:: text | |
42 | 46 | |
43 | 47 | <% for item in seq %> |
44 | 48 | ... |
45 | 49 | <% endfor %> |
46 | 50 | |
47 | While Loops:: | |
51 | While Loops: | |
52 | ||
53 | .. sourcecode:: text | |
48 | 54 | |
49 | 55 | <% while expression %> |
50 | 56 | <%py break / continue %> |
51 | 57 | <% endwhile %> |
52 | 58 | |
53 | If Conditions:: | |
59 | If Conditions: | |
60 | ||
61 | .. sourcecode:: text | |
54 | 62 | |
55 | 63 | <% if expression %> |
56 | 64 | ... |
60 | 68 | ... |
61 | 69 | <% endif %> |
62 | 70 | |
63 | Python Expressions:: | |
71 | Python Expressions: | |
72 | ||
73 | .. sourcecode:: text | |
64 | 74 | |
65 | 75 | <%py |
66 | 76 | ... |
71 | 81 | %> |
72 | 82 | |
73 | 83 | Note on python expressions: You cannot start a loop in a python block |
74 | and continue it in another one. This example does *not* work:: | |
84 | and continue it in another one. This example does *not* work: | |
85 | ||
86 | .. sourcecode:: text | |
75 | 87 | |
76 | 88 | <%python |
77 | 89 | for item in seq: |
78 | 90 | %> |
79 | 91 | ... |
80 | 92 | |
81 | Comments:: | |
93 | Comments: | |
94 | ||
95 | .. sourcecode:: text | |
82 | 96 | |
83 | 97 | <%# |
84 | 98 | This is a comment |
91 | 105 | If you try to access a missing variable you will get back an `Undefined` |
92 | 106 | object. You can iterate over such an object or print it and it won't |
93 | 107 | fail. However every other operation will raise an error. To test if a |
94 | variable is undefined you can use this expression:: | |
108 | variable is undefined you can use this expression: | |
109 | ||
110 | .. sourcecode:: text | |
95 | 111 | |
96 | 112 | <% if variable is Undefined %> |
97 | 113 | ... |
98 | 114 | <% endif %> |
99 | 115 | |
100 | Copyright notice: The `parse_data` method uses the string interpolation | |
101 | algorithm by Ka-Ping Yee which originally was part of `ltpl20.py`_ | |
102 | ||
103 | .. _ltipl20.py: http://lfw.org/python/Itpl20.py | |
104 | ||
105 | ||
106 | :copyright: 2006 by Armin Ronacher, Ka-Ping Yee. | |
116 | ||
117 | :copyright: 2006-2008 by Armin Ronacher, Ka-Ping Yee. | |
107 | 118 | :license: BSD License. |
108 | 119 | """ |
109 | 120 | import sys |
110 | 121 | import re |
111 | 122 | import __builtin__ as builtins |
112 | 123 | from compiler import ast, parse |
124 | from compiler.consts import SC_LOCAL, SC_GLOBAL, SC_FREE, SC_CELL | |
113 | 125 | from compiler.pycodegen import ModuleCodeGenerator |
114 | 126 | from tokenize import PseudoToken |
115 | 127 | from werkzeug import utils |
128 | ||
129 | ||
130 | # Copyright notice: The `parse_data` method uses the string interpolation | |
131 | # algorithm by Ka-Ping Yee which originally was part of `ltpl20.py`_ | |
132 | # | |
133 | # .. _ltipl20.py: http://lfw.org/python/Itpl20.py | |
116 | 134 | |
117 | 135 | |
118 | 136 | token_re = re.compile('%s|%s|%s(?i)' % ( |
129 | 147 | '__repr__': lambda x: 'Undefined', |
130 | 148 | '__str__': lambda x: '' |
131 | 149 | })() |
150 | runtime_vars = dict.fromkeys(('Undefined', '__to_unicode', '__context', | |
151 | '__write', '__write_many')) | |
132 | 152 | |
133 | 153 | |
134 | 154 | def call_stmt(func, args, lineno): |
168 | 188 | node.filename = filename |
169 | 189 | if node.__class__ in (ast.Printnl, ast.Print): |
170 | 190 | node.dest = ast.Name('__context') |
191 | elif node.__class__ is ast.Const and isinstance(node.value, str): | |
192 | try: | |
193 | node.value.decode('ascii') | |
194 | except UnicodeError: | |
195 | node.value = node.value.decode('utf-8') | |
171 | 196 | nodes.extend(node.getChildNodes()) |
172 | 197 | return root |
173 | 198 | |
191 | 216 | raise TemplateSyntaxError(msg, self.filename, self.lineno) |
192 | 217 | |
193 | 218 | def parse_python(self, expr, type='exec'): |
219 | if isinstance(expr, unicode): | |
220 | expr = '\xef\xbb\xbf' + expr.encode('utf-8') | |
194 | 221 | try: |
195 | 222 | node = parse(expr, type) |
196 | 223 | except SyntaxError, e: |
222 | 249 | elif name == 'if': |
223 | 250 | add(self.parse_if(args)) |
224 | 251 | else: |
225 | self.fail('unknown directive %S' % name) | |
252 | self.fail('unknown directive %s' % name) | |
226 | 253 | if needle: |
227 | 254 | self.fail('unexpected end of template') |
228 | 255 | return ast.Stmt(result, lineno=start_lineno) |
229 | 256 | |
230 | 257 | def parse_loop(self, args, type): |
231 | 258 | rv = self.parse_python('%s %s: pass' % (type, args), 'exec').nodes[0] |
232 | tag, value, rv.body = self.parse(('end' + type,)) | |
259 | tag, value, rv.body = self.parse(('end' + type, 'else')) | |
233 | 260 | if value: |
234 | self.fail('unexpected data after end' + type) | |
261 | self.fail('unexpected data after ' + tag) | |
262 | if tag == 'else': | |
263 | tag, value, rv.else_ = self.parse(('end' + type,)) | |
264 | if value: | |
265 | self.fail('unexpected data after else') | |
235 | 266 | return rv |
236 | 267 | |
237 | 268 | def parse_if(self, args): |
345 | 376 | self._buffer = [] |
346 | 377 | self._write = self._buffer.append |
347 | 378 | _extend = self._buffer.extend |
348 | self._namespace.update( | |
379 | self.runtime = dict( | |
349 | 380 | Undefined=undefined, |
350 | 381 | __to_unicode=self.to_unicode, |
351 | 382 | __context=self, |
367 | 398 | return rv.encode(self.encoding, self.errors) |
368 | 399 | return rv |
369 | 400 | |
370 | def __getitem__(self, key): | |
371 | if key in self._namespace: | |
401 | def __getitem__(self, key, default=undefined): | |
402 | try: | |
372 | 403 | return self._namespace[key] |
373 | return getattr(builtins, key, undefined) | |
404 | except KeyError: | |
405 | return getattr(builtins, key, default) | |
406 | ||
407 | def get(self, key, default=None): | |
408 | return self.__getitem__(key, default) | |
374 | 409 | |
375 | 410 | def __setitem__(self, key, value): |
376 | 411 | self._namespace[key] = value |
377 | 412 | |
378 | 413 | def __delitem__(self, key): |
379 | 414 | del self._namespace[key] |
415 | ||
416 | ||
417 | class TemplateCodeGenerator(ModuleCodeGenerator): | |
418 | ||
419 | def __init__(self, node, filename): | |
420 | ModuleCodeGenerator.__init__(self, transform(node, filename)) | |
421 | ||
422 | def _nameOp(self, prefix, name): | |
423 | if name in runtime_vars: | |
424 | return self.emit(prefix + '_GLOBAL', name) | |
425 | return ModuleCodeGenerator._nameOp(self, prefix, name) | |
380 | 426 | |
381 | 427 | |
382 | 428 | class Template(object): |
395 | 441 | errors='strict', unicode_mode=True): |
396 | 442 | if isinstance(source, str): |
397 | 443 | source = source.decode(encoding, errors) |
398 | node = Parser(tokenize('\n'.join(source.splitlines()), | |
444 | node = Parser(tokenize(u'\n'.join(source.splitlines()), | |
399 | 445 | filename), filename).parse() |
400 | self.code = ModuleCodeGenerator(transform(node, filename)).getCode() | |
446 | self.code = TemplateCodeGenerator(node, filename).getCode() | |
401 | 447 | self.filename = filename |
402 | 448 | self.encoding = encoding |
403 | 449 | self.errors = errors |
405 | 451 | |
406 | 452 | def from_file(cls, file, encoding='utf-8', errors='strict', |
407 | 453 | unicode_mode=True): |
454 | """Load a template from a file.""" | |
408 | 455 | close = False |
409 | 456 | if isinstance(file, basestring): |
410 | 457 | f = open(file, 'r') |
419 | 466 | from_file = classmethod(from_file) |
420 | 467 | |
421 | 468 | def render(self, *args, **kwargs): |
469 | """ | |
470 | This function accepts either a dict or some keyword arguments which | |
471 | will then be the context the template is evaluated in. The return | |
472 | value will be the rendered template. | |
473 | """ | |
422 | 474 | ns = self.default_context.copy() |
423 | 475 | ns.update(*args, **kwargs) |
424 | 476 | context = Context(ns, self.encoding, self.errors) |
425 | exec self.code in {}, context | |
477 | exec self.code in context.runtime, context | |
426 | 478 | return context.get_value(self.unicode_mode) |
427 | 479 | |
428 | 480 | def substitute(self, *args, **kwargs): |
2 | 2 | werkzeug.test |
3 | 3 | ~~~~~~~~~~~~~ |
4 | 4 | |
5 | Helper module for unittests. | |
5 | Quite often you want to unittest your application or just check the output | |
6 | from an interactive python session. In theory that is pretty simple because | |
7 | you can fake a WSGI environment and call the application with a dummy | |
8 | start_response and iterate over the application iterator but there are | |
9 | argumentably better ways to interact with an application. | |
10 | ||
11 | Werkzeug provides an object called `Client` which you can pass a WSGI | |
12 | application (and optionally a response wrapper) which you can use to send | |
13 | virtual requests to the application. | |
14 | ||
15 | A response wrapper is a callable that takes three arguments: the application | |
16 | iterator, the status and finally a list of headers. The default response | |
17 | wrapper returns a tuple. Because response objects have the same signature | |
18 | you can use them as response wrapper, ideally by subclassing them and hooking | |
19 | in test functionality. | |
20 | ||
21 | >>> from werkzeug import Client, BaseResponse, test_app | |
22 | >>> c = Client(test_app, BaseResponse) | |
23 | >>> resp = c.get('/') | |
24 | >>> resp.status_code | |
25 | 200 | |
26 | >>> resp.headers | |
27 | Headers([('Content-Type', 'text/html; charset=utf-8')]) | |
28 | >>> resp.response_body.splitlines()[:2] | |
29 | ['<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"', | |
30 | ' "http://www.w3.org/TR/html4/loose.dtd">'] | |
31 | ||
32 | Or here without wrapper defined: | |
33 | ||
34 | >>> from werkzeug import Client, test_app | |
35 | >>> c = Client(test_app) | |
36 | >>> app_iter, status, headers = c.get('/') | |
37 | >>> status | |
38 | '200 OK' | |
39 | >>> headers | |
40 | [('Content-Type', 'text/html; charset=utf-8')] | |
41 | >>> ''.join(app_iter).splitlines()[:2] | |
42 | ['<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"', | |
43 | ' "http://www.w3.org/TR/html4/loose.dtd">'] | |
6 | 44 | |
7 | 45 | :copyright: 2007 by Armin Ronacher. |
8 | 46 | :license: BSD, see LICENSE for more details. |
112 | 150 | def open(self, path='/', base_url=None, query_string=None, method='GET', |
113 | 151 | data=None, input_stream=None, content_type=None, |
114 | 152 | content_length=0, errors_stream=None, multithread=False, |
115 | multiprocess=False, run_once=False, environ_overrides=None): | |
116 | """ | |
117 | Open a page for the application. This function takes similar | |
118 | arguments as the `create_environ` method from the utils module. If | |
119 | the first argument is an environ or request object it is used as | |
120 | the environment for the request. | |
153 | multiprocess=False, run_once=False, environ_overrides=None, | |
154 | as_tuple=False): | |
155 | """ | |
156 | Takes the same arguments as the `create_environ` function from the utility | |
157 | module with some additions. | |
158 | ||
159 | The first parameter should be the path of the request which defaults to | |
160 | '/'. The second one can either be a absolute path (in that case the url | |
161 | host is localhost:80) or a full path to the request with scheme, | |
162 | netloc port and the path to the script. | |
163 | ||
164 | If the `path` contains a query string it will be used, even if the | |
165 | `query_string` parameter was given. If it does not contain one | |
166 | the `query_string` parameter is used as querystring. In that case | |
167 | it can either be a dict, MultiDict or string. | |
168 | ||
169 | The following options exist: | |
170 | ||
171 | `method` | |
172 | The request method. Defaults to `GET` | |
173 | ||
174 | `input_stream` | |
175 | The input stream. Defaults to an empty read only stream. | |
176 | ||
177 | `data` | |
178 | The data you want to transmit. You can set this to a string and | |
179 | define a content type instead of specifying an input stream. | |
180 | Additionally you can pass a dict with the form data. The values | |
181 | could then be strings (no unicode objects!) which are then url | |
182 | encoded or file objects. | |
183 | ||
184 | A file object for this method is either a file descriptor with | |
185 | an additional `name` attribute (like a file descriptor returned | |
186 | by the `open` / `file` function), a tuple in the form | |
187 | ``(fd, filename, mimetype)`` (all arguments except fd optional) | |
188 | or as dict with those keys and values. | |
189 | ||
190 | Additionally you can instanciate the `werkzeug.test.File` object | |
191 | (or a subclass of it) and pass it as value. | |
192 | ||
193 | `content_type` | |
194 | The content type for this request. Default is an empty content | |
195 | type. | |
196 | ||
197 | `content_length` | |
198 | The value for the content length header. Defaults to 0. | |
199 | ||
200 | `errors_stream` | |
201 | The wsgi.errors stream. Defaults to `sys.stderr`. | |
202 | ||
203 | `multithread` | |
204 | The multithreaded flag for the WSGI Environment. Defaults to | |
205 | `False`. | |
206 | ||
207 | `multiprocess` | |
208 | The multiprocess flag for the WSGI Environment. Defaults to | |
209 | `False`. | |
210 | ||
211 | `run_once` | |
212 | The run_once flag for the WSGI Environment. Defaults to `False`. | |
121 | 213 | """ |
122 | 214 | if input_stream is None and data and method in ('PUT', 'POST'): |
123 | 215 | need_multipart = False |
160 | 252 | if environ_overrides: |
161 | 253 | environ.update(environ_overrides) |
162 | 254 | rv = run_wsgi_app(self.application, environ) |
163 | return self.response_wrapper(*rv) | |
255 | response = self.response_wrapper(*rv) | |
256 | if as_tuple: | |
257 | return environ, response | |
258 | return response | |
164 | 259 | |
165 | 260 | def get(self, *args, **kw): |
166 | 261 | """Like open but method is enforced to GET""" |
5 | 5 | Provide a small test application that can be used to test a WSGI server |
6 | 6 | and check it for WSGI compliance. |
7 | 7 | |
8 | :copyright: Copyright 2007 by Armin Ronacher. | |
8 | :copyright: Copyright 2007-2008 by Armin Ronacher. | |
9 | 9 | :license: BSD, see LICENSE for more details. |
10 | 10 | """ |
11 | try: | |
12 | import pkg_resources | |
13 | except ImportError: | |
14 | pkg_resources = None | |
15 | 11 | from werkzeug.templates import Template |
16 | from werkzeug.wrappers import BaseRequest as Request | |
12 | from werkzeug.wrappers import BaseRequest as Request, BaseResponse as Response | |
17 | 13 | |
18 | 14 | |
19 | LOGO = '''\ | |
20 | R0lGODlhoACgAOMIAAEDACwpAEpCAGdgAJaKAM28AOnVAP3rAP////////////////////////// | |
21 | /////yH5BAEKAAgALAAAAACgAKAAAAT+EMlJq704680R+F0ojmRpnuj0rWnrvnB8rbRs33gu0bzu | |
22 | /0AObxgsGn3D5HHJbCUFyqZ0ukkSDlAidctNFg7gbI9LZlrBaHGtzAae0eloe257w9EDOX2fst/x | |
23 | enyCIn5/gFqDiVVDV4aGeYiKkhSFjnCQY5OTlZaXgZp8nJ2ekaB0SQOjqphrpnOiqncEn65UsLGy | |
24 | tLVmQ6m4sQazpbtLqL/HwpnER8bHyLrLOc3Oz8PRONPU1crXN9na263dMt/g4SzjMeXm5yDpLqgG | |
25 | 7OzJ4u8lT/P69ej3JPn69kHzN2OIAHkB9RUYSFCFQYQJFTIkCDBiwoXWGnowaLEjRm7+Gp9A7Hhx | |
26 | 4rUkAUaSLJlxHMqVMD/aSycSZkyTplCqtGnRAM5NQ1Ly5OmzZc6gO4d6DGAUKA+hSocWYAoSlM6o | |
27 | UWX2O/o0KdaVU5vuSQLAa0ADwQgMEMB2AIECZhVSnTno6spgbtXmHcBUrQACcc2FrTrWS8wAf78c | |
28 | MFBgwIBgbN+qvTt3ayikRBk7BoyGAGABAdYyfdzRQGV3l4coxrqQ84GpUBmrdR3xNIDUPAKDBSAA | |
29 | DIGDhhqTZIWaDcrVX8EsbNzbkvCOxG8bN5w8ly9H8jyTJHC6DFndQydbguh2e/ctZJFXRxMAqqPV | |
30 | AtQH5E64SPr1f0zz7sQYjAHg0In+JQ11+N2B0XXBeeYZgBZFx4tqBToiTCPv0YBgQv8JqA6BEf6R | |
31 | hXxw1ENhRBnWV8ctEX4Ul2zc3aVGcQNC2KElyTDYyYUWvShdjDyMOGMuFjqnII45aogPhz/CodUH | |
32 | FwaDxlTgsaOjNyhGWJQd+lFoAGk8ObghI0kawg+EV5blH3dr+digkYuAGSaQZFHFz2P/cTaLmhF5 | |
33 | 2QeSb45Jwxd+uSVGHlqOZpOeJpCFZ5J+rkAkFjQ0N1tah7JJSZUFNsrkeJUJMIBi8jyaEKIhKPom | |
34 | nC91Uo+NByyaJ5umnnpInIFh4t6ZSpGaAVmizqjpByDegYl8tPE0phCYrhcMWSv+uAqHfgH88ak5 | |
35 | UXZmlKLVJhddj78s1Fxnzo6yUCrV6rrDOkluG+QzCAUTbCwf9SrmMLzK6p+OPHx7DF+bsfMRq7Ec | |
36 | 61Av9i6GLw23ridnZ+/OO0a99pbIrJkproCQMA17OPG6suq3cca5ruDfXCCDoS7BEdvmJn5otdqs | |
37 | cn+uogRHHXs8cbhEIfYaDY1AkrC0cqwcZpnM6ludx72x0p7Fo/hZAcpJDjax0UdHavMKAbiKltMW | |
38 | CF3xxh9k25N/Viud8ba78iCvUkt+V6BpwMlErmcgc502x+u1nSxJSJP9Mi52awD1V4yB/QHONsnU | |
39 | 3L+A/zR4VL/indx/y64gqcj+qgTeweM86f0Qy1QVbvmWH1D9h+alqg254QD8HJXHvjQaGOqEqC22 | |
40 | M54PcftZVKVSQG9jhkv7CJyTyDoAJfPdu8v7DRZAxsP/ky9MJ3OL36DJfCFPASC3/aXlfLOOON9v | |
41 | GZZHydGf8LnxYJuuVIbl83yAz5n/RPz07E+9+zw2A2ahz4HxHo9Kt79HTMx1Q7ma7zAzHgHqYH0S | |
42 | oZWyTuOLMiHwSfZDAQTn0ajk9YQqodnUYjByQZhZak9Wu4gYQsMyEpIOAOQKze8CmEF45KuAHTvI | |
43 | DOfHJNipwoHMuGHBnJElUoDmAyXc2Qm/R8Ah/iILCCJOEokGowdhDYc/yoL+vpRGwyVSCWFYZNlj | |
44 | khEirGXsalWcAgOdeAdoXcktF2udbqbUhjWyMQxYO01o6KYKOr6iK3fE4MaS+DsvBsGOBaMb0Y6I | |
45 | xADaJhFICaOLmiWTlDAnY1KzDG4ambLcWBA8mUzjJsN2KjSaSXGqMCVXYpYkj33mcIApyhQf6Yqg | |
46 | eNAmNvuC0t4CsDbSshZJkCS1eNisKqlyGcF8G2JeiDX6tO6Mv0SmjCa3MFb0bJaGPMU0X7c8Xcpv | |
47 | MaOQmCajwSeY9G0WqbBmKv34DsMIEztU6Y2KiDlFdt6jnCSqx7Dmt6XnqSKaFFHNO5+FmODxMCWB | |
48 | Eaco77lNDGXBM0ECYB/+s7nKFdwSF5hgXumQeEZ7amRg39RHy3zIjyRCykQh8Zo2iviRKyTDn/zx | |
49 | 6EefptJj2Cw+Ep2FSc01U5ry4KLPYsTyWnVGnvbUpyGlhjBUljyjHhWpf8OFaXwhp9O4T1gU9Uey | |
50 | PPa8A2l0p1kNqPXEVRm1AOs1oAGZU596t6SOR2mcBOco1srWtkaVrMUzIErrKri85keKqRQYX9VX | |
51 | 0/eAUK1hrSu6HMEX3Qh2sCh0q0D2CtnUqS4hj62sE/zaDs2Sg7MBS6xnQeooc2R2tC9YrKpEi9pL | |
52 | XfYXp20tDCpSP8rKlrD4axprb9u1Df5hSbz9QU0cRpfgnkiIzwKucd0wsEHlLpe5yHXuc6FrNelO | |
53 | l7pY2+11kTWx7VpRu97dXA3DO1vbkhcb4zyvERYajQgAADs=''' | |
15 | logo = Response('''R0lGODlhoACgAOMIAAEDACwpAEpCAGdgAJaKAM28AOnVAP3rAP///////// | |
16 | //////////////////////yH5BAEKAAgALAAAAACgAKAAAAT+EMlJq704680R+F0ojmRpnuj0rWnrv | |
17 | nB8rbRs33gu0bzu/0AObxgsGn3D5HHJbCUFyqZ0ukkSDlAidctNFg7gbI9LZlrBaHGtzAae0eloe25 | |
18 | 7w9EDOX2fst/xenyCIn5/gFqDiVVDV4aGeYiKkhSFjnCQY5OTlZaXgZp8nJ2ekaB0SQOjqphrpnOiq | |
19 | ncEn65UsLGytLVmQ6m4sQazpbtLqL/HwpnER8bHyLrLOc3Oz8PRONPU1crXN9na263dMt/g4SzjMeX | |
20 | m5yDpLqgG7OzJ4u8lT/P69ej3JPn69kHzN2OIAHkB9RUYSFCFQYQJFTIkCDBiwoXWGnowaLEjRm7+G | |
21 | p9A7Hhx4rUkAUaSLJlxHMqVMD/aSycSZkyTplCqtGnRAM5NQ1Ly5OmzZc6gO4d6DGAUKA+hSocWYAo | |
22 | SlM6oUWX2O/o0KdaVU5vuSQLAa0ADwQgMEMB2AIECZhVSnTno6spgbtXmHcBUrQACcc2FrTrWS8wAf | |
23 | 78cMFBgwIBgbN+qvTt3ayikRBk7BoyGAGABAdYyfdzRQGV3l4coxrqQ84GpUBmrdR3xNIDUPAKDBSA | |
24 | ADIGDhhqTZIWaDcrVX8EsbNzbkvCOxG8bN5w8ly9H8jyTJHC6DFndQydbguh2e/ctZJFXRxMAqqPVA | |
25 | tQH5E64SPr1f0zz7sQYjAHg0In+JQ11+N2B0XXBeeYZgBZFx4tqBToiTCPv0YBgQv8JqA6BEf6RhXx | |
26 | w1ENhRBnWV8ctEX4Ul2zc3aVGcQNC2KElyTDYyYUWvShdjDyMOGMuFjqnII45aogPhz/CodUHFwaDx | |
27 | lTgsaOjNyhGWJQd+lFoAGk8ObghI0kawg+EV5blH3dr+digkYuAGSaQZFHFz2P/cTaLmhF52QeSb45 | |
28 | Jwxd+uSVGHlqOZpOeJpCFZ5J+rkAkFjQ0N1tah7JJSZUFNsrkeJUJMIBi8jyaEKIhKPomnC91Uo+NB | |
29 | yyaJ5umnnpInIFh4t6ZSpGaAVmizqjpByDegYl8tPE0phCYrhcMWSv+uAqHfgH88ak5UXZmlKLVJhd | |
30 | dj78s1Fxnzo6yUCrV6rrDOkluG+QzCAUTbCwf9SrmMLzK6p+OPHx7DF+bsfMRq7Ec61Av9i6GLw23r | |
31 | idnZ+/OO0a99pbIrJkproCQMA17OPG6suq3cca5ruDfXCCDoS7BEdvmJn5otdqscn+uogRHHXs8cbh | |
32 | EIfYaDY1AkrC0cqwcZpnM6ludx72x0p7Fo/hZAcpJDjax0UdHavMKAbiKltMWCF3xxh9k25N/Viud8 | |
33 | ba78iCvUkt+V6BpwMlErmcgc502x+u1nSxJSJP9Mi52awD1V4yB/QHONsnU3L+A/zR4VL/indx/y64 | |
34 | gqcj+qgTeweM86f0Qy1QVbvmWH1D9h+alqg254QD8HJXHvjQaGOqEqC22M54PcftZVKVSQG9jhkv7C | |
35 | JyTyDoAJfPdu8v7DRZAxsP/ky9MJ3OL36DJfCFPASC3/aXlfLOOON9vGZZHydGf8LnxYJuuVIbl83y | |
36 | Az5n/RPz07E+9+zw2A2ahz4HxHo9Kt79HTMx1Q7ma7zAzHgHqYH0SoZWyTuOLMiHwSfZDAQTn0ajk9 | |
37 | YQqodnUYjByQZhZak9Wu4gYQsMyEpIOAOQKze8CmEF45KuAHTvIDOfHJNipwoHMuGHBnJElUoDmAyX | |
38 | c2Qm/R8Ah/iILCCJOEokGowdhDYc/yoL+vpRGwyVSCWFYZNljkhEirGXsalWcAgOdeAdoXcktF2udb | |
39 | qbUhjWyMQxYO01o6KYKOr6iK3fE4MaS+DsvBsGOBaMb0Y6IxADaJhFICaOLmiWTlDAnY1KzDG4ambL | |
40 | cWBA8mUzjJsN2KjSaSXGqMCVXYpYkj33mcIApyhQf6YqgeNAmNvuC0t4CsDbSshZJkCS1eNisKqlyG | |
41 | cF8G2JeiDX6tO6Mv0SmjCa3MFb0bJaGPMU0X7c8XcpvMaOQmCajwSeY9G0WqbBmKv34DsMIEztU6Y2 | |
42 | KiDlFdt6jnCSqx7Dmt6XnqSKaFFHNO5+FmODxMCWBEaco77lNDGXBM0ECYB/+s7nKFdwSF5hgXumQe | |
43 | EZ7amRg39RHy3zIjyRCykQh8Zo2iviRKyTDn/zx6EefptJj2Cw+Ep2FSc01U5ry4KLPYsTyWnVGnvb | |
44 | UpyGlhjBUljyjHhWpf8OFaXwhp9O4T1gU9UeyPPa8A2l0p1kNqPXEVRm1AOs1oAGZU596t6SOR2mcB | |
45 | Oco1srWtkaVrMUzIErrKri85keKqRQYX9VX0/eAUK1hrSu6HMEX3Qh2sCh0q0D2CtnUqS4hj62sE/z | |
46 | aDs2Sg7MBS6xnQeooc2R2tC9YrKpEi9pLXfYXp20tDCpSP8rKlrD4axprb9u1Df5hSbz9QU0cRpfgn | |
47 | kiIzwKucd0wsEHlLpe5yHXuc6FrNelOl7pY2+11kTWx7VpRu97dXA3DO1vbkhcb4zyvERYajQgAADs | |
48 | ='''.decode('base64'), mimetype='image/png') | |
49 | ||
54 | 50 | |
55 | 51 | TEMPLATE = Template(ur'''\ |
56 | 52 | <%py |
57 | import sys | |
58 | import os | |
53 | import sys, os | |
59 | 54 | from textwrap import wrap |
60 | ||
55 | try: | |
56 | import pkg_resources | |
57 | except ImportError: | |
58 | eggs = None | |
59 | else: | |
60 | eggs = list(pkg_resources.working_set) | |
61 | eggs.sort(lambda a, b: cmp(a.project_name.lower(), | |
62 | b.project_name.lower())) | |
61 | 63 | sorted_environ = req.environ.items() |
62 | 64 | sorted_environ.sort(lambda a, b: cmp(str(a[0]).lower(), str(b[0]).lower())) |
63 | 65 | %> |
64 | 66 | <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" |
65 | 67 | "http://www.w3.org/TR/html4/loose.dtd"> |
66 | <html> | |
67 | <head> | |
68 | <title>WSGI Information</title> | |
69 | <style type="text/css"> | |
70 | body { | |
71 | font-family: sans-serif; | |
72 | background-color: #333; | |
73 | text-align: center; | |
74 | margin: 1em; | |
75 | padding: 0; | |
76 | } | |
77 | ||
78 | #logo { | |
79 | float: right; | |
80 | padding: 10px; | |
81 | } | |
82 | ||
83 | div.box { | |
84 | text-align: left; | |
85 | width: 45em; | |
86 | padding: 1em; | |
87 | margin: 0 auto 0 auto; | |
88 | border: 1px solid black; | |
89 | background-color: white; | |
90 | } | |
91 | ||
92 | h1 { | |
93 | color: #444; | |
94 | font-size: 2em; | |
95 | margin: 0 0 1em 0; | |
96 | font-family: 'Georgia', serif; | |
97 | } | |
98 | ||
99 | h2 { | |
100 | color: #333; | |
101 | font-size: 1.4em; | |
102 | margin: 1em 0 0.5em 0; | |
103 | } | |
104 | ||
105 | table { | |
106 | width: 100%; | |
107 | border-collapse: collapse; | |
108 | border: 1px solid #ccc; | |
109 | } | |
110 | ||
111 | table th { | |
112 | background-color: #555; | |
113 | color: white; | |
114 | font-size: 0.7em; | |
115 | font-weight: normal; | |
116 | width: 18em; | |
117 | padding: 0.5em 0 0.1em 0.5em; | |
118 | vertical-align: top; | |
119 | } | |
120 | ||
121 | table td { | |
122 | border: 1px solid #ccc; | |
123 | padding: 0.1em 0 0.1em 0.5em; | |
124 | } | |
125 | ||
126 | table td code { | |
127 | font-family: 'Consolas', 'Monaco', 'Bitstream Vera Sans', monospace; | |
128 | font-size: 0.7em; | |
129 | } | |
130 | ||
131 | ul li { | |
132 | line-height: 1.5em; | |
133 | } | |
134 | </style> | |
135 | </head> | |
136 | <body> | |
137 | <div class="box"> | |
138 | <img src="?resource=logo" id="logo" alt="" /> | |
139 | <h1>WSGI Information</h1> | |
140 | <p> | |
141 | This page displays all available information about the WSGI server and | |
142 | the underlaying Python interpreter that are available. | |
143 | </p> | |
144 | <h2 id="python-interpreter">Python Interpreter</h2> | |
145 | <table> | |
146 | <tr> | |
147 | <th>Python Version</th> | |
148 | <td>${'<br>'.join(escape(sys.version).splitlines())}</td> | |
149 | </tr> | |
150 | <tr> | |
151 | <th>Platform</th> | |
152 | <td>$escape(sys.platform) [$escape(os.name)]</td> | |
153 | </tr> | |
154 | <tr> | |
155 | <th>API Version</th> | |
156 | <td>$sys.api_version</td> | |
157 | </tr> | |
158 | <tr> | |
159 | <th>Byteorder</th> | |
160 | <td>$sys.byteorder</td> | |
161 | </tr> | |
162 | </table> | |
163 | <h2 id="wsgi-environment">WSGI Environment</h2> | |
164 | <table> | |
165 | <% for key, value in sorted_environ %> | |
166 | <tr> | |
167 | <th>$escape(str(key))</th> | |
168 | <td><code>${' '.join(wrap(escape(repr(value))))}</code></td> | |
169 | </tr> | |
170 | <% endfor %> | |
171 | </table> | |
172 | <% if eggs %> | |
173 | <h2 id="installed-eggs">Installed Eggs</h2> | |
174 | <ul> | |
175 | <% for egg in eggs %> | |
176 | <li>$escape(egg.project_name) <small>[$escape(egg.version)]</small></li> | |
177 | <% endfor %> | |
178 | </ul> | |
179 | <% endif %> | |
180 | </div> | |
181 | </body> | |
182 | </html> | |
183 | ''', unicode_mode=False) | |
68 | <title>WSGI Information</title> | |
69 | <style type="text/css"> | |
70 | body { font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', | |
71 | 'Verdana', sans-serif; background-color: #AFC1C4; color: #000; | |
72 | text-align: center; margin: 1em; padding: 0; } | |
73 | #logo { float: right; padding: 10px; } | |
74 | div.box { text-align: left; width: 45em; padding: 1em; margin: auto; | |
75 | border: 1px solid #aaa; background-color: white; } | |
76 | h1 { color: #11557C; font-size: 2em; margin: 0 0 0.8em 0; } | |
77 | h2 { font-size: 1.4em; margin: 1em 0 0.5em 0; } | |
78 | table { width: 100%; border-collapse: collapse; border: 1px solid #AFC5C9 } | |
79 | table th { background-color: #AFC1C4; color: white; font-size: 0.72em; | |
80 | font-weight: normal; width: 18em; vertical-align: top; | |
81 | padding: 0.5em 0 0.1em 0.5em; } | |
82 | table td { border: 1px solid #AFC5C9; padding: 0.1em 0 0.1em 0.5em; } | |
83 | code { font-family: 'Consolas', 'Monaco', 'Bitstream Vera Sans Mono', | |
84 | monospace; font-size: 0.7em; } | |
85 | ul li { line-height: 1.5em; } | |
86 | </style> | |
87 | <div class="box"> | |
88 | <img src="?resource=logo" id="logo" alt="[The Werkzeug Logo]" /> | |
89 | <h1>WSGI Information</h1> | |
90 | <p> | |
91 | This page displays all available information about the WSGI server and | |
92 | the underlying Python interpreter that are available. | |
93 | </p> | |
94 | <h2 id="python-interpreter">Python Interpreter</h2> | |
95 | <table> | |
96 | <tr> | |
97 | <th>Python Version</th> | |
98 | <td>${'<br>'.join(escape(sys.version).splitlines())}</td> | |
99 | </tr> | |
100 | <tr> | |
101 | <th>Platform</th> | |
102 | <td>$escape(sys.platform) [$escape(os.name)]</td> | |
103 | </tr> | |
104 | <tr> | |
105 | <th>API Version</th> | |
106 | <td>$sys.api_version</td> | |
107 | </tr> | |
108 | <tr> | |
109 | <th>Byteorder</th> | |
110 | <td>$sys.byteorder</td> | |
111 | </tr> | |
112 | </table> | |
113 | <h2 id="wsgi-environment">WSGI Environment</h2> | |
114 | <table> | |
115 | <% for key, value in sorted_environ %> | |
116 | <tr> | |
117 | <th>$escape(str(key))</th> | |
118 | <td><code>${' '.join(wrap(escape(repr(value))))}</code></td> | |
119 | </tr> | |
120 | <% endfor %> | |
121 | </table> | |
122 | <% if eggs %> | |
123 | <h2 id="installed-eggs">Installed Eggs</h2> | |
124 | <ul> | |
125 | <% for egg in eggs %> | |
126 | <li>$escape(egg.project_name) <small>[$escape(egg.version)]</small></li> | |
127 | <% endfor %> | |
128 | </ul> | |
129 | <% endif %> | |
130 | </div>''') | |
184 | 131 | |
185 | 132 | |
186 | 133 | def test_app(environ, start_response): |
134 | """Simple test application that dumps the environment.""" | |
187 | 135 | req = Request(environ, populate_request=False) |
188 | 136 | if req.args.get('resource') == 'logo': |
189 | image = LOGO.decode('base64') | |
190 | start_response('200 OK', [('Content-Type', 'image/gif'), | |
191 | ('Content-Length', str(len(image)))]) | |
192 | return [image] | |
193 | start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')]) | |
194 | eggs = None | |
195 | if pkg_resources is not None: | |
196 | eggs = list(pkg_resources.working_set) | |
197 | eggs.sort(lambda a, b: cmp(a.project_name.lower(), | |
198 | b.project_name.lower())) | |
199 | return [TEMPLATE.render(req=req, eggs=eggs)] | |
137 | response = logo | |
138 | else: | |
139 | response = Response(TEMPLATE.render(req=req), mimetype='text/html') | |
140 | return response(environ, start_response) | |
200 | 141 | |
201 | 142 | |
202 | 143 | if __name__ == '__main__': |
0 | # -*- coding: utf-8 -*- | |
1 | """ | |
2 | werkzeug.useragents | |
3 | ~~~~~~~~~~~~~~~~~~~ | |
4 | ||
5 | This module provides a helper to inspect user agent strings. This module | |
6 | is far from complete but should work for most of the current browsers that | |
7 | are available. | |
8 | ||
9 | ||
10 | :copyright: 2007-2008 by Armin Ronacher. | |
11 | :license: BSD, see LICENSE for more details. | |
12 | """ | |
13 | import re | |
14 | ||
15 | ||
16 | class UserAgentParser(object): | |
17 | """ | |
18 | A simple user agent parser. Used by the `UserAgent`. | |
19 | """ | |
20 | ||
21 | platforms = ( | |
22 | (r'darwin|mac|os\s*x', 'macos'), | |
23 | ('win', 'windows'), | |
24 | (r'x11|lin(\b|ux)?', 'linux'), | |
25 | ('(sun|i86)os', 'solaris'), | |
26 | ('iphone', 'iphone'), | |
27 | (r'nintendo\s+wii', 'wii'), | |
28 | ('irix', 'irix'), | |
29 | ('hp-?ux', 'hpux'), | |
30 | ('aix', 'aix'), | |
31 | ('sco|unix_sv', 'sco'), | |
32 | ('bsd', 'bsd'), | |
33 | ('amiga', 'amiga') | |
34 | ) | |
35 | browsers = ( | |
36 | ('googlebot', 'google'), | |
37 | ('msnbot', 'msn'), | |
38 | ('yahoo', 'yahoo'), | |
39 | ('ask jeeves', 'ask'), | |
40 | (r'aol|america\s+online\s+browser', 'aol'), | |
41 | ('opera', 'opera'), | |
42 | ('firefox|firebird|phoenix|iceweasel', 'firefox'), | |
43 | ('galeon', 'galeon'), | |
44 | ('safari', 'safari'), | |
45 | ('webkit', 'webkit'), | |
46 | ('camino', 'camino'), | |
47 | ('konqueror', 'konqueror'), | |
48 | ('k-meleon', 'kmeleon'), | |
49 | ('netscape', 'netscape'), | |
50 | (r'msie|microsoft\s+internet\s+explorer', 'msie'), | |
51 | ('lynx', 'lynx'), | |
52 | ('links', 'links'), | |
53 | ('seamonkey|mozilla', 'seamonkey') | |
54 | ) | |
55 | ||
56 | _browser_version_re = r'(?:%s)[/\sa-z(]*(\d+[.\da-z]+)?(?i)' | |
57 | _language_re = re.compile( | |
58 | r'(?:;\s*|\s+)(\b\w{2}\b(?:-\b\w{2}\b)?)\s*;|' | |
59 | r'(?:\(|\[|;)\s*(\b\w{2}\b(?:-\b\w{2}\b)?)\s*(?:\]|\)|;)' | |
60 | ) | |
61 | ||
62 | def __init__(self): | |
63 | self.platforms = re.compile(r'|'.join(['(?P<%s>%s)' % (b, a) for a, b | |
64 | in self.platforms]), re.I) | |
65 | self.browsers = [(b, re.compile(self._browser_version_re % a)) | |
66 | for a, b in self.browsers] | |
67 | ||
68 | def __call__(self, user_agent): | |
69 | match = self.platforms.search(user_agent) | |
70 | if match is not None: | |
71 | for platform, value in match.groupdict().iteritems(): | |
72 | if value: | |
73 | break | |
74 | else: | |
75 | platform = None | |
76 | for browser, regex in self.browsers: | |
77 | match = regex.search(user_agent) | |
78 | if match is not None: | |
79 | version = match.group(1) | |
80 | break | |
81 | else: | |
82 | browser = version = None | |
83 | match = self._language_re.search(user_agent) | |
84 | if match is not None: | |
85 | language = match.group(1) or match.group(2) | |
86 | else: | |
87 | language = None | |
88 | return platform, browser, version, language | |
89 | ||
90 | ||
91 | class UserAgent(object): | |
92 | """ | |
93 | Represents a user agent. Pass it a WSGI environment or an user agent | |
94 | string and you can inspect some of the details from the user agent | |
95 | string via the attributes. The following attribute exist: | |
96 | ||
97 | - `string`, the raw user agent string | |
98 | - `platform`, the browser platform | |
99 | - `browser`, the name of the browser | |
100 | - `version`, the version of the browser | |
101 | - `language`, the language of the browser | |
102 | """ | |
103 | _parser = UserAgentParser() | |
104 | ||
105 | def __init__(self, environ_or_string): | |
106 | if isinstance(environ_or_string, dict): | |
107 | environ_or_string = environ_or_string.get('HTTP_USER_AGENT', '') | |
108 | self.string = environ_or_string | |
109 | self.platform, self.browser, self.version, self.language = \ | |
110 | self._parser(environ_or_string) | |
111 | ||
112 | def to_header(self): | |
113 | return self.string | |
114 | ||
115 | def __str__(self): | |
116 | return self.string | |
117 | ||
118 | def __nonzero__(self): | |
119 | return bool(self.browser) | |
120 | ||
121 | def __repr__(self): | |
122 | return '<%s %r/%s>' % ( | |
123 | self.__class__.__name__, | |
124 | self.browser, | |
125 | self.version | |
126 | ) | |
127 | ||
128 | ||
129 | # conceptionally this belongs in this module but because we want to lazily | |
130 | # load the user agent module (which happens in wrappers.py) we have to import | |
131 | # it afterwards. The class itself has the module set to this module so | |
132 | # pickle, inspect and similar modules treat the object as if it was really | |
133 | # implemented here. | |
134 | from werkzeug.wrappers import UserAgentMixin |
2 | 2 | werkzeug.utils |
3 | 3 | ~~~~~~~~~~~~~~ |
4 | 4 | |
5 | Various utils. | |
6 | ||
7 | :copyright: 2007 by Armin Ronacher, Georg Brandl. | |
5 | This module implements various utilities for WSGI applications. Most of | |
6 | them are used by the request and response wrappers but especially for | |
7 | middleware development it makes sense to use them without the wrappers. | |
8 | ||
9 | :copyright: 2007-2008 by Armin Ronacher, Georg Brandl. | |
8 | 10 | :license: BSD, see LICENSE for more details. |
9 | 11 | """ |
10 | 12 | import re |
13 | 15 | import cgi |
14 | 16 | import urllib |
15 | 17 | import urlparse |
18 | from itertools import chain | |
19 | from Cookie import BaseCookie, Morsel, CookieError | |
16 | 20 | from time import asctime, gmtime, time |
17 | from datetime import datetime | |
21 | from datetime import datetime, timedelta | |
18 | 22 | from cStringIO import StringIO |
19 | 23 | try: |
20 | set | |
24 | set = set | |
21 | 25 | except NameError: |
22 | 26 | from sets import Set as set |
23 | ||
24 | 27 | def reversed(item): |
25 | return tuple(item)[::-1] | |
28 | return item[::-1] | |
26 | 29 | |
27 | 30 | |
28 | 31 | _empty_stream = StringIO('') |
32 | _logger = None | |
29 | 33 | |
30 | 34 | _format_re = re.compile(r'\$(%s|\{%s\})' % (('[a-zA-Z_][a-zA-Z0-9_]*',) * 2)) |
35 | _entity_re = re.compile(r'&([^;]+);') | |
36 | ||
37 | ||
38 | def _log(type, message, *args, **kwargs): | |
39 | """ | |
40 | Log into the internal werkzeug logger. | |
41 | ||
42 | :internal: | |
43 | """ | |
44 | global _logger | |
45 | if _logger is None: | |
46 | import logging | |
47 | handler = logging.StreamHandler() | |
48 | _logger = logging.getLogger('werkzeug') | |
49 | _logger.addHandler(handler) | |
50 | _logger.setLevel(logging.INFO) | |
51 | getattr(_logger, type)(message.rstrip(), *args, **kwargs) | |
52 | ||
53 | ||
54 | def _patch_wrapper(old, new): | |
55 | """ | |
56 | Helper function that forwards all the function details to the | |
57 | decorated function. | |
58 | """ | |
59 | try: | |
60 | new.__name__ = old.__name__ | |
61 | new.__module__ = old.__module__ | |
62 | new.__doc__ = old.__doc__ | |
63 | new.__dict__ = old.__dict__ | |
64 | except AttributeError: | |
65 | pass | |
66 | return new | |
67 | ||
68 | ||
69 | class _ExtendedMorsel(Morsel): | |
70 | """ | |
71 | Subclass of regular morsels for simpler usage and support of the | |
72 | nonstandard but useful http only header. | |
73 | ||
74 | :internal: | |
75 | """ | |
76 | _reserved = {'httponly': 'HttpOnly'} | |
77 | _reserved.update(Morsel._reserved) | |
78 | ||
79 | def __init__(self, name=None, value=None): | |
80 | Morsel.__init__(self) | |
81 | if name is not None: | |
82 | self.set(name, value, value) | |
83 | ||
84 | def OutputString(self, attrs=None): | |
85 | httponly = self.pop('httponly', False) | |
86 | result = Morsel.OutputString(self, attrs).rstrip('\t ;') | |
87 | if httponly: | |
88 | result += '; HttpOnly' | |
89 | return result | |
90 | ||
91 | ||
92 | class _ExtendedCookie(BaseCookie): | |
93 | """ | |
94 | Form of the base cookie that doesn't raise a `CookieError` for | |
95 | malformed keys. This has the advantage that broken cookies submitted | |
96 | by nonstandard browsers don't cause the cookie to be empty. | |
97 | ||
98 | :internal: | |
99 | """ | |
100 | ||
101 | def _BaseCookie__set(self, key, real_value, coded_value): | |
102 | morsel = self.get(key, _ExtendedMorsel()) | |
103 | try: | |
104 | morsel.set(key, real_value, coded_value) | |
105 | except CookieError: | |
106 | pass | |
107 | dict.__setitem__(self, key, morsel) | |
31 | 108 | |
32 | 109 | |
33 | 110 | class MultiDict(dict): |
34 | 111 | """ |
35 | A dict that takes a list of multiple values as only argument | |
36 | in order to store multiple values per key. | |
112 | A `MultiDict` is a dictionary subclass customized to deal with multiple | |
113 | values for the same key which is for example used by the parsing functions | |
114 | in the wrappers. This is necessary because some HTML form elements pass | |
115 | multiple values for the same key. | |
116 | ||
117 | `MultiDict` implements the all standard dictionary methods. Internally, | |
118 | it saves all values for a key as a list, but the standard dict access | |
119 | methods will only return the first value for a key. If you want to gain | |
120 | access to the other values too you have to use the `list` methods as | |
121 | explained below. | |
122 | ||
123 | Basic Usage: | |
124 | ||
125 | >>> d = MultiDict([('a', 'b'), ('a', 'c')]) | |
126 | >>> d | |
127 | MultiDict([('a', 'b'), ('a', 'c')]) | |
128 | >>> d['a'] | |
129 | 'b' | |
130 | >>> d.getlist('a') | |
131 | ['b', 'c'] | |
132 | >>> 'a' in d | |
133 | True | |
134 | ||
135 | It behaves like a normal dict thus all dict functions will only return the | |
136 | first value when multiple values for one key are found. | |
37 | 137 | """ |
38 | 138 | |
39 | 139 | def __init__(self, mapping=()): |
140 | """ | |
141 | A `MultiDict` can be constructed from an iterable of ``(key, value)`` | |
142 | tuples, a dict, a `MultiDict` or with Werkzeug 0.2 onwards some | |
143 | keyword parameters. | |
144 | """ | |
40 | 145 | if isinstance(mapping, MultiDict): |
41 | 146 | dict.__init__(self, [(k, v[:]) for k, v in mapping.lists()]) |
42 | 147 | elif isinstance(mapping, dict): |
58 | 163 | """ |
59 | 164 | Return the first data value for this key; |
60 | 165 | raises KeyError if not found. |
166 | ||
167 | :raise KeyError: if the key does not exist | |
61 | 168 | """ |
62 | 169 | return dict.__getitem__(self, key)[0] |
63 | 170 | |
68 | 175 | def get(self, key, default=None, type=None): |
69 | 176 | """ |
70 | 177 | Return the default value if the requested data doesn't exist. |
71 | Additionally you can pass it a type function that is used as | |
72 | converter. That function should either conver the value or | |
73 | raise a `ValueError`. | |
178 | If `type` is provided and is a callable it should convert the value, | |
179 | return it or raise a `ValueError` if that is not possible. In this | |
180 | case the function will return the default as if the value was not | |
181 | found. | |
182 | ||
183 | Example: | |
184 | ||
185 | >>> d = MultiDict(foo='42', bar='blub') | |
186 | >>> d.get('foo', type=int) | |
187 | 42 | |
188 | >>> d.get('bar', -1, type=int) | |
189 | -1 | |
74 | 190 | """ |
75 | 191 | try: |
76 | 192 | rv = self[key] |
81 | 197 | return rv |
82 | 198 | |
83 | 199 | def getlist(self, key, type=None): |
84 | """Return an empty list if the requested data doesn't exist""" | |
200 | """ | |
201 | Return the list of items for a given key. If that key is not in the | |
202 | `MultiDict`, the return value will be an empty list. Just as `get` | |
203 | `getlist` accepts a `type` parameter. All items will be converted | |
204 | with the callable defined there. | |
205 | ||
206 | :return: list | |
207 | """ | |
85 | 208 | try: |
86 | 209 | rv = dict.__getitem__(self, key) |
87 | 210 | except KeyError: |
97 | 220 | return result |
98 | 221 | |
99 | 222 | def setlist(self, key, new_list): |
100 | """Set new values for an key.""" | |
223 | """ | |
224 | Remove the old values for a key and add new ones. Note that the list | |
225 | you pass the values in will be shallow-copied before it is inserted in | |
226 | the dictionary. | |
227 | ||
228 | >>> multidict.setlist('foo', ['1', '2']) | |
229 | >>> multidict['foo'] | |
230 | '1' | |
231 | >>> multidict.getlist('foo') | |
232 | ['1', '2'] | |
233 | """ | |
101 | 234 | dict.__setitem__(self, key, list(new_list)) |
102 | 235 | |
103 | 236 | def setdefault(self, key, default=None): |
108 | 241 | return default |
109 | 242 | |
110 | 243 | def setlistdefault(self, key, default_list=()): |
244 | """Like `setdefault` but sets multiple values.""" | |
111 | 245 | if key not in self: |
112 | 246 | default_list = list(default_list) |
113 | 247 | dict.__setitem__(self, key, default_list) |
148 | 282 | |
149 | 283 | def to_dict(self, flat=True): |
150 | 284 | """ |
151 | Returns the contents as simple dict. If `flat` is `True` the | |
152 | resulting dict will only have the first item present, if `flat` | |
153 | is `False` all values will be lists. | |
285 | Return the contents as regular dict. If `flat` is `True` the returned | |
286 | dict will only have the first item present, if `flat` is `False` all | |
287 | values will be returned as lists. | |
288 | ||
289 | :return: dict | |
154 | 290 | """ |
155 | 291 | if flat: |
156 | 292 | return dict(self.iteritems()) |
190 | 326 | |
191 | 327 | class CombinedMultiDict(MultiDict): |
192 | 328 | """ |
193 | Pass it multiple multidicts to create a new read only | |
194 | dict which resolves items from the passed dicts. | |
329 | A read only `MultiDict` decorator that you can pass multiple `MultiDict` | |
330 | instances as sequence and it will combine the return values of all wrapped | |
331 | dicts: | |
332 | ||
333 | >>> from werkzeug import MultiDict, CombinedMultiDict | |
334 | >>> post = MultiDict([('foo', 'bar')]) | |
335 | >>> get = MultiDict([('blub', 'blah')]) | |
336 | >>> combined = CombinedMultiDict([get, post]) | |
337 | >>> combined['foo'] | |
338 | 'bar' | |
339 | >>> combined['blub'] | |
340 | 'blah' | |
341 | ||
342 | This works for all read operations and will raise a `TypeError` for | |
343 | methods that usually change data which isn't possible. | |
195 | 344 | """ |
196 | 345 | |
197 | 346 | def __init__(self, dicts=None): |
213 | 362 | if key in d: |
214 | 363 | if type is not None: |
215 | 364 | try: |
216 | type(d[key]) | |
365 | return type(d[key]) | |
217 | 366 | except ValueError: |
218 | 367 | continue |
219 | 368 | return d[key] |
312 | 461 | |
313 | 462 | class FileStorage(object): |
314 | 463 | """ |
315 | Represents an uploaded file. | |
316 | """ | |
317 | ||
318 | def __init__(self, name, filename, content_type, content_length, stream): | |
464 | The `FileStorage` object is a thin wrapper over incoming files. It is | |
465 | used by the request object to represent uploaded files. All the | |
466 | attributes of the wrapper stream are proxied by the file storage so | |
467 | it's possible to do ``storage.read()`` instead of the long form | |
468 | ``storage.stream.read()``. | |
469 | """ | |
470 | ||
471 | def __init__(self, stream=None, filename=None, name=None, | |
472 | content_type='application/octet-stream', content_length=-1): | |
473 | """ | |
474 | Creates a new `FileStorage` object. The constructor looked different | |
475 | for Werkzeug 0.1 but there the object was only used internally. | |
476 | ||
477 | :param stream: the input stream for uploaded file. Usually this | |
478 | points to a temporary file. | |
479 | :param filename: The filename of the file on the client. | |
480 | :param name: the name of the form field | |
481 | :param content_type: the content type of the file | |
482 | :param content_length: the content length of the file. | |
483 | """ | |
319 | 484 | self.name = name |
320 | self.filename = filename | |
485 | self.stream = stream or _empty_stream | |
486 | self.filename = filename or getattr(stream, 'name', None) | |
321 | 487 | self.content_type = content_type |
322 | 488 | self.content_length = content_length |
323 | self.stream = stream | |
324 | 489 | |
325 | 490 | def save(self, dst, buffer_size=16384): |
326 | 491 | """ |
327 | 492 | Save the file to a destination path or file object. If the |
328 | destination is a file object you have to close it yourself after the call. | |
329 | The buffer size is the number of bytes held in the memory during the copy | |
330 | process. It defaults to 16KB. | |
493 | destination is a file object you have to close it yourself after the | |
494 | call. The buffer size is the number of bytes held in the memory | |
495 | during the copy process. It defaults to 16KB. | |
331 | 496 | """ |
332 | 497 | from shutil import copyfileobj |
333 | 498 | if isinstance(dst, basestring): |
349 | 514 | return bool(self.filename and self.content_length) |
350 | 515 | |
351 | 516 | def __len__(self): |
352 | return self.content_length | |
517 | return max(self.content_length, 0) | |
353 | 518 | |
354 | 519 | def __iter__(self): |
355 | 520 | return iter(self.readline, '') |
366 | 531 | """ |
367 | 532 | An object that stores some headers. It has a dict like interface |
368 | 533 | but is ordered and can store keys multiple times. |
369 | """ | |
370 | ||
371 | def __init__(self, defaults=None): | |
372 | self._list = [] | |
534 | ||
535 | This data structure is useful if you want a nicer way to handle WSGI | |
536 | headers which are stored as tuples in a list. | |
537 | """ | |
538 | ||
539 | def __init__(self, defaults=None, _list=None): | |
540 | """ | |
541 | Create a new `Headers` object based on a list or dict of headers which | |
542 | are used as default values. This does not reuse the list passed to | |
543 | the constructor for internal usage. To create a `Headers` object that | |
544 | uses as internal storage the list or list-like object provided it's | |
545 | possible to use the `linked` classmethod. | |
546 | """ | |
547 | if _list is None: | |
548 | _list = [] | |
549 | self._list = _list | |
373 | 550 | if isinstance(defaults, dict): |
374 | 551 | for key, value in defaults.iteritems(): |
375 | 552 | if isinstance(value, (tuple, list)): |
380 | 557 | elif defaults is not None: |
381 | 558 | self._list[:] = defaults |
382 | 559 | |
560 | def linked(cls, headerlist): | |
561 | """ | |
562 | Create a new `Headers` object that uses the list of headers passed as | |
563 | internal storage: | |
564 | ||
565 | >>> headerlist = [('Content-Length', '40')] | |
566 | >>> headers = Headers.linked(headerlist) | |
567 | >>> headers.add('Content-Type', 'text/html') | |
568 | >>> headerlist | |
569 | [('Content-Length', '40'), ('Content-Type', 'text/html')] | |
570 | ||
571 | :return: new linked `Headers` object. | |
572 | """ | |
573 | return cls(_list=headerlist) | |
574 | linked = classmethod(linked) | |
575 | ||
383 | 576 | def __getitem__(self, key): |
384 | 577 | ikey = key.lower() |
385 | 578 | for k, v in self._list: |
394 | 587 | def __ne__(self, other): |
395 | 588 | return not self.__eq__(other) |
396 | 589 | |
397 | def get(self, key, default=None): | |
590 | def get(self, key, default=None, type=None): | |
591 | """ | |
592 | Return the default value if the requested data doesn't exist. | |
593 | If `type` is provided and is a callable it should convert the value, | |
594 | return it or raise a `ValueError` if that is not possible. In this | |
595 | case the function will return the default as if the value was not | |
596 | found. | |
597 | ||
598 | Example: | |
599 | ||
600 | >>> d = Headers([('Content-Length', '42')]) | |
601 | >>> d.get('Content-Length', type=int) | |
602 | 42 | |
603 | ||
604 | If a headers object is bound you must notadd unicode strings | |
605 | because no encoding takes place. | |
606 | """ | |
398 | 607 | try: |
399 | return self[key] | |
608 | rv = self[key] | |
400 | 609 | except KeyError: |
401 | 610 | return default |
402 | ||
403 | def getlist(self, key): | |
611 | if type is None: | |
612 | return rv | |
613 | try: | |
614 | return type(rv) | |
615 | except ValueError: | |
616 | return default | |
617 | ||
618 | def getlist(self, key, type=None): | |
619 | """ | |
620 | Return the list of items for a given key. If that key is not in the | |
621 | `MultiDict`, the return value will be an empty list. Just as `get` | |
622 | `getlist` accepts a `type` parameter. All items will be converted | |
623 | with the callable defined there. | |
624 | ||
625 | :return: list | |
626 | """ | |
404 | 627 | ikey = key.lower() |
405 | 628 | result = [] |
406 | 629 | for k, v in self: |
407 | 630 | if k.lower() == ikey: |
631 | if type is not None: | |
632 | try: | |
633 | v = type(v) | |
634 | except ValueError: | |
635 | continue | |
408 | 636 | result.append(v) |
409 | 637 | return result |
410 | 638 | |
430 | 658 | |
431 | 659 | def items(self, lower=False): |
432 | 660 | return list(self.iteritems(lower)) |
661 | ||
662 | def extend(self, iterable): | |
663 | """ | |
664 | Extend the headers with a dict or an iterable yielding keys and | |
665 | values. | |
666 | """ | |
667 | if isinstance(iterable, dict): | |
668 | iterable = iterable.iteritems() | |
669 | for key, value in iterable: | |
670 | self.add(key, value) | |
433 | 671 | |
434 | 672 | def __delitem__(self, key): |
435 | 673 | key = key.lower() |
442 | 680 | remove = __delitem__ |
443 | 681 | |
444 | 682 | def __contains__(self, key): |
683 | """Check if a key is present.""" | |
445 | 684 | try: |
446 | 685 | self[key] |
447 | 686 | except KeyError: |
451 | 690 | has_key = __contains__ |
452 | 691 | |
453 | 692 | def __iter__(self): |
693 | """Yield ``(key, value)`` tuples.""" | |
454 | 694 | return iter(self._list) |
455 | 695 | |
456 | 696 | def add(self, key, value): |
471 | 711 | __setitem__ = set |
472 | 712 | |
473 | 713 | def to_list(self, charset='utf-8'): |
474 | """Create a str only list of the headers.""" | |
714 | """ | |
715 | Convert the headers into a list and converts the unicode header items | |
716 | to the specified charset. | |
717 | ||
718 | :return: list | |
719 | """ | |
475 | 720 | result = [] |
476 | 721 | for k, v in self: |
477 | 722 | if isinstance(v, unicode): |
496 | 741 | |
497 | 742 | class EnvironHeaders(Headers): |
498 | 743 | """ |
499 | Read only version of the headers from wsgi environment. | |
744 | Read only version of the headers from a WSGI environment. This | |
745 | provides the same interface as `Headers` and is constructed from | |
746 | a WSGI environment. | |
500 | 747 | """ |
501 | 748 | |
502 | 749 | def __init__(self, environ): |
503 | 750 | self.environ = environ |
751 | ||
752 | def linked(cls, environ): | |
753 | raise TypeError('%r object is always linked to environment, ' | |
754 | 'no separate initializer' % self.__class__.__name__) | |
755 | linked = classmethod(linked) | |
504 | 756 | |
505 | 757 | def __eq__(self, other): |
506 | 758 | return self is other |
518 | 770 | |
519 | 771 | def _immutable(self, *a, **kw): |
520 | 772 | raise TypeError('%r is immutable' % self.__class__.__name__) |
521 | remove = __delitem__ = add = clear = set = __setitem__ = _immutable | |
773 | remove = __delitem__ = add = clear = extend = set = __setitem__ = \ | |
774 | _immutable | |
522 | 775 | del _immutable |
523 | 776 | |
524 | 777 | |
525 | 778 | class SharedDataMiddleware(object): |
526 | 779 | """ |
527 | Redirects calls to an folder with static data. | |
780 | A WSGI middleware that provides static content for development | |
781 | environments or simple server setups. Usage is quite simple:: | |
782 | ||
783 | import os | |
784 | from werkzeug import SharedDataMiddleware | |
785 | ||
786 | app = SharedDataMiddleware(app, { | |
787 | '/shared': os.path.join(os.path.dirname(__file__), 'shared') | |
788 | }) | |
789 | ||
790 | The contents of the folder ``./shared`` will now be available on | |
791 | ``http://example.com/shared/``. This is pretty useful during development | |
792 | because a standalone media server is not required. One can also mount | |
793 | files on the root folder and still continue to use the application because | |
794 | the shared data middleware forwards all unhandled requests to the | |
795 | application, even if the requests are below one of the shared folders. | |
796 | ||
797 | If `pkg_resources` is available you can also tell the middleware to serve | |
798 | files from package data:: | |
799 | ||
800 | app = SharedDataMiddleware(app, { | |
801 | '/shared': ('myapplication', 'shared_files') | |
802 | }) | |
803 | ||
804 | This will then serve the ``shared_files`` folder in the `myapplication` | |
805 | python package. | |
528 | 806 | """ |
529 | 807 | |
530 | 808 | def __init__(self, app, exports, disallow=None): |
531 | 809 | self.app = app |
532 | self.exports = exports | |
533 | self.disallow = disallow | |
534 | ||
535 | def serve_file(self, filename, start_response): | |
810 | self.exports = {} | |
811 | for key, value in exports.iteritems(): | |
812 | if isinstance(value, tuple): | |
813 | loader = self.get_package_loader(*value) | |
814 | elif isinstance(value, basestring): | |
815 | if os.path.isfile(value): | |
816 | loader = self.get_file_loader(value) | |
817 | else: | |
818 | loader = self.get_directory_loader(value) | |
819 | else: | |
820 | raise TypeError('unknown def %r' % value) | |
821 | self.exports[key] = loader | |
822 | if disallow is not None: | |
823 | from fnmatch import fnmatch | |
824 | self.is_allowed = lambda x: not fnmatch(x, disallow) | |
825 | ||
826 | def is_allowed(self, filename): | |
827 | return True | |
828 | ||
829 | def get_file_loader(self, filename): | |
830 | return lambda x: os.path.basename(filename), \ | |
831 | lambda: open(filename, 'rb') | |
832 | ||
833 | def get_package_loader(self, package, package_path): | |
834 | from pkg_resources import resource_exists, resource_stream | |
835 | def loader(path): | |
836 | path = posixpath.join(package_path, path) | |
837 | if resource_exists(package, path): | |
838 | return posixpath.basename(path), \ | |
839 | lambda: resource_stream(package, path) | |
840 | return None, None | |
841 | return loader | |
842 | ||
843 | def get_directory_loader(self, directory): | |
844 | def loader(path): | |
845 | path = os.path.join(directory, path) | |
846 | if os.path.isfile(path): | |
847 | return os.path.basename(path), lambda: open(path, 'rb') | |
848 | return None, None | |
849 | return loader | |
850 | ||
851 | def __call__(self, environ, start_response): | |
852 | # sanitize the path for non unix systems | |
853 | cleaned_path = environ.get('PATH_INFO', '').strip('/') | |
854 | for sep in os.sep, os.altsep: | |
855 | if sep and sep != '/': | |
856 | cleaned_path = cleaned_path.replace(sep, '/') | |
857 | path = '/'.join([''] + [x for x in cleaned_path.split('/') | |
858 | if x and x != '..']) | |
859 | stream_maker = None | |
860 | for search_path, loader in self.exports.iteritems(): | |
861 | if search_path == path: | |
862 | real_filename, stream_maker = loader.load(None) | |
863 | if stream_maker is not None: | |
864 | break | |
865 | if not search_path.endswith('/'): | |
866 | search_path += '/' | |
867 | if path.startswith(search_path): | |
868 | real_filename, stream_maker = loader(path[len(search_path):]) | |
869 | if stream_maker is not None: | |
870 | break | |
871 | if stream_maker is None or not self.is_allowed(real_filename): | |
872 | return self.app(environ, start_response) | |
536 | 873 | from mimetypes import guess_type |
537 | guessed_type = guess_type(filename) | |
874 | guessed_type = guess_type(real_filename) | |
538 | 875 | mime_type = guessed_type[0] or 'text/plain' |
539 | 876 | expiry = asctime(gmtime(time() + 3600)) |
540 | 877 | start_response('200 OK', [('Content-Type', mime_type), |
541 | 878 | ('Cache-Control', 'public'), |
542 | 879 | ('Expires', expiry)]) |
543 | fp = file(filename, 'rb') | |
880 | stream = stream_maker() | |
544 | 881 | try: |
545 | return [fp.read()] | |
882 | return [stream.read()] | |
546 | 883 | finally: |
547 | fp.close() | |
884 | stream.close() | |
885 | ||
886 | ||
887 | class DispatcherMiddleware(object): | |
888 | """ | |
889 | Allows one to mount middlewares or application in a WSGI application. | |
890 | This is useful if you want to combine multiple WSGI applications:: | |
891 | ||
892 | app = DispatcherMiddleware(app, { | |
893 | '/app2': app2, | |
894 | '/app3': app3 | |
895 | }) | |
896 | """ | |
897 | ||
898 | def __init__(self, app, mounts=None): | |
899 | self.app = app | |
900 | self.mounts = mounts or {} | |
548 | 901 | |
549 | 902 | def __call__(self, environ, start_response): |
550 | path = '/'.join([''] + [x for x in environ.get('PATH_INFO', ''). | |
551 | strip('/').split() if x != '..']) | |
552 | for search_path, file_path in self.exports.iteritems(): | |
553 | if search_path == path and os.path.isfile(file_path): | |
554 | return self.serve_file(file_path, start_response) | |
555 | if not search_path.endswith('/'): | |
556 | search_path += '/' | |
557 | if path.startswith(search_path): | |
558 | real_path = os.path.join(file_path, path[len(search_path):]) | |
559 | if os.path.exists(real_path) and os.path.isfile(real_path): | |
560 | if self.disallow: | |
561 | from fnmatch import fnmatch | |
562 | for pattern in self.disallow: | |
563 | if fnmatch(real_path, pattern): | |
564 | break | |
565 | else: | |
566 | return self.serve_file(real_path, start_response) | |
567 | else: | |
568 | return self.serve_file(real_path, start_response) | |
569 | return self.app(environ, start_response) | |
903 | script = environ.get('PATH_INFO', '') | |
904 | path_info = '' | |
905 | while '/' in script: | |
906 | if script in self.mounts: | |
907 | app = self.mounts[script] | |
908 | break | |
909 | items = script.split('/') | |
910 | script = '/'.join(items[:-1]) | |
911 | path_info = '/%s%s' % (items[-1], path_info) | |
912 | else: | |
913 | app = self.mounts.get(script, self.app) | |
914 | original_script_name = environ.get('SCRIPT_NAME', '') | |
915 | environ['SCRIPT_NAME'] = original_script_name + script | |
916 | environ['PATH_INFO'] = path_info | |
917 | return app(environ, start_response) | |
570 | 918 | |
571 | 919 | |
572 | 920 | class ClosingIterator(object): |
573 | 921 | """ |
574 | A class that wraps an iterator (which can have a close method) and | |
575 | adds a close method for the callback and the iterator. | |
576 | ||
577 | Usage:: | |
578 | ||
579 | return ClosingIterator(iter, [list, of, callbacks]) | |
922 | The WSGI specification requires that all middlewares and gateways respect the | |
923 | `close` callback of an iterator. Because it is useful to add another close | |
924 | action to a returned iterator and adding a custom iterator is a boring task | |
925 | this class can be used for that:: | |
926 | ||
927 | return ClosingIterator(app(environ, start_response), [cleanup_session, | |
928 | cleanup_locals]) | |
929 | ||
930 | If there is just one close function it can be bassed instead of the list. | |
931 | ||
932 | A closing iterator is non needed if the application uses response objects | |
933 | and finishes the processing if the resonse is started:: | |
934 | ||
935 | try: | |
936 | return response(environ, start_response) | |
937 | finally: | |
938 | cleanup_session() | |
939 | cleanup_locals() | |
580 | 940 | """ |
581 | 941 | |
582 | 942 | def __init__(self, iterable, callbacks=None): |
604 | 964 | callback() |
605 | 965 | |
606 | 966 | |
607 | class lazy_property(object): | |
608 | """ | |
609 | Descriptor implementing a "lazy property", i.e. the function | |
610 | calculating the property value is called only once. | |
967 | class Href(object): | |
968 | """ | |
969 | Implements a callable that constructs URLs with the given base. The | |
970 | function can be called with any number of positional and keyword | |
971 | arguments which than are used to assemble the URL. Works with URLs | |
972 | and posix paths. | |
973 | ||
974 | Positional arguments are appended as individual segments to | |
975 | the path of the URL: | |
976 | ||
977 | >>> href = Href('/foo') | |
978 | >>> href('bar', 23) | |
979 | '/foo/bar/23' | |
980 | >>> href('foo', bar=23) | |
981 | '/foo/foo?bar=23' | |
982 | ||
983 | If any of the arguments (positional or keyword) evaluates to `None` it | |
984 | will be skipped. If no keyword arguments are given the last argument | |
985 | can be a `dict` or `MultiDict` (or any other dict subclass), otherwise | |
986 | the keyword arguments are used for the query parameters, cutting off | |
987 | the first trailing underscore of the parameter name: | |
988 | ||
989 | >>> href(is_=42) | |
990 | '/foo?is=42' | |
991 | ||
992 | Accessing attributes on the href object creates a new href object with | |
993 | the attribute name as prefix: | |
994 | ||
995 | >>> bar_href = href.bar | |
996 | >>> bar_href("blub") | |
997 | '/foo/bar/blub' | |
998 | """ | |
999 | ||
1000 | def __init__(self, base='./', charset='utf-8'): | |
1001 | if not base: | |
1002 | base = './' | |
1003 | self.base = base | |
1004 | self.charset = charset | |
1005 | ||
1006 | def __getattr__(self, name): | |
1007 | if name[:2] == '__': | |
1008 | raise AttributeError(name) | |
1009 | base = self.base | |
1010 | if base[-1:] != '/': | |
1011 | base += '/' | |
1012 | return Href(urlparse.urljoin(base, name), self.charset) | |
1013 | ||
1014 | def __call__(self, *path, **query): | |
1015 | if query: | |
1016 | if path and isinstance(path[-1], dict): | |
1017 | query, path = path[-1], path[:-1] | |
1018 | else: | |
1019 | query = dict([(k.endswith('_') and k[:-1] or k, v) | |
1020 | for k, v in query.items()]) | |
1021 | path = '/'.join([url_quote(x, self.charset) for x in path | |
1022 | if x is not None]).lstrip('/') | |
1023 | rv = self.base | |
1024 | if path: | |
1025 | if not rv.endswith('/'): | |
1026 | rv += '/' | |
1027 | rv = urlparse.urljoin(rv, path) | |
1028 | if query: | |
1029 | rv += '?' + url_encode(query, self.charset) | |
1030 | return str(rv) | |
1031 | ||
1032 | ||
1033 | class cached_property(object): | |
1034 | """ | |
1035 | A decorator that converts a function into a lazy property. The | |
1036 | function wrapped is called the first time to retrieve the result | |
1037 | and than that calculated result is used the next time you access | |
1038 | the value:: | |
1039 | ||
1040 | class Foo(object): | |
1041 | ||
1042 | @cached_property | |
1043 | def foo(self): | |
1044 | # calculate something important here | |
1045 | return 42 | |
611 | 1046 | """ |
612 | 1047 | |
613 | 1048 | def __init__(self, func, name=None, doc=None): |
623 | 1058 | return value |
624 | 1059 | |
625 | 1060 | |
626 | class environ_property(object): | |
1061 | def lazy_property(func, name=None, doc=None): | |
1062 | """Backwards compatibility interface.""" | |
1063 | from warnings import warn | |
1064 | warn(DeprecationWarning('lazy_property is now called cached_property ' | |
1065 | 'because it reflects the purpose better. With ' | |
1066 | 'Werkzeug 0.3 the old name will be unavailable')) | |
1067 | return cached_property(func, name, doc) | |
1068 | ||
1069 | ||
1070 | class _DictAccessorProperty(object): | |
1071 | """ | |
1072 | Baseclass for `environ_property` and `header_property`. | |
1073 | ||
1074 | :internal: | |
1075 | """ | |
1076 | ||
1077 | def __init__(self, name, default=None, load_func=None, dump_func=None, | |
1078 | read_only=False, doc=None): | |
1079 | self.name = name | |
1080 | self.default = default | |
1081 | self.load_func = load_func | |
1082 | self.dump_func = dump_func | |
1083 | self.read_only = read_only | |
1084 | self.__doc__ = doc | |
1085 | ||
1086 | def lookup(self): | |
1087 | raise NotImplementedError() | |
1088 | ||
1089 | def __get__(self, obj, type=None): | |
1090 | if obj is None: | |
1091 | return self | |
1092 | storage = self.lookup(obj) | |
1093 | if self.name not in storage: | |
1094 | return self.default | |
1095 | rv = storage[self.name] | |
1096 | if self.load_func is not None: | |
1097 | try: | |
1098 | rv = self.load_func(rv) | |
1099 | except (ValueError, TypeError): | |
1100 | rv = self.default | |
1101 | return rv | |
1102 | ||
1103 | def __set__(self, obj, value): | |
1104 | if self.read_only: | |
1105 | raise AttributeError('read only property') | |
1106 | if self.dump_func is not None: | |
1107 | value = self.dump_func(value) | |
1108 | self.lookup(obj)[self.name] = value | |
1109 | ||
1110 | def __delete__(self, obj): | |
1111 | if self.read_only: | |
1112 | raise AttributeError('read only property') | |
1113 | self.lookup(obj).pop(self.name, None) | |
1114 | ||
1115 | def __repr__(self): | |
1116 | return '<%s %s>' % ( | |
1117 | self.__class__.__name__, | |
1118 | self.name | |
1119 | ) | |
1120 | ||
1121 | ||
1122 | class environ_property(_DictAccessorProperty): | |
627 | 1123 | """ |
628 | 1124 | Maps request attributes to environment variables. This works not only |
629 | 1125 | for the Werzeug request object, but also any other class with an |
645 | 1141 | `read_only` to False it will block set/delete. |
646 | 1142 | """ |
647 | 1143 | |
648 | def __init__(self, name, default=None, convert=None, read_only=False, | |
649 | doc=None): | |
650 | self.name = name | |
651 | self.default = default | |
652 | self.convert = convert | |
653 | self.read_only = read_only | |
654 | self.__doc__ = doc | |
655 | ||
656 | def __get__(self, obj, type=None): | |
657 | if obj is None: | |
658 | return self | |
659 | rv = obj.environ.get(self.name, self.default) | |
660 | if rv is self.default or self.convert is None: | |
661 | return rv | |
662 | try: | |
663 | return self.convert(rv) | |
664 | except (ValueError, TypeError): | |
665 | return self.default | |
666 | ||
667 | def __set__(self, obj, value): | |
668 | if self.read_only: | |
669 | raise AttributeError('read only property') | |
670 | obj.environ[self.name] = value | |
671 | ||
672 | def __delete__(self, obj): | |
673 | if self.read_only: | |
674 | raise AttributeError('read only property') | |
675 | obj.environ.pop(self.name, None) | |
1144 | def lookup(self, obj): | |
1145 | return obj.environ | |
1146 | ||
1147 | ||
1148 | class header_property(_DictAccessorProperty): | |
1149 | """ | |
1150 | Like `environ_property` but for headers. | |
1151 | """ | |
1152 | ||
1153 | def lookup(self, obj): | |
1154 | return obj.headers | |
1155 | ||
1156 | ||
1157 | class HTMLBuilder(object): | |
1158 | """ | |
1159 | Helper object for HTML generation. | |
1160 | ||
1161 | Per default there are two instances of that class. The `html` one, and | |
1162 | the `xhtml` one for those two dialects. The class uses keyword parameters | |
1163 | and positional parameters to generate small snippets of HTML. | |
1164 | ||
1165 | Keyword parameters are converted to XML/SGML attributes, positional | |
1166 | arguments are used as children. Because Python accepts positional | |
1167 | arguments before keyword arguments it's a good idea to use a list with the | |
1168 | star-syntax for some children: | |
1169 | ||
1170 | >>> html.p(class_='foo', *[html.a('foo', href='foo.html'), ' ', | |
1171 | ... html.a('bar', href='bar.html')]) | |
1172 | '<p class="foo"><a href="foo.html">foo</a> <a href="bar.html">bar</a></p>' | |
1173 | ||
1174 | This class works around some browser limitations and can not be used for | |
1175 | arbitrary SGML/XML generation. For that purpose lxml and similar | |
1176 | libraries exist. | |
1177 | ||
1178 | Calling the builder escapes the string passed: | |
1179 | ||
1180 | >>> html.p(html("<foo>")) | |
1181 | '<p><foo></p>' | |
1182 | """ | |
1183 | ||
1184 | from htmlentitydefs import name2codepoint | |
1185 | _entity_re = re.compile(r'&([^;]+);') | |
1186 | _entities = name2codepoint.copy() | |
1187 | _entities['apos'] = 39 | |
1188 | _empty_elements = set([ | |
1189 | 'area', 'base', 'basefont', 'br', 'col', 'frame', 'hr', 'img', | |
1190 | 'input', 'isindex', 'link', 'meta', 'param' | |
1191 | ]) | |
1192 | _boolean_attributes = set([ | |
1193 | 'selected', 'checked', 'compact', 'declare', 'defer', 'disabled', | |
1194 | 'ismap', 'multiple', 'nohref', 'noresize', 'noshade', 'nowrap' | |
1195 | ]) | |
1196 | _plaintext_elements = set(['textarea']) | |
1197 | _c_like_cdata = set(['script', 'style']) | |
1198 | del name2codepoint | |
1199 | ||
1200 | def __init__(self, dialect): | |
1201 | self._dialect = dialect | |
1202 | ||
1203 | def __call__(self, s): | |
1204 | return escape(s) | |
1205 | ||
1206 | def __getattr__(self, tag): | |
1207 | if tag[:2] == '__': | |
1208 | raise AttributeError(tag) | |
1209 | def proxy(*children, **arguments): | |
1210 | buffer = ['<' + tag] | |
1211 | write = buffer.append | |
1212 | for key, value in arguments.iteritems(): | |
1213 | if value is None: | |
1214 | continue | |
1215 | if key.endswith('_'): | |
1216 | key = key[:-1] | |
1217 | if key in self._boolean_attributes: | |
1218 | value = self._dialect == 'xhtml' and '="%s"' % key or '' | |
1219 | else: | |
1220 | value = '="%s"' % escape(value, True) | |
1221 | write(' ' + key + value) | |
1222 | if not children and tag in self._empty_elements: | |
1223 | write(self._dialect == 'xhtml' and ' />' or '>') | |
1224 | return ''.join(buffer) | |
1225 | write('>') | |
1226 | children_as_string = ''.join(children) | |
1227 | if children_as_string: | |
1228 | if tag in self._plaintext_elements: | |
1229 | children_as_string = escape(children_as_string) | |
1230 | elif tag in self._c_like_cdata and self._dialect == 'xhtml': | |
1231 | children_as_string = '/*<![CDATA[*/%s/*]]>*/' % \ | |
1232 | children_as_string | |
1233 | buffer.extend((children_as_string, '</%s>' % tag)) | |
1234 | return ''.join(buffer) | |
1235 | return proxy | |
676 | 1236 | |
677 | 1237 | def __repr__(self): |
678 | return '<%s %s>' % ( | |
1238 | return '<%s for %r>' % ( | |
679 | 1239 | self.__class__.__name__, |
680 | self.name | |
1240 | self._dialect | |
681 | 1241 | ) |
1242 | ||
1243 | ||
1244 | html = HTMLBuilder('html') | |
1245 | xhtml = HTMLBuilder('xhtml') | |
1246 | ||
1247 | ||
1248 | def get_content_type(mimetype, charset): | |
1249 | """ | |
1250 | Return the full content type string with charset for a mimetype. | |
1251 | ||
1252 | If the mimetype represents text the charset will be appended as charset | |
1253 | parameter, otherwise the mimetype is returned unchanged. | |
1254 | """ | |
1255 | if mimetype.startswith('text/') or \ | |
1256 | mimetype == 'application/xml' or \ | |
1257 | (mimetype.startswith('application/') and | |
1258 | mimetype.endswith('+xml')): | |
1259 | mimetype += '; charset=' + charset | |
1260 | return mimetype | |
682 | 1261 | |
683 | 1262 | |
684 | 1263 | def format_string(string, context): |
699 | 1278 | return _format_re.sub(lookup_arg, string) |
700 | 1279 | |
701 | 1280 | |
702 | def url_decode(s, charset='utf-8'): | |
703 | """ | |
704 | Parse a querystring and return it as `MultiDict`. | |
1281 | def url_decode(s, charset='utf-8', decode_keys=False, include_empty=True): | |
1282 | """ | |
1283 | Parse a querystring and return it as `MultiDict`. Per default only values | |
1284 | are decoded into unicode strings. If `decode_keys` is set to ``True`` the | |
1285 | same will happen for keys. | |
1286 | ||
1287 | Per default a missing value for a key will default to an empty key. If | |
1288 | you don't want that behavior you can set `include_empty` to `False`. | |
705 | 1289 | """ |
706 | 1290 | tmp = [] |
707 | for key, values in cgi.parse_qs(str(s)).iteritems(): | |
1291 | for key, values in cgi.parse_qs(str(s), include_empty).iteritems(): | |
708 | 1292 | for value in values: |
1293 | if decode_keys: | |
1294 | key = key.decode(charset, 'ignore') | |
709 | 1295 | tmp.append((key, value.decode(charset, 'ignore'))) |
710 | 1296 | return MultiDict(tmp) |
711 | 1297 | |
712 | 1298 | |
713 | def url_encode(obj, charset='utf-8'): | |
714 | """Urlencode a dict/MultiDict.""" | |
1299 | def url_encode(obj, charset='utf-8', encode_keys=False): | |
1300 | """ | |
1301 | URL encode a dict/`MultiDict`. If a value is `None` it will not appear in | |
1302 | the result string. Per default only values are encoded into the target | |
1303 | charset strings. If `encode_keys` is set to ``True`` unicode keys are | |
1304 | supported too. | |
1305 | """ | |
715 | 1306 | if obj is None: |
716 | 1307 | items = [] |
717 | 1308 | elif isinstance(obj, MultiDict): |
722 | 1313 | items = obj |
723 | 1314 | tmp = [] |
724 | 1315 | for key, values in items: |
1316 | if encode_keys and isinstance(key, unicode): | |
1317 | key = key.encode(charset) | |
1318 | else: | |
1319 | key = str(key) | |
725 | 1320 | for value in values: |
726 | 1321 | if value is None: |
727 | 1322 | continue |
734 | 1329 | return '&'.join(tmp) |
735 | 1330 | |
736 | 1331 | |
737 | def url_quote(s, charset='utf-8'): | |
1332 | def url_quote(s, charset='utf-8', safe='/:'): | |
738 | 1333 | """ |
739 | 1334 | URL encode a single string with a given encoding. |
740 | 1335 | """ |
741 | 1336 | if isinstance(s, unicode): |
742 | 1337 | s = s.encode(charset) |
743 | return urllib.quote(s) | |
744 | ||
745 | ||
746 | def url_quote_plus(s, charset='utf-8'): | |
1338 | elif not isinstance(s, str): | |
1339 | s = str(s) | |
1340 | return urllib.quote(s, safe=safe) | |
1341 | ||
1342 | ||
1343 | def url_quote_plus(s, charset='utf-8', safe=''): | |
747 | 1344 | """ |
748 | 1345 | URL encode a single string with the given encoding and convert |
749 | 1346 | whitespace to "+". |
750 | 1347 | """ |
751 | 1348 | if isinstance(s, unicode): |
752 | 1349 | s = s.encode(charset) |
753 | return urllib.quote_plus(s) | |
1350 | elif not isinstance(s, str): | |
1351 | s = str(s) | |
1352 | return urllib.quote_plus(s, safe=safe) | |
754 | 1353 | |
755 | 1354 | |
756 | 1355 | def url_unquote(s, charset='utf-8'): |
768 | 1367 | return urllib.unquote_plus(s).decode(charset, 'ignore') |
769 | 1368 | |
770 | 1369 | |
771 | escape = cgi.escape | |
1370 | def url_fix(s, charset='utf-8'): | |
1371 | """ | |
1372 | Sometimes you get an URL by a user that just isn't a real URL because | |
1373 | it contains unsafe characters like ' ' and so on. This function can fix | |
1374 | some of the problems in a similar way browsers handle data entered by the | |
1375 | user: | |
1376 | ||
1377 | >>> url_fix(u'http://de.wikipedia.org/wiki/Elf (Begriffsklärung)') | |
1378 | 'http://de.wikipedia.org/wiki/Elf%20%28Begriffskl%C3%A4rung%29' | |
1379 | ||
1380 | :param charset: The target charset for the URL if the url was given as | |
1381 | unicode string. | |
1382 | """ | |
1383 | if isinstance(s, unicode): | |
1384 | s = s.encode(charset, 'ignore') | |
1385 | scheme, netloc, path, qs, anchor = urlparse.urlsplit(s) | |
1386 | path = urllib.quote(path, '/%') | |
1387 | qs = urllib.quote_plus(qs, ':&=') | |
1388 | return urlparse.urlunsplit((scheme, netloc, path, qs, anchor)) | |
1389 | ||
1390 | ||
1391 | def escape(s, quote=None): | |
1392 | """ | |
1393 | Replace special characters "&", "<" and ">" to HTML-safe sequences. If the | |
1394 | optional flag `quote` is `True`, the quotation mark character (") is also | |
1395 | translated. | |
1396 | ||
1397 | There is a special handling for `None` which escapes to an empty string. | |
1398 | """ | |
1399 | if s is None: | |
1400 | return '' | |
1401 | elif not isinstance(s, basestring): | |
1402 | s = unicode(s) | |
1403 | s = s.replace('&', '&').replace('<', '<').replace('>', '>') | |
1404 | if quote: | |
1405 | s = s.replace('"', """) | |
1406 | return s | |
1407 | ||
1408 | ||
1409 | def unescape(s): | |
1410 | """ | |
1411 | The reverse function of `escape`. This unescapes all the HTML entities, | |
1412 | not only the XML entities inserted by `escape`. | |
1413 | """ | |
1414 | def handle_match(m): | |
1415 | name = m.group(1) | |
1416 | if name in HTMLBuilder._entities: | |
1417 | return unichr(HTMLBuilder._entities[name]) | |
1418 | if name[:2] in ('#x', '#X'): | |
1419 | try: | |
1420 | return unichr(int(name[2:], 16)) | |
1421 | except ValueError: | |
1422 | return u'' | |
1423 | elif name.startswith('#'): | |
1424 | try: | |
1425 | return unichr(int(name[1:])) | |
1426 | except ValueError: | |
1427 | return u'' | |
1428 | return u'' | |
1429 | return _entity_re.sub(handle_match, s) | |
772 | 1430 | |
773 | 1431 | |
774 | 1432 | def get_host(environ): |
775 | 1433 | """ |
776 | Return the real host for the given environment. | |
1434 | Return the real host for the given WSGI enviornment. This takes care | |
1435 | of the `X-Forwarded-Host` header. | |
777 | 1436 | """ |
778 | 1437 | if 'HTTP_X_FORWARDED_HOST' in environ: |
779 | 1438 | return environ['HTTP_X_FORWARDED_HOST'] |
789 | 1448 | def get_current_url(environ, root_only=False, strip_querystring=False, |
790 | 1449 | host_only=False): |
791 | 1450 | """ |
792 | Recreate the URL of the current request. | |
1451 | A handy helper function that recreates the full URL for the current | |
1452 | request or parts of it. Here an example: | |
1453 | ||
1454 | >>> env = create_environ("/?param=foo", "http://localhost/script") | |
1455 | >>> get_current_url(env) | |
1456 | 'http://localhost/script/?param=foo' | |
1457 | >>> get_current_url(env, root_only=True) | |
1458 | 'http://localhost/script/' | |
1459 | >>> get_current_url(env, host_only=True) | |
1460 | 'http://localhost/' | |
1461 | >>> get_current_url(env, strip_querystring=True) | |
1462 | 'http://localhost/script/' | |
793 | 1463 | """ |
794 | 1464 | tmp = [environ['wsgi.url_scheme'], '://'] |
795 | 1465 | cat = tmp.append |
813 | 1483 | return ''.join(tmp) |
814 | 1484 | |
815 | 1485 | |
816 | def cookie_date(expires, _date_delim='-'): | |
1486 | def cookie_date(expires=None, _date_delim='-'): | |
817 | 1487 | """ |
818 | 1488 | Formats the time to ensure compatibility with Netscape's cookie standard. |
819 | 1489 | |
820 | 1490 | Accepts a floating point number expressed in seconds since the epoc in, a |
821 | datetime object or a timetuple. All times in UTC. | |
822 | ||
823 | Outputs a string in the format 'Wdy, DD-Mon-YYYY HH:MM:SS GMT'. | |
824 | """ | |
825 | if isinstance(expires, datetime): | |
1491 | datetime object or a timetuple. All times in UTC. The `parse_date` | |
1492 | function in `werkzeug.http` can be used to parse such a date. | |
1493 | ||
1494 | Outputs a string in the format ``Wdy, DD-Mon-YYYY HH:MM:SS GMT``. | |
1495 | """ | |
1496 | if expires is None: | |
1497 | expires = gmtime() | |
1498 | elif isinstance(expires, datetime): | |
826 | 1499 | expires = expires.utctimetuple() |
827 | elif isinstance(expires, (int, long)): | |
1500 | elif isinstance(expires, (int, long, float)): | |
828 | 1501 | expires = gmtime(expires) |
829 | 1502 | |
830 | 1503 | return '%s, %02d%s%s%s%s %02d:%02d:%02d GMT' % ( |
841 | 1514 | ) |
842 | 1515 | |
843 | 1516 | |
844 | def http_date(timestamp): | |
1517 | def parse_cookie(header, charset='utf-8'): | |
1518 | """Parse a cookie. Either from a string or WSGI environ.""" | |
1519 | if isinstance(header, dict): | |
1520 | header = header.get('HTTP_COOKIE', '') | |
1521 | cookie = _ExtendedCookie() | |
1522 | cookie.load(header) | |
1523 | result = {} | |
1524 | ||
1525 | # decode to unicode and skip broken items. Our extended morsel | |
1526 | # and extended cookie will catch CookieErrors and convert them to | |
1527 | # `None` items which we have to skip here. | |
1528 | for key, value in cookie.iteritems(): | |
1529 | if value.value is not None: | |
1530 | result[key] = value.value.decode(charset, 'ignore') | |
1531 | ||
1532 | return result | |
1533 | ||
1534 | ||
1535 | def dump_cookie(key, value='', max_age=None, expires=None, path='/', | |
1536 | domain=None, secure=None, httponly=False, charset='utf-8', | |
1537 | sync_expires=True): | |
1538 | """ | |
1539 | Creates a new Set-Cookie header without the ``Set-Cookie`` prefix | |
1540 | The parameters are the same as in the cookie Morsel object in the | |
1541 | Python standard library but it accepts unicode data too. | |
1542 | ||
1543 | :param max_age: should be a number of seconds, or `None` (default) if | |
1544 | the cookie should last only as long as the client's | |
1545 | browser session. Additionally `timedelta` objects | |
1546 | are accepted too. | |
1547 | :param expires: should be a `datetime` object or unix timestamp. | |
1548 | :param path: limits the cookie to a given path, per default it will | |
1549 | span the whole domain. | |
1550 | :param domain: Use this if you want to set a cross-domain cookie. For | |
1551 | example, ``domain=".example.com"`` will set a cookie | |
1552 | that is readable by the domain ``www.example.com``, | |
1553 | ``foo.example.com`` etc. Otherwise, a cookie will only | |
1554 | be readable by the domain that set it. | |
1555 | :param secure: The cookie will only be available via HTTPS | |
1556 | :param httponly: disallow JavaScript to access the cookie. This is an | |
1557 | extension to the cookie standard and probably not | |
1558 | supported by all browsers. | |
1559 | :param charset: the encoding for unicode values. | |
1560 | :param sync_expires: automatically set expires if max_age is defined | |
1561 | but expires not. | |
1562 | """ | |
1563 | try: | |
1564 | key = str(key) | |
1565 | except UnicodeError: | |
1566 | raise TypeError('invalid key %r' % key) | |
1567 | if isinstance(value, unicode): | |
1568 | value = value.encode(charset) | |
1569 | morsel = _ExtendedMorsel(key, value) | |
1570 | if isinstance(max_age, timedelta): | |
1571 | max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds | |
1572 | if expires is not None: | |
1573 | if not isinstance(expires, basestring): | |
1574 | expires = cookie_date(expires) | |
1575 | morsel['expires'] = expires | |
1576 | elif max_age is not None and sync_expires: | |
1577 | morsel['expires'] = cookie_date(time() + max_age) | |
1578 | for k, v in (('path', path), ('domain', domain), ('secure', secure), | |
1579 | ('max-age', max_age), ('httponly', httponly)): | |
1580 | if v is not None and v is not False: | |
1581 | morsel[k] = str(v) | |
1582 | return morsel.output(header='').lstrip() | |
1583 | ||
1584 | ||
1585 | def http_date(timestamp=None): | |
845 | 1586 | """ |
846 | 1587 | Formats the time to match the RFC1123 date format. |
847 | 1588 | |
848 | 1589 | Accepts a floating point number expressed in seconds since the epoc in, a |
849 | datetime object or a timetuple. All times in UTC. | |
850 | ||
851 | Outputs a string in the format 'Wdy, DD Mon YYYY HH:MM:SS GMT'. | |
1590 | datetime object or a timetuple. All times in UTC. The `parse_date` | |
1591 | function in `werkzeug.http` can be used to parse such a date. | |
1592 | ||
1593 | Outputs a string in the format ``Wdy, DD Mon YYYY HH:MM:SS GMT``. | |
852 | 1594 | """ |
853 | 1595 | return cookie_date(timestamp, ' ') |
854 | 1596 | |
874 | 1616 | return response |
875 | 1617 | |
876 | 1618 | |
1619 | def append_slash_redirect(environ, code=301): | |
1620 | """ | |
1621 | Redirect to the same URL but with a slash appended. The behavior | |
1622 | of this function is undefined if the path ends with a slash already. | |
1623 | """ | |
1624 | new_path = environ['PATH_INFO'].strip('/') + '/' | |
1625 | query_string = environ['QUERY_STRING'] | |
1626 | if query_string: | |
1627 | new_path += '?' + query_string | |
1628 | if not new_path.startswith('/'): | |
1629 | new_path = '/' + new_path | |
1630 | return redirect(new_path) | |
1631 | ||
1632 | ||
1633 | def responder(f): | |
1634 | """ | |
1635 | Marks a function as responder. Decorate a function with it and it | |
1636 | will automatically call the return value as WSGI application. | |
1637 | ||
1638 | Example:: | |
1639 | ||
1640 | @responder | |
1641 | def application(environ, start_response): | |
1642 | return Response('Hello World!') | |
1643 | """ | |
1644 | return _patch_wrapper(f, lambda *a: f(*a)(*a[-2:])) | |
1645 | ||
1646 | ||
1647 | def import_string(import_name, silent=False): | |
1648 | """ | |
1649 | Imports an object based on a string. This use useful if you want to use | |
1650 | import paths as endpoints or something similar. An import path can | |
1651 | be specified either in dotted notation (``xml.sax.saxutils.escape``) | |
1652 | or with a colon as object delimiter (``xml.sax.saxutils:escape``). | |
1653 | ||
1654 | If the `silent` is True the return value will be `None` if the import | |
1655 | fails. | |
1656 | ||
1657 | :return: imported object | |
1658 | """ | |
1659 | try: | |
1660 | if ':' in import_name: | |
1661 | module, obj = import_name.split(':', 1) | |
1662 | elif '.' in import_name: | |
1663 | items = import_name.split('.') | |
1664 | module = '.'.join(items[:-1]) | |
1665 | obj = items[-1] | |
1666 | else: | |
1667 | return __import__(import_name) | |
1668 | return getattr(__import__(module, None, None, [obj]), obj) | |
1669 | except (ImportError, AttributeError): | |
1670 | if not silent: | |
1671 | raise | |
1672 | ||
1673 | ||
1674 | def _iter_modules(path): | |
1675 | import pkgutil | |
1676 | if hasattr(pkgutil, 'iter_modules'): | |
1677 | for importer, modname, ispkg in pkgutil.iter_modules(path): | |
1678 | yield modname, ispkg | |
1679 | return | |
1680 | from inspect import getmodulename | |
1681 | from pydoc import ispackage | |
1682 | found = set() | |
1683 | for path in path: | |
1684 | for filename in os.listdir(path): | |
1685 | p = os.path.join(path, filename) | |
1686 | modname = getmodulename(filename) | |
1687 | if modname and modname != '__init__': | |
1688 | if modname not in found: | |
1689 | found.add(modname) | |
1690 | yield modname, ispackage(modname) | |
1691 | ||
1692 | ||
1693 | def find_modules(import_path, include_packages=False, recursive=False): | |
1694 | """ | |
1695 | Find all the modules below a package. This can be useful to automatically | |
1696 | import all views / controllers so that their metaclasses / function | |
1697 | decorators have a chance to register themselves on the application. | |
1698 | ||
1699 | Packages are not returned unless `include_packages` is `True`. This can | |
1700 | also recursively list modules but in that case it will import all the | |
1701 | packages to get the correct load path of that module. | |
1702 | ||
1703 | :return: generator | |
1704 | """ | |
1705 | module = import_string(import_path) | |
1706 | path = getattr(module, '__path__', None) | |
1707 | if path is None: | |
1708 | raise ValueError('%r is not a package' % import_path) | |
1709 | basename = module.__name__ + '.' | |
1710 | for modname, ispkg in _iter_modules(path): | |
1711 | modname = basename + modname | |
1712 | if ispkg: | |
1713 | if include_packages: | |
1714 | yield modname | |
1715 | if recursive: | |
1716 | for item in find_modules(modname, include_packages, True): | |
1717 | yield item | |
1718 | else: | |
1719 | yield modname | |
1720 | ||
1721 | ||
877 | 1722 | def create_environ(path='/', base_url=None, query_string=None, method='GET', |
878 | 1723 | input_stream=None, content_type=None, content_length=0, |
879 | 1724 | errors_stream=None, multithread=False, |
881 | 1726 | """ |
882 | 1727 | Create a new WSGI environ dict based on the values passed. The first |
883 | 1728 | parameter should be the path of the request which defaults to '/'. |
884 | The second one can either be a absolute path (in that case the url | |
1729 | The second one can either be a absolute path (in that case the URL | |
885 | 1730 | host is localhost:80) or a full path to the request with scheme, |
886 | 1731 | netloc port and the path to the script. |
1732 | ||
1733 | If the `path` contains a query string it will be used, even if the | |
1734 | `query_string` parameter was given. If it does not contain one | |
1735 | the `query_string` parameter is used as querystring. In that case | |
1736 | it can either be a dict, MultiDict or string. | |
1737 | ||
1738 | The following options exist: | |
1739 | ||
1740 | `method` | |
1741 | The request method. Defaults to `GET` | |
1742 | ||
1743 | `input_stream` | |
1744 | The input stream. Defaults to an empty read only stream. | |
1745 | ||
1746 | `content_type` | |
1747 | The content type for this request. Default is an empty content | |
1748 | type. | |
1749 | ||
1750 | `content_length` | |
1751 | The value for the content length header. Defaults to 0. | |
1752 | ||
1753 | `errors_stream` | |
1754 | The wsgi.errors stream. Defaults to `sys.stderr`. | |
1755 | ||
1756 | `multithread` | |
1757 | The multithreaded flag for the WSGI Environment. Defaults to `False`. | |
1758 | ||
1759 | `multiprocess` | |
1760 | The multiprocess flag for the WSGI Environment. Defaults to `False`. | |
1761 | ||
1762 | `run_once` | |
1763 | The run_once flag for the WSGI Environment. Defaults to `False`. | |
887 | 1764 | """ |
888 | 1765 | if base_url is not None: |
889 | 1766 | scheme, netloc, script_name, qs, fragment = urlparse.urlsplit(base_url) |
935 | 1812 | """ |
936 | 1813 | Return a tuple in the form (app_iter, status, headers) of the application |
937 | 1814 | output. This works best if you pass it an application that returns a |
938 | generator all the time. | |
1815 | iterator all the time. | |
939 | 1816 | |
940 | 1817 | Sometimes applications may use the `write()` callable returned |
941 | 1818 | by the `start_response` function. This tries to resolve such edge |
942 | 1819 | cases automatically. But if you don't get the expected output you |
943 | 1820 | should set `buffered` to `True` which enforces buffering. |
944 | """ | |
945 | # TODO: only read until a response is set, then return a closing | |
946 | # iterator that yields the buffer first and then the data. | |
1821 | ||
1822 | If passed an invalid WSGI application the behavior of this function is | |
1823 | undefined. Never pass non-conforming WSGI applications to this function. | |
1824 | """ | |
947 | 1825 | response = [] |
948 | 1826 | buffer = [] |
949 | 1827 | |
955 | 1833 | |
956 | 1834 | app_iter = app(environ, start_response) |
957 | 1835 | |
958 | if buffered or buffer or not response: | |
1836 | # when buffering we emit the close call early and conver the | |
1837 | # application iterator into a regular list | |
1838 | if buffered: | |
1839 | close_func = getattr(app_iter, 'close', None) | |
959 | 1840 | try: |
960 | buffer.extend(app_iter) | |
1841 | app_iter = list(app_iter) | |
961 | 1842 | finally: |
962 | if hasattr(app_iter, 'close'): | |
963 | app_iter.close() | |
964 | app_iter = buffer | |
1843 | if close_func is not None: | |
1844 | close_func() | |
1845 | ||
1846 | # otherwise we iterate the application iter until we have | |
1847 | # a response, chain the already received data with the already | |
1848 | # collected data and wrap it in a new `ClosingIterator` if | |
1849 | # we have a close callable. | |
1850 | else: | |
1851 | while not response: | |
1852 | buffer.append(app_iter.next()) | |
1853 | if buffer: | |
1854 | app_iter = chain(buffer, app_iter) | |
1855 | close_func = getattr(app_iter, 'close', None) | |
1856 | if close_func is not None: | |
1857 | app_iter = ClosingIterator(app_iter, close_func) | |
965 | 1858 | |
966 | 1859 | return app_iter, response[0], response[1] |
2 | 2 | werkzeug.wrappers |
3 | 3 | ~~~~~~~~~~~~~~~~~ |
4 | 4 | |
5 | This module provides simple wrappers around `environ`, | |
6 | `start_response` and `wsgi.input`. | |
7 | ||
8 | :copyright: 2007 by Armin Ronacher, Georg Brandl. | |
5 | The wrappers are simple request and response objects which you can | |
6 | subclass to do whatever you want them to do. The request object contains | |
7 | the information transmitted by the client (webbrowser) and the response | |
8 | object contains all the information sent back to the browser. | |
9 | ||
10 | An important detail is that the request object is created with the WSGI | |
11 | environ and will act as high-level proxy whereas the response object is an | |
12 | actual WSGI application. | |
13 | ||
14 | Like everything else in Werkzeug these objects will work correctly with | |
15 | unicode data. Incoming form data parsed by the response object will be | |
16 | decoded into an unicode object if possible and if it makes sense. | |
17 | ||
18 | ||
19 | :copyright: 2007-2008 by Armin Ronacher, Georg Brandl. | |
9 | 20 | :license: BSD, see LICENSE for more details. |
10 | 21 | """ |
11 | 22 | import cgi |
12 | 23 | import tempfile |
13 | 24 | import urlparse |
14 | try: | |
15 | from hashlib import md5 | |
16 | except ImportError: | |
17 | from md5 import new as md5 | |
18 | from Cookie import SimpleCookie | |
25 | from datetime import datetime, timedelta | |
19 | 26 | from werkzeug.http import HTTP_STATUS_CODES, Accept, CacheControl, \ |
20 | parse_accept_header, parse_cache_control_header | |
27 | parse_accept_header, parse_cache_control_header, parse_etags, \ | |
28 | parse_date, generate_etag, is_resource_modified, unquote_etag, \ | |
29 | quote_etag, parse_set_header | |
21 | 30 | from werkzeug.utils import MultiDict, CombinedMultiDict, FileStorage, \ |
22 | Headers, EnvironHeaders, lazy_property, environ_property, \ | |
31 | Headers, EnvironHeaders, cached_property, environ_property, \ | |
23 | 32 | get_current_url, create_environ, url_encode, run_wsgi_app, get_host, \ |
24 | cookie_date, http_date, escape, _empty_stream | |
33 | cookie_date, parse_cookie, dump_cookie, http_date, escape, \ | |
34 | header_property, get_content_type, _empty_stream | |
25 | 35 | |
26 | 36 | |
27 | 37 | class _StorageHelper(cgi.FieldStorage): |
62 | 72 | |
63 | 73 | class BaseRequest(object): |
64 | 74 | """ |
65 | Base Request class. | |
75 | Very basic request object. This does not implement advanced stuff like | |
76 | entity tag parsing or cache controls. The request object is created with | |
77 | the WSGI environment as first argument and will add itself to the WSGI | |
78 | environment as ``'werkzeug.request'`` unless it's created with | |
79 | `populate_request` set to False. | |
80 | ||
81 | There are a couple of mixins available that add additional functionality | |
82 | to the request object, there is also a class called `Request` which | |
83 | subclasses `BaseRequest` and all the important mixins. | |
84 | ||
85 | It's a good idea to create a custom subclass of the `BaseRequest` and add | |
86 | missing functionality either via mixins or direct implementation. Here | |
87 | an example for such subclasses:: | |
88 | ||
89 | from werkzeug import BaseRequest, ETagRequestMixin | |
90 | ||
91 | class Request(BaseRequest, ETagRequestMixin): | |
92 | pass | |
93 | ||
94 | Request objects should be considered *read only*. Even though the object | |
95 | doesn't enforce read only access everywhere you should never modify any | |
96 | data on the object itself unless you know exactly what you are doing. | |
66 | 97 | """ |
67 | 98 | charset = 'utf-8' |
68 | 99 | is_behind_proxy = False |
69 | 100 | |
70 | 101 | def __init__(self, environ, populate_request=True): |
102 | """ | |
103 | Per default the request object will be added to the WSGI enviornment | |
104 | as `werkzeug.request` to support the debugging system. If you don't | |
105 | want that, set `populate_request` to `False`. | |
106 | """ | |
71 | 107 | self.environ = environ |
72 | 108 | if populate_request: |
73 | 109 | self.environ['werkzeug.request'] = self |
75 | 111 | |
76 | 112 | def from_values(cls, path='/', base_url=None, query_string=None, **options): |
77 | 113 | """ |
78 | Create a new request object based on the values provided. If | |
79 | environ is given missing values are filled from there. This method | |
80 | is useful for small scripts when you need to simulate a request | |
81 | from an url. Do not use this method for unittesting, there is a | |
82 | full featured client object in `werkzeug.test` that allows to create | |
83 | multipart requests etc. | |
114 | Create a new request object based on the values provided. If environ | |
115 | is given missing values are filled from there. This method is useful | |
116 | for small scripts when you need to simulate a request from an URL. Do | |
117 | not use this method for unittesting, there is a full featured client | |
118 | object in `werkzeug.test` that allows to create multipart requests | |
119 | etc. | |
120 | ||
121 | This accepts the same options as the `create_environ` function from the | |
122 | utils module and additionally an `environ` parameter that can contain | |
123 | values which will override the values from dict returned by | |
124 | `create_environ`. | |
125 | ||
126 | Additionally a dict passed to `query_string` will be encoded in the | |
127 | request class charset. | |
128 | ||
129 | :return: request object | |
84 | 130 | """ |
85 | 131 | if isinstance(query_string, dict): |
86 | 132 | query_string = url_encode(query_string, cls.charset) |
94 | 140 | from_values = classmethod(from_values) |
95 | 141 | |
96 | 142 | def _get_file_stream(self): |
97 | """Called to get a stream for the file upload. | |
143 | """ | |
144 | Called to get a stream for the file upload. | |
98 | 145 | |
99 | 146 | This must provide a file-like class with `read()`, `readline()` |
100 | and `seek()` methods that is both writeable and readable.""" | |
147 | and `seek()` methods that is both writeable and readable. | |
148 | ||
149 | The default implementation returns a temporary file. | |
150 | """ | |
101 | 151 | return tempfile.TemporaryFile('w+b') |
102 | 152 | |
103 | def _load_post_data(self): | |
104 | """Method used internally to retrieve submitted data.""" | |
153 | def _load_form_data(self): | |
154 | """ | |
155 | Method used internally to retrieve submitted data. After calling | |
156 | this sets `_form` and `_files` on the request object to multi dicts | |
157 | filled with the incoming form data. As a matter of fact the input | |
158 | stream will be empty afterwards. | |
159 | ||
160 | :internal: | |
161 | """ | |
105 | 162 | self._data_stream = _empty_stream |
106 | post = [] | |
163 | form = [] | |
107 | 164 | files = [] |
108 | 165 | if self.environ['REQUEST_METHOD'] in ('POST', 'PUT'): |
109 | 166 | storage = _StorageHelper(self.environ, self._get_file_stream) |
118 | 175 | if getattr(item, 'filename', None) is not None: |
119 | 176 | fn = item.filename.decode(self.charset, 'ignore') |
120 | 177 | # fix stupid IE bug (IE6 sends the whole path) |
121 | if fn[1:3] == ':\\': | |
178 | if fn[1:3] == ':\\' or fn[:2] == '\\\\': | |
122 | 179 | fn = fn.split('\\')[-1] |
123 | files.append((key, FileStorage(key, fn, item.type, | |
124 | item.length, item.file))) | |
180 | files.append((key, FileStorage(item.file, fn, | |
181 | key, item.type, item.length))) | |
125 | 182 | else: |
126 | post.append((key, item.value.decode(self.charset, | |
183 | form.append((key, item.value.decode(self.charset, | |
127 | 184 | 'ignore'))) |
128 | self._form = MultiDict(post) | |
185 | self._form = MultiDict(form) | |
129 | 186 | self._files = MultiDict(files) |
130 | 187 | |
131 | 188 | def stream(self): |
132 | 189 | """ |
133 | 190 | The parsed stream if the submitted data was not multipart or |
134 | urlencoded form data. | |
191 | urlencoded form data. This stream is the stream left by the CGI | |
192 | module after parsing. This is *not* the WSGI input stream. | |
135 | 193 | """ |
136 | 194 | if self._data_stream is None: |
137 | self._load_post_data() | |
195 | self._load_form_data() | |
138 | 196 | return self._data_stream |
139 | 197 | stream = property(stream, doc=stream) |
140 | 198 | input_stream = environ_property('wsgi.input', 'The WSGI input stream.') |
141 | 199 | |
142 | 200 | def args(self): |
143 | """URL parameters""" | |
201 | """The parsed URL parameters as `MultiDict`.""" | |
144 | 202 | items = [] |
145 | 203 | qs = self.environ.get('QUERY_STRING', '') |
146 | 204 | for key, values in cgi.parse_qs(qs, True).iteritems(): |
148 | 206 | value = value.decode(self.charset, 'ignore') |
149 | 207 | items.append((key, value)) |
150 | 208 | return MultiDict(items) |
151 | args = lazy_property(args) | |
209 | args = cached_property(args) | |
152 | 210 | |
153 | 211 | def data(self): |
154 | 212 | """ |
155 | 213 | This reads the buffered incoming data from the client into the string. |
214 | Usually it's a bad idea to access `data` because a client could send | |
215 | dozens of megabytes or more to cause memory problems on the server. | |
156 | 216 | """ |
157 | 217 | return self.stream.read() |
158 | data = lazy_property(data) | |
218 | data = cached_property(data) | |
159 | 219 | |
160 | 220 | def form(self): |
161 | 221 | """ |
165 | 225 | uses a dict internally and loses the ordering. |
166 | 226 | """ |
167 | 227 | if not hasattr(self, '_form'): |
168 | self._load_post_data() | |
228 | self._load_form_data() | |
169 | 229 | return self._form |
170 | form = lazy_property(form) | |
230 | form = property(form, doc=form.__doc__) | |
171 | 231 | |
172 | 232 | def values(self): |
173 | """combined multi dict for `args` and `form`""" | |
233 | """Combined multi dict for `args` and `form`""" | |
174 | 234 | return CombinedMultiDict([self.args, self.form]) |
175 | values = lazy_property(values) | |
235 | values = cached_property(values) | |
176 | 236 | |
177 | 237 | def files(self): |
178 | """File uploads.""" | |
238 | """ | |
239 | A `MultiDict` containing all uploaded files. Each key in | |
240 | `files` is the name from the ``<input type="file" name="" />``. Each | |
241 | value in `files` is a Werkzeug `FileStorage` object. | |
242 | """ | |
179 | 243 | if not hasattr(self, '_files'): |
180 | self._load_post_data() | |
244 | self._load_form_data() | |
181 | 245 | return self._files |
182 | files = lazy_property(files) | |
246 | files = property(files, doc=files.__doc__) | |
183 | 247 | |
184 | 248 | def cookies(self): |
185 | """Stored Cookies.""" | |
186 | cookie = SimpleCookie() | |
187 | cookie.load(self.environ.get('HTTP_COOKIE', '')) | |
188 | result = {} | |
189 | for key, value in cookie.iteritems(): | |
190 | result[key] = value.value.decode(self.charset, 'ignore') | |
191 | return result | |
192 | cookies = lazy_property(cookies) | |
249 | """The retreived cookie values as regular dictionary.""" | |
250 | return parse_cookie(self.environ, self.charset) | |
251 | cookies = cached_property(cookies) | |
193 | 252 | |
194 | 253 | def headers(self): |
195 | """The headers from the WSGI environ.""" | |
254 | """The headers from the WSGI environ as immutable `EnvironHeaders`.""" | |
196 | 255 | return EnvironHeaders(self.environ) |
197 | headers = lazy_property(headers) | |
198 | ||
199 | def accept_mimetypes(self): | |
200 | """List of mimetypes this client supports.""" | |
201 | if not 'HTTP_ACCEPT' in self.environ: | |
202 | return Accept(None) | |
203 | return parse_accept_header(self.environ['HTTP_ACCEPT']) | |
204 | accept_mimetypes = lazy_property(accept_mimetypes) | |
205 | ||
206 | def accept_charsets(self): | |
207 | """list of charsets this client supports.""" | |
208 | if not 'HTTP_ACCEPT_CHARSET' in self.environ: | |
209 | return Accept(None) | |
210 | return parse_accept_header(self.environ['HTTP_ACCEPT_CHARSET']) | |
211 | accept_charsets = lazy_property(accept_charsets) | |
212 | ||
213 | def accept_encodings(self): | |
214 | """ | |
215 | List of encodings this client accepts. Encodings in a HTTP term are | |
216 | compression encodings such as gzip. For charsets have a look at | |
217 | `accept_charset`. | |
218 | """ | |
219 | if not 'HTTP_ACCEPT_ENCODING' in self.environ: | |
220 | return Accept(None) | |
221 | return parse_accept_header(self.environ['HTTP_ACCEPT_ENCODING']) | |
222 | accept_encodings = lazy_property(accept_encodings) | |
223 | ||
224 | def accept_languages(self): | |
225 | """List of languages this client accepts.""" | |
226 | if not 'HTTP_ACCEPT_LANGUAGE' in self.environ: | |
227 | return Accept(None) | |
228 | return parse_accept_header(self.environ['HTTP_ACCEPT_LANGUAGE']) | |
229 | accept_languages = lazy_property(accept_languages) | |
230 | ||
231 | def cache_control(self): | |
232 | """A `CacheControl` object for the incoming cache control headers.""" | |
233 | if not 'HTTP_CACHE_CONTROL' in self.environ: | |
234 | return CacheControl(None) | |
235 | return parse_cache_control_header(self.environ['HTTP_CACHE_CONTROL']) | |
256 | headers = cached_property(headers) | |
236 | 257 | |
237 | 258 | def path(self): |
238 | """Requested path.""" | |
259 | """ | |
260 | Requested path as unicode. This works a bit like the regular path | |
261 | info in the WSGI environment but will always include a leading slash, | |
262 | even if the URL root is accessed. | |
263 | """ | |
239 | 264 | path = '/' + (self.environ.get('PATH_INFO') or '').lstrip('/') |
240 | 265 | return path.decode(self.charset, 'ignore') |
241 | path = lazy_property(path) | |
266 | path = cached_property(path) | |
242 | 267 | |
243 | 268 | def script_root(self): |
244 | """The root path of the script.""" | |
269 | """The root path of the script without the trailing slash.""" | |
245 | 270 | path = (self.environ.get('SCRIPT_NAME') or '').rstrip('/') |
246 | 271 | return path.decode(self.charset, 'ignore') |
247 | script_root = lazy_property(script_root) | |
272 | script_root = cached_property(script_root) | |
248 | 273 | |
249 | 274 | def url(self): |
250 | 275 | """The reconstructed current URL""" |
251 | 276 | return get_current_url(self.environ) |
252 | url = lazy_property(url) | |
277 | url = cached_property(url) | |
253 | 278 | |
254 | 279 | def base_url(self): |
255 | 280 | """Like `url` but without the querystring""" |
256 | 281 | return get_current_url(self.environ, strip_querystring=True) |
257 | base_url = lazy_property(base_url) | |
282 | base_url = cached_property(base_url) | |
258 | 283 | |
259 | 284 | def url_root(self): |
260 | 285 | """The full URL root (with hostname), this is the application root.""" |
261 | 286 | return get_current_url(self.environ, True) |
262 | url_root = lazy_property(url_root) | |
287 | url_root = cached_property(url_root) | |
263 | 288 | |
264 | 289 | def host_url(self): |
265 | 290 | """Just the host with scheme.""" |
266 | 291 | return get_current_url(self.environ, host_only=True) |
267 | host_url = lazy_property(host_url) | |
292 | host_url = cached_property(host_url) | |
268 | 293 | |
269 | 294 | def host(self): |
270 | 295 | """Just the host including the port if available.""" |
271 | 296 | return get_host(self.environ) |
272 | host = lazy_property(host) | |
273 | ||
274 | def is_secure(self): | |
275 | """True if the request is secure.""" | |
276 | return self.environ['wsgi.url_scheme'] == 'https' | |
277 | is_secure = property(is_secure, doc=is_secure.__doc__) | |
278 | ||
279 | query_string = environ_property('QUERY_STRING', '', read_only=True) | |
280 | remote_addr = environ_property('REMOTE_ADDR', read_only=True) | |
281 | method = environ_property('REQUEST_METHOD', 'GET', read_only=True) | |
297 | host = cached_property(host) | |
298 | ||
299 | query_string = environ_property('QUERY_STRING', '', read_only=True, doc= | |
300 | '''The URL parameters as raw bytestring.''') | |
301 | method = environ_property('REQUEST_METHOD', 'GET', read_only=True, doc= | |
302 | '''The transmission method. (For example ``'GET'`` or ``'POST'``).''') | |
282 | 303 | |
283 | 304 | def access_route(self): |
284 | 305 | """ |
291 | 312 | elif 'REMOTE_ADDR' in self.environ: |
292 | 313 | return [self.environ['REMOTE_ADDR']] |
293 | 314 | return [] |
294 | access_route = lazy_property(access_route) | |
315 | access_route = cached_property(access_route) | |
295 | 316 | |
296 | 317 | def remote_addr(self): |
297 | 318 | """The remote address of the client.""" |
300 | 321 | return self.environ.get('REMOTE_ADDR') |
301 | 322 | remote_addr = property(remote_addr) |
302 | 323 | |
303 | def is_xhr(self): | |
304 | """ | |
324 | is_xhr = property(lambda x: x.environ.get('X_REQUESTED_WITH') == | |
325 | 'XmlHttpRequest', doc=''' | |
305 | 326 | True if the request was triggered via an JavaScript XMLHttpRequest. |
306 | 327 | This only works with libraries that support the X-Requested-With |
307 | 328 | header and set it to "XMLHttpRequest". Libraries that do that are |
308 | prototype, jQuery and Mochikit. | |
309 | """ | |
310 | return self.environ.get('X_REQUESTED_WITH') == 'XmlHttpRequest' | |
311 | is_xhr = property(is_xhr, doc=is_xhr.__doc__) | |
329 | prototype, jQuery and Mochikit and probably some more.''') | |
330 | is_secure = property(lambda x: x.environ['wsgi.url_scheme'] == 'https', | |
331 | doc='`True` if the request is secure.') | |
332 | is_multithread = environ_property('wsgi.multithread', doc=''' | |
333 | boolean that is `True` if the application is served by | |
334 | a multithreaded WSGI server.''') | |
335 | is_multiprocess = environ_property('wsgi.multiprocess', doc=''' | |
336 | boolean that is `True` if the application is served by | |
337 | a WSGI server that spawns multiple processes.''') | |
338 | is_run_once = environ_property('wsgi.run_once', doc=''' | |
339 | boolean that is `True` if the application will be executed only | |
340 | once in a process lifetime. This is the case for CGI for example, | |
341 | but it's not guaranteed that the exeuction only happens one time.''') | |
312 | 342 | |
313 | 343 | |
314 | 344 | class BaseResponse(object): |
315 | 345 | """ |
316 | Base response class. | |
346 | Base response class. The most important fact about a response object is | |
347 | that it's a regular WSGI application. It's initialized with a couple of | |
348 | response parameters (headers, body, status code etc.) and will start a | |
349 | valid WSGI response when called with the environ and start response | |
350 | callable. | |
351 | ||
352 | Because it's a WSGI application itself processing usually ends before the | |
353 | actual response is sent to the server. This helps debugging systems | |
354 | because they can catch all the exceptions before responses are started. | |
355 | ||
356 | Here a small example WSGI application that takes advantage of the | |
357 | response objects:: | |
358 | ||
359 | from werkzeug import BaseResponse as Response | |
360 | ||
361 | def index(): | |
362 | return Response('Index page') | |
363 | ||
364 | def application(environ, start_response): | |
365 | path = environ.get('PATH_INFO') or '/' | |
366 | if path == '/': | |
367 | response = index() | |
368 | else: | |
369 | response = Response('Not Found', status=404) | |
370 | return response(environ, start_response) | |
371 | ||
372 | Like `BaseRequest` which object is lacking a lot of functionality | |
373 | implemented in mixins. This gives you a better control about the actual | |
374 | API of your response objects, so you can create subclasses and add custom | |
375 | functionality. A full featured response object is available as `Response` | |
376 | which implements a couple of useful mixins. | |
377 | ||
378 | To enforce a new type of already existing responses you can use the | |
379 | `force_type` method. This is useful if you're working with different | |
380 | subclasses of response objects and you want to post process them with a | |
381 | know interface. | |
317 | 382 | """ |
318 | 383 | charset = 'utf-8' |
384 | default_status = 200 | |
319 | 385 | default_mimetype = 'text/plain' |
320 | 386 | |
321 | def __init__(self, response=None, status=200, headers=None, mimetype=None, | |
322 | content_type=None): | |
387 | def __init__(self, response=None, status=None, headers=None, | |
388 | mimetype=None, content_type=None): | |
323 | 389 | """ |
324 | 390 | Response can be any kind of iterable or string. If it's a string it's |
325 | 391 | considered being an iterable with one item which is the string passed. |
347 | 413 | if content_type is None: |
348 | 414 | if mimetype is None and 'Content-Type' not in self.headers: |
349 | 415 | mimetype = self.default_mimetype |
350 | if mimetype is not None and mimetype.startswith('text/'): | |
351 | mimetype += '; charset=' + self.charset | |
416 | if mimetype is not None: | |
417 | mimetype = get_content_type(mimetype, self.charset) | |
352 | 418 | content_type = mimetype |
353 | 419 | if content_type is not None: |
354 | 420 | self.headers['Content-Type'] = content_type |
421 | if status is None: | |
422 | status = self.default_status | |
355 | 423 | if isinstance(status, (int, long)): |
356 | 424 | self.status_code = status |
357 | 425 | else: |
358 | 426 | self.status = status |
427 | ||
428 | def force_type(cls, response, environ=None): | |
429 | """ | |
430 | Enforce that the WSGI response is a response object of the current | |
431 | type. Werkzeug will use the `BaseResponse` internally in many | |
432 | situations like the exceptions. If you call `get_response` on an | |
433 | exception you will get back a regular `BaseResponse` object, even if | |
434 | you are using a custom subclass. | |
435 | ||
436 | This method can enforce a given response type, and it will also | |
437 | convert arbitrary WSGI callables into response objects if an environ | |
438 | is provided:: | |
439 | ||
440 | # convert a Werkzeug response object into an instance of the | |
441 | # MyResponseClass subclass. | |
442 | response = MyResponseClass.force_type(response) | |
443 | ||
444 | # convert any WSGI application into a request object | |
445 | response = MyResponseClass.force_type(response, environ) | |
446 | ||
447 | This is especially useful if you want to post-process responses in | |
448 | the main dispatcher and use functionality provided by your subclass. | |
449 | ||
450 | Keep in mind that this will modify response objects in place if | |
451 | possible! | |
452 | """ | |
453 | if not isinstance(response, BaseResponse): | |
454 | if environ is None: | |
455 | raise TypeError('cannot convert WSGI application into ' | |
456 | 'response objects without an environ') | |
457 | response = BaseResponse(*run_wsgi_app(response, environ)) | |
458 | response.__class__ = cls | |
459 | return response | |
460 | force_type = classmethod(force_type) | |
359 | 461 | |
360 | 462 | def from_app(cls, app, environ, buffered=False): |
361 | 463 | """ |
370 | 472 | from_app = classmethod(from_app) |
371 | 473 | |
372 | 474 | def _get_status_code(self): |
373 | return int(self.status.split(None, 1)[0]) | |
475 | try: | |
476 | return int(self.status.split(None, 1)[0]) | |
477 | except ValueError: | |
478 | return 0 | |
374 | 479 | def _set_status_code(self, code): |
375 | self.status = '%d %s' % (code, HTTP_STATUS_CODES[code].upper()) | |
480 | try: | |
481 | self.status = '%d %s' % (code, HTTP_STATUS_CODES[code].upper()) | |
482 | except KeyError: | |
483 | self.status = '%d UNKNOWN' % code | |
376 | 484 | status_code = property(_get_status_code, _set_status_code, |
377 | 'Get the HTTP Status code as number') | |
485 | 'The HTTP Status code as number') | |
378 | 486 | del _get_status_code, _set_status_code |
379 | 487 | |
380 | def cache_control(self): | |
381 | def on_update(cache_control): | |
382 | if not cache_control and 'cache-control' in self.headers: | |
383 | del self.headers['cache-control'] | |
384 | elif cache_control: | |
385 | self.headers['Cache-Control'] = cache_control.to_header() | |
386 | value = self.headers.get('Cache-Control') | |
387 | if value is not None: | |
388 | value = parse_cache_control_header(value) | |
389 | return CacheControl(value, on_update) | |
390 | cache_control = lazy_property(cache_control) | |
391 | ||
392 | 488 | def write(self, data): |
393 | """If we have a buffered response this writes to the buffer.""" | |
489 | """ | |
490 | **deprecated** | |
491 | ||
492 | Use `response.stream` now, if you are using a `BaseResponse` subclass | |
493 | and mix the `ResponseStreamMixin` in. | |
494 | """ | |
495 | from warnings import warn | |
496 | warn(DeprecationWarning('response.write() will go away in Werkzeug ' | |
497 | '0.3. Use the new response.stream available ' | |
498 | 'on `Response`.')) | |
394 | 499 | if not isinstance(self.response, list): |
395 | 500 | raise RuntimeError('cannot write to a streamed response.') |
396 | 501 | self.response.append(data) |
397 | 502 | |
398 | 503 | def writelines(self, lines): |
399 | """Write lines.""" | |
504 | """ | |
505 | **deprecated** | |
506 | ||
507 | :see: `write` | |
508 | """ | |
400 | 509 | self.write(''.join(lines)) |
401 | 510 | |
402 | def _get_response_body(self): | |
511 | def _get_data(self): | |
403 | 512 | """ |
404 | 513 | The string representation of the request body. Whenever you access |
405 | 514 | this property the request iterable is encoded and flattened. This |
408 | 517 | if not isinstance(self.response, list): |
409 | 518 | self.response = list(self.response) |
410 | 519 | return ''.join(self.iter_encoded()) |
411 | def _set_response_body(self, value): | |
412 | """Set a new string as response body.""" | |
520 | def _set_data(self, value): | |
413 | 521 | self.response = [value] |
414 | response_body = property(_get_response_body, _set_response_body, | |
415 | doc=_get_response_body.__doc__) | |
416 | del _get_response_body, _set_response_body | |
522 | data = property(_get_data, _set_data, doc=_get_data.__doc__) | |
523 | ||
524 | def _deprecate_data(f): | |
525 | def proxy(*args, **kwargs): | |
526 | from warnings import warn | |
527 | warn(DeprecationWarning('response_body is now called data')) | |
528 | return f(*args, **kwargs) | |
529 | return proxy | |
530 | response_body = property(_deprecate_data(_get_data), | |
531 | _deprecate_data(_set_data), | |
532 | doc='**deprecated**\ncalled `data` now. ' | |
533 | 'Will go away in Werkzeug 0.3') | |
534 | del _get_data, _set_data, _deprecate_data | |
417 | 535 | |
418 | 536 | def iter_encoded(self, charset=None): |
419 | 537 | """ |
430 | 548 | |
431 | 549 | def set_cookie(self, key, value='', max_age=None, expires=None, |
432 | 550 | path='/', domain=None, secure=None, httponly=False): |
433 | """Set a new cookie.""" | |
434 | try: | |
435 | key = str(key) | |
436 | except UnicodeError: | |
437 | raise TypeError('invalid key %r' % key) | |
438 | c = SimpleCookie() | |
439 | if isinstance(value, unicode): | |
440 | value = value.encode(self.charset) | |
441 | c[key] = value | |
442 | if expires is not None: | |
443 | if not isinstance(expires, basestring): | |
444 | expires = cookie_date(expires) | |
445 | c[key]['expires'] = expires | |
446 | for k, v in (('path', path), ('domain', domain), ('secure', secure), | |
447 | ('max-age', max_age), ('HttpOnly', httponly)): | |
448 | if v is not None and v is not False: | |
449 | c[key][k] = str(v) | |
450 | self.headers.add('Set-Cookie', c[key].output(header='').lstrip()) | |
551 | """ | |
552 | Sets a cookie. The parameters are the same as in the cookie `Morsel` | |
553 | object in the Python standard library but it accepts unicode data too: | |
554 | ||
555 | - `max_age` should be a number of seconds, or `None` (default) if the | |
556 | cookie should last only as long as the client’s browser session. | |
557 | - `expires` should be a `datetime` object or UNIX timestamp. | |
558 | - Use `domain` if you want to set a cross-domain cookie. For example, | |
559 | ``domain=".example.com"`` will set a cookie that is readable by the | |
560 | domain ``www.example.com``, ``foo.example.com`` etc. Otherwise, a | |
561 | cookie will only be readable by the domain that set it. | |
562 | - `path` limits the cookie to a given path, per default it will span | |
563 | the whole domain. | |
564 | """ | |
565 | self.headers.add('Set-Cookie', dump_cookie(key, value, max_age, | |
566 | expires, path, domain, secure, httponly, | |
567 | self.charset)) | |
451 | 568 | |
452 | 569 | def delete_cookie(self, key, path='/', domain=None): |
453 | """Delete a cookie.""" | |
570 | """Delete a cookie. Fails silently if key doesn't exist.""" | |
454 | 571 | self.set_cookie(key, expires=0, max_age=0, path=path, domain=domain) |
455 | 572 | |
456 | 573 | def header_list(self): |
463 | 580 | |
464 | 581 | def is_streamed(self): |
465 | 582 | """ |
466 | If the request is streamed (the response is not a sequence) this | |
467 | property is `True`. | |
583 | If the response is streamed (the response is not a sequence) this | |
584 | property is `True`. In this case streamed means that there is no | |
585 | information about the number of iterations. This is usully `True` | |
586 | if a generator is passed to the response object. | |
468 | 587 | """ |
469 | 588 | try: |
470 | 589 | len(self.response) |
481 | 600 | """ |
482 | 601 | if 'Location' in self.headers: |
483 | 602 | self.headers['Location'] = urlparse.urljoin( |
484 | get_current_url(environ, host_only=True), | |
603 | get_current_url(environ, root_only=True), | |
485 | 604 | self.headers['Location'] |
486 | 605 | ) |
487 | ||
488 | def make_conditional(self, request_or_environ): | |
489 | """ | |
490 | Make the response conditional to the request. This method works best | |
491 | if an etag was defined for the response already. The `add_etag` | |
492 | method can be used to do that. If called without etag just the date | |
493 | header is set. | |
494 | ||
495 | This does nothing if the request method in the request or enviorn is | |
496 | anything but GET. | |
497 | """ | |
498 | if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): | |
499 | return | |
500 | environ = getattr(request_or_environ, 'environ', request_or_environ) | |
501 | self.headers['Date'] = http_date() | |
502 | if 'etag' in self.headers: | |
503 | if_none_match = environ.get('HTTP_IF_NONE_MATCH') | |
504 | last_modified = self.headers.get('last-modified') | |
505 | if_modified_since = environ.get('HTTP_IF_MODIFIED_SINCE') | |
506 | # we only set the status code because the request object removes | |
507 | # contents for 304 responses automatically on `__call__` | |
508 | if if_none_match and if_none_match == self.headers['etag'] or \ | |
509 | if_modified_since == last_modified: | |
510 | self.status_code = 304 | |
511 | ||
512 | def add_etag(self, overwrite=False): | |
513 | """Add an etag for the current response if there is none yet.""" | |
514 | if not overwrite and 'etag' in self.headers: | |
515 | return | |
516 | etag = md5(self.response_body).hexdigest() | |
517 | self.headers['ETag'] = etag | |
518 | 606 | |
519 | 607 | def close(self): |
520 | 608 | """Close the wrapped response if possible.""" |
521 | 609 | if hasattr(self.response, 'close'): |
522 | 610 | self.response.close() |
611 | ||
612 | def freeze(self): | |
613 | """ | |
614 | Call this method if you want to make your response object ready for | |
615 | pickeling. This buffers the generator if there is one. | |
616 | """ | |
617 | BaseResponse.data.__get__(self) | |
523 | 618 | |
524 | 619 | def __call__(self, environ, start_response): |
525 | 620 | """Process this response as WSGI application.""" |
535 | 630 | return resp |
536 | 631 | |
537 | 632 | |
538 | class BaseReporterStream(object): | |
539 | """ | |
540 | This class can be used to wrap `wsgi.input` in order to get informed about | |
541 | changes of the stream. | |
542 | ||
543 | Usage:: | |
544 | ||
545 | from random import randrange | |
546 | ||
547 | class ReporterStream(BaseReporterStream): | |
548 | ||
549 | def __init__(self, environ): | |
550 | super(ReporterStream, self).__init__(environ, 1024 * 16) | |
551 | self.transport_id = randrange(0, 100000) | |
552 | ||
553 | def processed(self): | |
554 | s = self.environ['my.session.service'] | |
555 | s.store['upload/%s' % self.transport_id] = (self.pos, self.length) | |
556 | s.flush() | |
557 | ||
558 | ||
559 | And before accessing `request.form` or similar attributes add the stream: | |
560 | ||
561 | stream = ReporterStream(environ) | |
562 | environ['wsgi.input'] = stream | |
563 | """ | |
564 | ||
565 | def __init__(self, environ, threshold): | |
566 | self.threshold = threshold | |
567 | self.length = int(environ.get('CONTENT_LENGTH') or 0) | |
568 | self.pos = 0 | |
569 | self.environ = environ | |
570 | self._stream = environ['wsgi.input'] | |
571 | ||
572 | def processed(self): | |
573 | """Called after pos has changed for threshold or a line was read.""" | |
574 | ||
575 | def read(self, size=None): | |
576 | length = self.length | |
577 | threshold = self.threshold | |
578 | buffer = [] | |
579 | ||
580 | if size is None: | |
581 | while self.pos < length: | |
582 | step = min(threshold, length - self.pos) | |
583 | data = self._stream.read(step) | |
584 | self.pos += step | |
585 | self.processed() | |
586 | buffer.append(data) | |
633 | class AcceptMixin(object): | |
634 | """ | |
635 | A mixin for classes with an `environ` attribute to get and all the HTTP | |
636 | accept headers as `Accept` objects. This can be mixed in request objects | |
637 | or any other object that has a WSGI environ available as `environ`. | |
638 | """ | |
639 | ||
640 | def accept_mimetypes(self): | |
641 | """List of mimetypes this client supports.""" | |
642 | return parse_accept_header(self.environ.get('HTTP_ACCEPT')) | |
643 | accept_mimetypes = cached_property(accept_mimetypes) | |
644 | ||
645 | def accept_charsets(self): | |
646 | """List of charsets this client supports.""" | |
647 | return parse_accept_header(self.environ.get('HTTP_ACCEPT_CHARSET')) | |
648 | accept_charsets = cached_property(accept_charsets) | |
649 | ||
650 | def accept_encodings(self): | |
651 | """ | |
652 | List of encodings this client accepts. Encodings in a HTTP term are | |
653 | compression encodings such as gzip. For charsets have a look at | |
654 | `accept_charset`. | |
655 | """ | |
656 | return parse_accept_header(self.environ.get('HTTP_ACCEPT_ENCODING')) | |
657 | accept_encodings = cached_property(accept_encodings) | |
658 | ||
659 | def accept_languages(self): | |
660 | """List of languages this client accepts.""" | |
661 | return parse_accept_header(self.environ.get('HTTP_ACCEPT_LANGUAGE')) | |
662 | accept_languages = cached_property(accept_languages) | |
663 | ||
664 | ||
665 | class ETagRequestMixin(object): | |
666 | """ | |
667 | Add entity tag and cache descriptors to a request object or object with | |
668 | an WSGI environment available as `environ`. This not only provides | |
669 | access to etags but also to the cache control header. | |
670 | """ | |
671 | ||
672 | def cache_control(self): | |
673 | """A `CacheControl` object for the incoming cache control headers.""" | |
674 | cache_control = self.environ.get('HTTP_CACHE_CONTROL') | |
675 | return parse_cache_control_header(cache_control) | |
676 | cache_control = cached_property(cache_control) | |
677 | ||
678 | def if_match(self): | |
679 | """An object containing all the etags in the `If-Match` header.""" | |
680 | return parse_etags(self.environ.get('HTTP_IF_MATCH')) | |
681 | if_match = cached_property(if_match) | |
682 | ||
683 | def if_none_match(self): | |
684 | """An object containing all the etags in the `If-None-Match` header.""" | |
685 | return parse_etags(self.environ.get('HTTP_IF_NONE_MATCH')) | |
686 | if_none_match = cached_property(if_none_match) | |
687 | ||
688 | def if_modified_since(self): | |
689 | """The parsed `If-Modified-Since` header as datetime object.""" | |
690 | return parse_date(self.environ.get('HTTP_IF_MODIFIED_SINCE')) | |
691 | if_modified_since = cached_property(if_modified_since) | |
692 | ||
693 | def if_unmodified_since(self): | |
694 | """The parsed `If-Unmodified-Since` header as datetime object.""" | |
695 | return parse_date(self.environ.get('HTTP_IF_UNMODIFIED_SINCE')) | |
696 | if_unmodified_since = cached_property(if_unmodified_since) | |
697 | ||
698 | ||
699 | class UserAgentMixin(object): | |
700 | """ | |
701 | Adds a `user_agent` attribute to the request object which contains the | |
702 | parsed user agent of the browser that triggered the request as `UserAgent` | |
703 | object. | |
704 | """ | |
705 | ||
706 | # this class actually belongs to a different module. For more details | |
707 | # have a look at `werkzeug.useragents`. On the bottom of that module is | |
708 | # a small comment that explains it. | |
709 | __module__ = 'werkzeug.useragents' | |
710 | ||
711 | def user_agent(self): | |
712 | """The current user agent.""" | |
713 | from werkzeug.useragents import UserAgent | |
714 | return UserAgent(self.environ) | |
715 | user_agent = cached_property(user_agent) | |
716 | ||
717 | ||
718 | class ETagResponseMixin(object): | |
719 | """ | |
720 | Adds extra functionality to a response object for etag and cache | |
721 | handling. This mixin requires an object with at least a `headers` | |
722 | object that implements a dict like interface similar to `Headers`. | |
723 | """ | |
724 | ||
725 | def cache_control(self): | |
726 | """ | |
727 | The Cache-Control general-header field is used to specify directives | |
728 | that MUST be obeyed by all caching mechanisms along the | |
729 | request/response chain. | |
730 | """ | |
731 | def on_update(cache_control): | |
732 | if not cache_control and 'cache-control' in self.headers: | |
733 | del self.headers['cache-control'] | |
734 | elif cache_control: | |
735 | self.headers['Cache-Control'] = cache_control.to_header() | |
736 | return parse_cache_control_header(self.headers.get('cache-control'), | |
737 | on_update) | |
738 | cache_control = property(cache_control, doc=cache_control.__doc__) | |
739 | ||
740 | def make_conditional(self, request_or_environ): | |
741 | """ | |
742 | Make the response conditional to the request. This method works best | |
743 | if an etag was defined for the response already. The `add_etag` | |
744 | method can be used to do that. If called without etag just the date | |
745 | header is set. | |
746 | ||
747 | This does nothing if the request method in the request or enviorn is | |
748 | anything but GET or HEAD. | |
749 | ||
750 | It does not remove the body of the response because that's something | |
751 | the `__call__` function does for us automatically. | |
752 | ||
753 | Returns self so that you can do ``return resp.make_conditional(req)`` | |
754 | but modifies the object in-place. | |
755 | """ | |
756 | environ = getattr(request_or_environ, 'environ', request_or_environ) | |
757 | if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): | |
758 | return | |
759 | self.headers['Date'] = http_date() | |
760 | if 'content-length' in self.headers: | |
761 | self.headers['Content-Length'] = len(self.data) | |
762 | if not is_resource_modified(environ, self.headers.get('etag'), None, | |
763 | self.headers.get('last-modified')): | |
764 | self.status_code = 304 | |
765 | return self | |
766 | ||
767 | def add_etag(self, overwrite=False, weak=False): | |
768 | """Add an etag for the current response if there is none yet.""" | |
769 | if overwrite or 'etag' not in self.headers: | |
770 | self.set_etag(generate_etag(self.data), weak) | |
771 | ||
772 | def set_etag(self, etag, weak=False): | |
773 | """Set the etag, and override the old one if there was one.""" | |
774 | self.headers['ETag'] = quote_etag(etag, weak) | |
775 | ||
776 | def get_etag(self): | |
777 | """ | |
778 | Return a tuple in the form ``(etag, is_weak)``. If there is no | |
779 | ETag the return value is ``(None, None)``. | |
780 | """ | |
781 | return unquote_etag(self.headers.get('ETag')) | |
782 | ||
783 | def freeze(self, no_etag=False): | |
784 | """ | |
785 | Call this method if you want to make your response object ready for | |
786 | pickeling. This buffers the generator if there is one. This also | |
787 | sets the etag unless `no_etag` is set to `True`. | |
788 | """ | |
789 | if not no_etag: | |
790 | self.add_etag() | |
791 | super(ETagResponseMixin, self).freeze() | |
792 | ||
793 | ||
794 | class ResponseStream(object): | |
795 | """ | |
796 | A file descriptor like object used by the `ResponseStreamMixin` to | |
797 | represent the body of the stream. It directly pushes into the response | |
798 | iterable of the response object. | |
799 | """ | |
800 | ||
801 | closed = False | |
802 | mode = 'wb' | |
803 | ||
804 | def __init__(self, response): | |
805 | self.response = response | |
806 | ||
807 | def write(self, value): | |
808 | if self.closed: | |
809 | raise ValueError('I/O operation on closed file') | |
810 | buf = self.response.response | |
811 | if not isinstance(buf, list): | |
812 | self.response.response = buf = list(self.response.response) | |
813 | buf.append(value) | |
814 | ||
815 | def writelines(self, seq): | |
816 | for item in seq: | |
817 | self.write(item) | |
818 | ||
819 | def close(self): | |
820 | self.closed = True | |
821 | ||
822 | def flush(self): | |
823 | if self.closed: | |
824 | raise ValueError('I/O operation on closed file') | |
825 | ||
826 | def isatty(self): | |
827 | if self.closed: | |
828 | raise ValueError('I/O operation on closed file') | |
829 | return False | |
830 | ||
831 | def encoding(self): | |
832 | return self.response.charset | |
833 | encoding = property(encoding) | |
834 | ||
835 | ||
836 | class ResponseStreamMixin(object): | |
837 | """ | |
838 | Mixin for `BaseRequest` subclasses. Classes that inherit from this mixin | |
839 | will automatically get a `stream` property that provides a write-only | |
840 | interface to the response iterable. | |
841 | """ | |
842 | ||
843 | def stream(self): | |
844 | """The response iterable as write-only stream.""" | |
845 | return ResponseStream(self) | |
846 | stream = cached_property(stream) | |
847 | ||
848 | ||
849 | class CommonResponseDescriptorsMixin(object): | |
850 | """ | |
851 | A mixin for `BaseResponse` subclasses. Response objects that mix this | |
852 | class in will automatically get descriptors for a couple of HTTP headers | |
853 | with automatic type conversion. | |
854 | """ | |
855 | ||
856 | def _get_mimetype(self): | |
857 | """The mimetype (content type without charset etc.)""" | |
858 | ct = self.headers.get('Content-Type') | |
859 | if ct: | |
860 | return ct.split(';')[0].strip() | |
861 | ||
862 | def _set_mimetype(self, value): | |
863 | self.headers['Content-Type'] = get_content_type(value, self.charset) | |
864 | ||
865 | mimetype = property(_get_mimetype, _set_mimetype, doc=''' | |
866 | The mimetype (content type without charset etc.)''') | |
867 | location = header_property('Location', doc=''' | |
868 | The Location response-header field is used to redirect the recipient | |
869 | to a location other than the Request-URI for completion of the request | |
870 | or identification of a new resource.''') | |
871 | age = header_property('Age', None, parse_date, http_date, doc=''' | |
872 | The Age response-header field conveys the sender's estimate of the | |
873 | amount of time since the response (or its revalidation) was | |
874 | generated at the origin server. | |
875 | ||
876 | Age values are non-negative decimal integers, representing time in | |
877 | seconds.''') | |
878 | content_type = header_property('Content-Type', doc=''' | |
879 | The Content-Type entity-header field indicates the media type of the | |
880 | entity-body sent to the recipient or, in the case of the HEAD method, | |
881 | the media type that would have been sent had the request been a GET. | |
882 | ''') | |
883 | content_length = header_property('Content-Length', None, int, str, doc=''' | |
884 | The Content-Length entity-header field indicates the size of the | |
885 | entity-body, in decimal number of OCTETs, sent to the recipient or, | |
886 | in the case of the HEAD method, the size of the entity-body that would | |
887 | have been sent had the request been a GET.''') | |
888 | content_location = header_property('Content-Location', doc=''' | |
889 | The Content-Location entity-header field MAY be used to supply the | |
890 | resource location for the entity enclosed in the message when that | |
891 | entity is accessible from a location separate from the requested | |
892 | resource's URI.''') | |
893 | content_encoding = header_property('Content-Encoding', doc=''' | |
894 | The Content-Encoding entity-header field is used as a modifier to the | |
895 | media-type. When present, its value indicates what additional content | |
896 | codings have been applied to the entity-body, and thus what decoding | |
897 | mechanisms must be applied in order to obtain the media-type | |
898 | referenced by the Content-Type header field.''') | |
899 | content_md5 = header_property('Content-MD5', doc=''' | |
900 | The Content-MD5 entity-header field, as defined in RFC 1864, is an | |
901 | MD5 digest of the entity-body for the purpose of providing an | |
902 | end-to-end message integrity check (MIC) of the entity-body. (Note: | |
903 | a MIC is good for detecting accidental modification of the | |
904 | entity-body in transit, but is not proof against malicious attacks.) | |
905 | ''') | |
906 | date = header_property('Date', None, parse_date, http_date, doc=''' | |
907 | The Date general-header field represents the date and time at which | |
908 | the message was originated, having the same semantics as orig-date | |
909 | in RFC 822.''') | |
910 | expires = header_property('Expires', None, parse_date, http_date, doc=''' | |
911 | The Expires entity-header field gives the date/time after which the | |
912 | response is considered stale. A stale cache entry may not normally be | |
913 | returned by a cache.''') | |
914 | last_modified = header_property('Last-Modified', None, parse_date, | |
915 | http_date, doc=''' | |
916 | The Last-Modified entity-header field indicates the date and time at | |
917 | which the origin server believes the variant was last modified.''') | |
918 | ||
919 | def _get_retry_after(self): | |
920 | value = self.headers.get('retry-after') | |
921 | if value is None: | |
922 | return | |
923 | elif value.isdigit(): | |
924 | return datetime.utcnow() + timedelta(seconds=int(value)) | |
925 | return parse_date(value) | |
926 | def _set_retry_after(self, value): | |
927 | if value is None: | |
928 | if 'retry-after' in self.headers: | |
929 | del self.headers['retry-after'] | |
930 | return | |
931 | elif isinstance(value, datetime): | |
932 | value = http_date(value) | |
587 | 933 | else: |
588 | read = 0 | |
589 | while read < size: | |
590 | step = min(threshold, length - self.pos) | |
591 | step = min(step, size) | |
592 | data = self._stream.read(step) | |
593 | self.pos += step | |
594 | read += step | |
595 | self.processed() | |
596 | buffer.append(data) | |
597 | ||
598 | return ''.join(buffer) | |
599 | ||
600 | def readline(self, *args): | |
601 | line = self._stream.readline(*args) | |
602 | self.pos += len(line) | |
603 | self.processed() | |
604 | return line | |
605 | ||
606 | def readlines(self, hint=None): | |
607 | result = [] | |
608 | while self.pos < self.length: | |
609 | result.append(self.readline()) | |
610 | return result | |
934 | value = str(value) | |
935 | self.headers['Retry-After'] = value | |
936 | ||
937 | retry_after = property(_get_retry_after, _set_retry_after, doc=''' | |
938 | The Retry-After response-header field can be used with a 503 (Service | |
939 | Unavailable) response to indicate how long the service is expected | |
940 | to be unavailable to the requesting client. | |
941 | ||
942 | Time in seconds until expiration or date.''') | |
943 | ||
944 | def _set_property(name, doc=None): | |
945 | def fget(self): | |
946 | def on_update(header_set): | |
947 | if not header_set and name in self.headers: | |
948 | del self.headers[name] | |
949 | elif header_set: | |
950 | self.headers[name] = header_set.to_header() | |
951 | return parse_set_header(self.headers.get(name), on_update) | |
952 | return property(fget, doc=doc) | |
953 | ||
954 | vary = _set_property('Vary', doc=''' | |
955 | The Vary field value indicates the set of request-header fields that | |
956 | fully determines, while the response is fresh, whether a cache is | |
957 | permitted to use the response to reply to a subsequent request | |
958 | without revalidation.''') | |
959 | content_language = _set_property('Content-Language', doc=''' | |
960 | The Content-Language entity-header field describes the natural | |
961 | language(s) of the intended audience for the enclosed entity. Note | |
962 | that this might not be equivalent to all the languages used within | |
963 | the entity-body.''') | |
964 | allow = _set_property('Allow', doc=''' | |
965 | The Allow entity-header field lists the set of methods supported | |
966 | by the resource identified by the Request-URI. The purpose of this | |
967 | field is strictly to inform the recipient of valid methods | |
968 | associated with the resource. An Allow header field MUST be | |
969 | present in a 405 (Method Not Allowed) response.''') | |
970 | ||
971 | del _set_property, _get_mimetype, _set_mimetype, _get_retry_after, \ | |
972 | _set_retry_after | |
973 | ||
974 | ||
975 | class Request(BaseRequest, AcceptMixin, ETagRequestMixin, | |
976 | UserAgentMixin): | |
977 | """ | |
978 | Full featured request object implementing the following mixins: | |
979 | ||
980 | - `AcceptMixin` for accept header parsing | |
981 | - `ETagRequestMixin` for etag and cache control handling | |
982 | - `UserAgentMixin` for user agent introspection | |
983 | """ | |
984 | ||
985 | ||
986 | class Response(BaseResponse, ETagResponseMixin, ResponseStreamMixin, | |
987 | CommonResponseDescriptorsMixin): | |
988 | """ | |
989 | Full featured response object implementing the following mixins: | |
990 | ||
991 | - `ETagResponseMixin` for etag and cache control handling | |
992 | - `ResponseStreamMixin` to add support for the `stream` property | |
993 | - `CommonResponseDescriptorsMixin` for various HTTP descriptors | |
994 | """ | |
995 | ||
996 | ||
997 | # XXX: backwards compatibility interface. goes away with werkzeug 0.3 | |
998 | try: | |
999 | from werkzeug.contrib.reporterstream import BaseReporterStream | |
1000 | except ImportError: | |
1001 | class BaseReporterStream(object): | |
1002 | def __new__(*args, **kw): | |
1003 | raise RuntimeError('base reporter stream is now part of the ' | |
1004 | 'contrib package. In order to use it install ' | |
1005 | 'werkzeug with the contrib package enabled ' | |
1006 | 'and import it from ' | |
1007 | 'werkzeug.contrib.reporterstream') | |
1008 | else: | |
1009 | class BaseReporterStream(BaseReporterStream): | |
1010 | def __init__(self, environ, threshold): | |
1011 | from warnings import warn | |
1012 | warn(DeprecationWarning('BaseReporterStream is now part of ' | |
1013 | 'the werkzeug contrib module. Import ' | |
1014 | 'it from werkzeug.contrib.reporterstream' | |
1015 | '. As of werkzeug 0.3 this will be' | |
1016 | 'required.')) | |
1017 | super(BaseReporterStream, self).__init__(environ, threshold) |