Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Merge remote-tracking branch 'mitsuhiko/master'

  • Loading branch information...
commit a9e09ec50e24c379348bc22606e39e88e47e4a8a 2 parents 03a71e0 + ccf4641
Pedro Algarvio authored

Showing 66 changed files with 3,262 additions and 2,432 deletions. Show diff stats Hide diff stats

  1. +11 0 CHANGES
  2. +4 1 MANIFEST.in
  3. +1 1  Makefile
  4. +25 8 README
  5. +9 0 docs/api.rst
  6. +45 9 docs/config.rst
  7. +16 0 docs/design.rst
  8. +25 29 docs/foreword.rst
  9. +9 1 docs/patterns/sqlite3.rst
  10. +9 1 docs/reqcontext.rst
  11. +32 0 docs/testing.rst
  12. +7 1 docs/upgrading.rst
  13. +21 0 docs/views.rst
  14. +2 1  examples/flaskr/flaskr.py
  15. +2 1  examples/minitwit/minitwit.py
  16. +42 31 flask/app.py
  17. +1 1  flask/ctx.py
  18. +2 1  flask/debughelpers.py
  19. +10 0 flask/helpers.py
  20. +31 3 flask/sessions.py
  21. +1 1  flask/signals.py
  22. +78 22 flask/testing.py
  23. +194 0 flask/testsuite/__init__.py
  24. +1,051 0 flask/testsuite/basic.py
  25. +509 0 flask/testsuite/blueprints.py
  26. +177 0 flask/testsuite/config.py
  27. +38 0 flask/testsuite/deprecations.py
  28. +38 0 flask/testsuite/examples.py
  29. +295 0 flask/testsuite/helpers.py
  30. +103 0 flask/testsuite/signals.py
  31. 0  {tests → flask/testsuite}/static/index.html
  32. 0  {tests → flask/testsuite}/templates/_macro.html
  33. 0  {tests → flask/testsuite}/templates/context_template.html
  34. 0  {tests → flask/testsuite}/templates/escaping_template.html
  35. 0  {tests → flask/testsuite}/templates/mail.txt
  36. 0  {tests → flask/testsuite}/templates/nested/nested.txt
  37. 0  {tests → flask/testsuite}/templates/simple_template.html
  38. 0  {tests → flask/testsuite}/templates/template_filter.html
  39. +141 0 flask/testsuite/templating.py
  40. 0  {tests → flask/testsuite/test_apps}/blueprintapp/__init__.py
  41. 0  {tests → flask/testsuite/test_apps}/blueprintapp/apps/__init__.py
  42. 0  {tests → flask/testsuite/test_apps}/blueprintapp/apps/admin/__init__.py
  43. 0  {tests → flask/testsuite/test_apps}/blueprintapp/apps/admin/static/css/test.css
  44. 0  {tests → flask/testsuite/test_apps}/blueprintapp/apps/admin/static/test.txt
  45. 0  {tests → flask/testsuite/test_apps}/blueprintapp/apps/admin/templates/admin/index.html
  46. 0  {tests → flask/testsuite/test_apps}/blueprintapp/apps/frontend/__init__.py
  47. 0  {tests → flask/testsuite/test_apps}/blueprintapp/apps/frontend/templates/frontend/index.html
  48. +4 0 flask/testsuite/test_apps/config_module_app.py
  49. +4 0 flask/testsuite/test_apps/config_package_app/__init__.py
  50. 0  {tests → flask/testsuite/test_apps}/moduleapp/__init__.py
  51. 0  {tests → flask/testsuite/test_apps}/moduleapp/apps/__init__.py
  52. 0  {tests → flask/testsuite/test_apps}/moduleapp/apps/admin/__init__.py
  53. 0  {tests → flask/testsuite/test_apps}/moduleapp/apps/admin/static/css/test.css
  54. 0  {tests → flask/testsuite/test_apps}/moduleapp/apps/admin/static/test.txt
  55. 0  {tests → flask/testsuite/test_apps}/moduleapp/apps/admin/templates/index.html
  56. 0  {tests → flask/testsuite/test_apps}/moduleapp/apps/frontend/__init__.py
  57. 0  {tests → flask/testsuite/test_apps}/moduleapp/apps/frontend/templates/index.html
  58. 0  {tests → flask/testsuite/test_apps}/subdomaintestmodule/__init__.py
  59. 0  {tests → flask/testsuite/test_apps}/subdomaintestmodule/static/hello.txt
  60. +165 0 flask/testsuite/testing.py
  61. +117 0 flask/testsuite/views.py
  62. +35 0 flask/views.py
  63. +3 0  run-tests.py
  64. 0  {tests → scripts}/flaskext_test.py
  65. +5 8 setup.py
  66. +0 2,312 tests/flask_tests.py
11 CHANGES
@@ -32,6 +32,17 @@ Relase date to be decided, codename to be chosen.
32 32 conceptionally only instance depending and outside version control so it's
33 33 the perfect place to put configuration files etc. For more information
34 34 see :ref:`instance-folders`.
  35 +- Added the ``APPLICATION_ROOT`` configuration variable.
  36 +- Implemented :meth:`~flask.testing.TestClient.session_transaction` to
  37 + easily modify sessions from the test environment.
  38 +- Refactored test client internally. The ``APPLICATION_ROOT`` configuration
  39 + variable as well as ``SERVER_NAME`` are now properly used by the test client
  40 + as defaults.
  41 +- Added :attr:`flask.views.View.decorators` to support simpler decorating of
  42 + pluggable (class based) views.
  43 +- Fixed an issue where the test client if used with the with statement did not
  44 + trigger the execution of the teardown handlers.
  45 +- Added finer control over the session cookie parameters.
35 46
36 47 Version 0.7.3
37 48 -------------
5 MANIFEST.in
... ... @@ -1,4 +1,4 @@
1   -include Makefile CHANGES LICENSE AUTHORS
  1 +include Makefile CHANGES LICENSE AUTHORS run-tests.py
2 2 recursive-include artwork *
3 3 recursive-include tests *
4 4 recursive-include examples *
@@ -9,5 +9,8 @@ recursive-exclude tests *.pyc
9 9 recursive-exclude tests *.pyo
10 10 recursive-exclude examples *.pyc
11 11 recursive-exclude examples *.pyo
  12 +recursive-include flask/testsuite/static *
  13 +recursive-include flask/testsuite/templates *
  14 +recursive-include flask/testsuite/test_apps *
12 15 prune docs/_build
13 16 prune docs/_themes/.git
2  Makefile
@@ -3,7 +3,7 @@
3 3 all: clean-pyc test
4 4
5 5 test:
6   - python setup.py test
  6 + python run-tests.py
7 7
8 8 audit:
9 9 python setup.py audit
33 README
@@ -11,24 +11,41 @@
11 11
12 12 ~ Is it ready?
13 13
14   - A preview release is out now, and I'm hoping for some
15   - input about what you want from a microframework and
16   - how it should look like. Consider the API to slightly
17   - improve over time.
  14 + It's still not 1.0 but it's shaping up nicely and is
  15 + already widely used. Consider the API to slightly
  16 + improve over time but we don't plan on breaking it.
18 17
19 18 ~ What do I need?
20 19
21   - Jinja 2.4 and Werkzeug 0.6.1. `easy_install` will
22   - install them for you if you do `easy_install Flask==dev`.
  20 + Jinja 2.4 and Werkzeug 0.6.1. `pip` or `easy_install` will
  21 + install them for you if you do `easy_install Flask`.
23 22 I encourage you to use a virtualenv. Check the docs for
24 23 complete installation and usage instructions.
25 24
26 25 ~ Where are the docs?
27 26
28   - Go to http://flask.pocoo.org/ for a prebuilt version of
29   - the current documentation. Otherwise build them yourself
  27 + Go to http://flask.pocoo.org/docs/ for a prebuilt version
  28 + of the current documentation. Otherwise build them yourself
30 29 from the sphinx sources in the docs folder.
31 30
  31 + ~ Where are the tests?
  32 +
  33 + Good that you're asking. The tests are in the
  34 + flask/testsuite package. To run the tests use the
  35 + `run-tests.py` file:
  36 +
  37 + $ python run-tests.py
  38 +
  39 + If it's not enough output for you, you can use the
  40 + `--verbose` flag:
  41 +
  42 + $ python run-tests.py --verbose
  43 +
  44 + If you just want one particular testcase to run you can
  45 + provide it on the command line:
  46 +
  47 + $ python run-tests.py test_to_run
  48 +
32 49 ~ Where can I get help?
33 50
34 51 Either use the #pocoo IRC channel on irc.freenode.net or
9 docs/api.rst
Source Rendered
@@ -218,6 +218,15 @@ implementation that Flask is using.
218 218 :members:
219 219
220 220
  221 +Test Client
  222 +-----------
  223 +
  224 +.. currentmodule:: flask.testing
  225 +
  226 +.. autoclass:: FlaskClient
  227 + :members:
  228 +
  229 +
221 230 Application Globals
222 231 -------------------
223 232
54 docs/config.rst
Source Rendered
@@ -70,6 +70,20 @@ The following configuration values are used internally by Flask:
70 70 very risky).
71 71 ``SECRET_KEY`` the secret key
72 72 ``SESSION_COOKIE_NAME`` the name of the session cookie
  73 +``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If
  74 + this is not set, the cookie will be
  75 + valid for all subdomains of
  76 + ``SERVER_NAME``.
  77 +``SESSION_COOKIE_PATH`` the path for the session cookie. If
  78 + this is not set the cookie will be valid
  79 + for all of ``APPLICATION_ROOT`` or if
  80 + that is not set for ``'/'``.
  81 +``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set
  82 + with the httponly flag. Defaults to
  83 + `True`.
  84 +``SESSION_COOKIE_SECURE`` controls if the cookie should be set
  85 + with the secure flag. Defaults to
  86 + `False`.
73 87 ``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as
74 88 :class:`datetime.timedelta` object.
75 89 ``USE_X_SENDFILE`` enable/disable x-sendfile
@@ -77,6 +91,13 @@ The following configuration values are used internally by Flask:
77 91 ``SERVER_NAME`` the name and port number of the server.
78 92 Required for subdomain support (e.g.:
79 93 ``'localhost:5000'``)
  94 +``APPLICATION_ROOT`` If the application does not occupy
  95 + a whole domain or subdomain this can
  96 + be set to the path where the application
  97 + is configured to live. This is for
  98 + session cookie as path value. If
  99 + domains are used, this should be
  100 + ``None``.
80 101 ``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will
81 102 reject incoming requests with a
82 103 content length greater than this by
@@ -134,7 +155,10 @@ The following configuration values are used internally by Flask:
134 155 ``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION``
135 156
136 157 .. versionadded:: 0.8
137   - ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``
  158 + ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``,
  159 + ``APPLICATION_ROOT``, ``SESSION_COOKIE_DOMAIN``,
  160 + ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``,
  161 + ``SESSION_COOKIE_SECURE``
138 162
139 163 Configuring from Files
140 164 ----------------------
@@ -291,25 +315,37 @@ With Flask 0.8 a new attribute was introduced:
291 315 version control and be deployment specific. It's the perfect place to
292 316 drop things that either change at runtime or configuration files.
293 317
294   -To make it easier to put this folder into an ignore list for your version
295   -control system it's called ``instance`` and placed directly next to your
296   -package or module by default. This path can be overridden by specifying
297   -the `instance_path` parameter to your application::
  318 +You can either explicitly provide the path of the instance folder when
  319 +creating the Flask application or you can let Flask autodetect the
  320 +instance folder. For explicit configuration use the `instance_path`
  321 +parameter::
298 322
299 323 app = Flask(__name__, instance_path='/path/to/instance/folder')
300 324
301   -Default locations::
  325 +Please keep in mind that this path *must* be absolute when provided.
  326 +
  327 +If the `instance_path` parameter is not provided the following default
  328 +locations are used:
  329 +
  330 +- Uninstalled module::
302 331
303   - Module situation:
304 332 /myapp.py
305 333 /instance
306 334
307   - Package situation:
  335 +- Uninstalled package::
  336 +
308 337 /myapp
309 338 /__init__.py
310 339 /instance
311 340
312   -Please keep in mind that this path *must* be absolute when provided.
  341 +- Installed module or package::
  342 +
  343 + $PREFIX/lib/python2.X/site-packages/myapp
  344 + $PREFIX/var/myapp-instance
  345 +
  346 + ``$PREFIX`` is the prefix of your Python installation. This can be
  347 + ``/usr`` or the path to your virtualenv. You can print the value of
  348 + ``sys.prefix`` to see what the prefix is set to.
313 349
314 350 Since the config object provided loading of configuration files from
315 351 relative filenames we made it possible to change the loading via filenames
16 docs/design.rst
Source Rendered
@@ -79,6 +79,22 @@ Furthermore this design makes it possible to use a factory function to
79 79 create the application which is very helpful for unittesting and similar
80 80 things (:ref:`app-factories`).
81 81
  82 +The Routing System
  83 +------------------
  84 +
  85 +Flask uses the Werkzeug routing system which has was designed to
  86 +automatically order routes by complexity. This means that you can declare
  87 +routes in arbitrary order and they will still work as expected. This is a
  88 +requirement if you want to properly implement decorator based routing
  89 +since decorators could be fired in undefined order when the application is
  90 +split into multiple modules.
  91 +
  92 +Another design decision with the Werkzeug routing system is that routes
  93 +in Werkzeug try to ensure that there is that URLs are unique. Werkzeug
  94 +will go quite far with that in that it will automatically redirect to a
  95 +canonical URL if a route is ambiguous.
  96 +
  97 +
82 98 One Template Engine
83 99 -------------------
84 100
54 docs/foreword.rst
Source Rendered
@@ -9,27 +9,30 @@ What does "micro" mean?
9 9 -----------------------
10 10
11 11 To me, the "micro" in microframework refers not only to the simplicity and
12   -small size of the framework, but also to the typically limited complexity
13   -and size of applications that are written with the framework. Also the
14   -fact that you can have an entire application in a single Python file. To
15   -be approachable and concise, a microframework sacrifices a few features
16   -that may be necessary in larger or more complex applications.
17   -
18   -For example, Flask uses thread-local objects internally so that you don't
19   -have to pass objects around from function to function within a request in
20   -order to stay threadsafe. While this is a really easy approach and saves
21   -you a lot of time, it might also cause some troubles for very large
22   -applications because changes on these thread-local objects can happen
23   -anywhere in the same thread.
24   -
25   -Flask provides some tools to deal with the downsides of this approach but
26   -it might be an issue for larger applications because in theory
27   -modifications on these objects might happen anywhere in the same thread.
  12 +small size of the framework, but also the fact that it does not make much
  13 +decisions for you. While Flask does pick a templating engine for you, we
  14 +won't make such decisions for your datastore or other parts.
  15 +
  16 +For us however the term “micro” does not mean that the whole implementation
  17 +has to fit into a single Python file.
  18 +
  19 +One of the design decisions with Flask was that simple tasks should be
  20 +simple and not take up a lot of code and yet not limit yourself. Because
  21 +of that we took a few design choices that some people might find
  22 +surprising or unorthodox. For example, Flask uses thread-local objects
  23 +internally so that you don't have to pass objects around from function to
  24 +function within a request in order to stay threadsafe. While this is a
  25 +really easy approach and saves you a lot of time, it might also cause some
  26 +troubles for very large applications because changes on these thread-local
  27 +objects can happen anywhere in the same thread. In order to solve these
  28 +problems we don't hide the thread locals for you but instead embrace them
  29 +and provide you with a lot of tools to make it as pleasant as possible to
  30 +work with them.
28 31
29 32 Flask is also based on convention over configuration, which means that
30 33 many things are preconfigured. For example, by convention, templates and
31 34 static files are in subdirectories within the Python source tree of the
32   -application.
  35 +application. While this can be changed you usually don't have to.
33 36
34 37 The main reason however why Flask is called a "microframework" is the idea
35 38 to keep the core simple but extensible. There is no database abstraction
@@ -40,22 +43,15 @@ was implemented in Flask itself. There are currently extensions for
40 43 object relational mappers, form validation, upload handling, various open
41 44 authentication technologies and more.
42 45
43   -However Flask is not much code and it is built on a very solid foundation
44   -and with that it is very easy to adapt for large applications. If you are
45   -interested in that, check out the :ref:`becomingbig` chapter.
  46 +Since Flask is based on a very solid foundation there is not a lot of code
  47 +in Flask itself. As such it's easy to adapt even for lage applications
  48 +and we are making sure that you can either configure it as much as
  49 +possible by subclassing things or by forking the entire codebase. If you
  50 +are interested in that, check out the :ref:`becomingbig` chapter.
46 51
47 52 If you are curious about the Flask design principles, head over to the
48 53 section about :ref:`design`.
49 54
50   -A Framework and an Example
51   ---------------------------
52   -
53   -Flask is not only a microframework; it is also an example. Based on
54   -Flask, there will be a series of blog posts that explain how to create a
55   -framework. Flask itself is just one way to implement a framework on top
56   -of existing libraries. Unlike many other microframeworks, Flask does not
57   -try to implement everything on its own; it reuses existing code.
58   -
59 55 Web Development is Dangerous
60 56 ----------------------------
61 57
10 docs/patterns/sqlite3.rst
Source Rendered
@@ -24,7 +24,15 @@ So here is a simple example of how you can use SQLite 3 with Flask::
24 24
25 25 @app.teardown_request
26 26 def teardown_request(exception):
27   - g.db.close()
  27 + if hasattr(g, 'db'):
  28 + g.db.close()
  29 +
  30 +.. note::
  31 +
  32 + Please keep in mind that the teardown request functions are always
  33 + executed, even if a before-request handler failed or was never
  34 + executed. Because of this we have to make sure here that the database
  35 + is there before we close it.
28 36
29 37 Connect on Demand
30 38 -----------------
10 docs/reqcontext.rst
Source Rendered
@@ -131,7 +131,9 @@ understand what is actually happening. The new behavior is quite simple:
131 131
132 132 4. At the end of the request the :meth:`~flask.Flask.teardown_request`
133 133 functions are executed. This always happens, even in case of an
134   - unhandled exception down the road.
  134 + unhandled exception down the road or if a before-request handler was
  135 + not executed yet or at all (for example in test environments sometimes
  136 + you might want to not execute before-request callbacks).
135 137
136 138 Now what happens on errors? In production mode if an exception is not
137 139 caught, the 500 internal server handler is called. In development mode
@@ -183,6 +185,12 @@ It's easy to see the behavior from the command line:
183 185 this runs after request
184 186 >>>
185 187
  188 +Keep in mind that teardown callbacks are always executed, even if
  189 +before-request callbacks were not executed yet but an exception happened.
  190 +Certain parts of the test system might also temporarily create a request
  191 +context without calling the before-request handlers. Make sure to write
  192 +your teardown-request handlers in a way that they will never fail.
  193 +
186 194 .. _notes-on-proxies:
187 195
188 196 Notes On Proxies
32 docs/testing.rst
Source Rendered
@@ -273,3 +273,35 @@ is no longer available (because you are trying to use it outside of the actual r
273 273 However, keep in mind that any :meth:`~flask.Flask.after_request` functions
274 274 are already called at this point so your database connection and
275 275 everything involved is probably already closed down.
  276 +
  277 +
  278 +Accessing and Modifying Sessions
  279 +--------------------------------
  280 +
  281 +.. versionadded:: 0.8
  282 +
  283 +Sometimes it can be very helpful to access or modify the sessions from the
  284 +test client. Generally there are two ways for this. If you just want to
  285 +ensure that a session has certain keys set to certain values you can just
  286 +keep the context around and access :data:`flask.session`::
  287 +
  288 + with app.test_client() as c:
  289 + rv = c.get('/')
  290 + assert flask.session['foo'] == 42
  291 +
  292 +This however does not make it possible to also modify the session or to
  293 +access the session before a request was fired. Starting with Flask 0.8 we
  294 +provide a so called “session transaction” which simulates the appropriate
  295 +calls to open a session in the context of the test client and to modify
  296 +it. At the end of the transaction the session is stored. This works
  297 +independently of the session backend used::
  298 +
  299 + with app.test_client() as c:
  300 + with c.session_transaction() as sess:
  301 + sess['a_key'] = 'a value'
  302 +
  303 + # once this is reached the session was stored
  304 +
  305 +Note that in this case you have to use the ``sess`` object instead of the
  306 +:data:`flask.session` proxy. The object however itself will provide the
  307 +same interface.
8 docs/upgrading.rst
Source Rendered
@@ -36,6 +36,11 @@ longer have to handle that error to avoid an internal server error showing
36 36 up for the user. If you were catching this down explicitly in the past
37 37 as `ValueError` you will need to change this.
38 38
  39 +Due to a bug in the test client Flask 0.7 did not trigger teardown
  40 +handlers when the test client was used in a with statement. This was
  41 +since fixed but might require some changes in your testsuites if you
  42 +relied on this behavior.
  43 +
39 44 Version 0.7
40 45 -----------
41 46
@@ -142,7 +147,8 @@ You are now encouraged to use this instead::
142 147
143 148 @app.teardown_request
144 149 def after_request(exception):
145   - g.db.close()
  150 + if hasattr(g, 'db'):
  151 + g.db.close()
146 152
147 153 On the upside this change greatly improves the internal code flow and
148 154 makes it easier to customize the dispatching and error handling. This
21 docs/views.rst
Source Rendered
@@ -135,3 +135,24 @@ easily do that. Each HTTP method maps to a function with the same name
135 135 That way you also don't have to provide the
136 136 :attr:`~flask.views.View.methods` attribute. It's automatically set based
137 137 on the methods defined in the class.
  138 +
  139 +Decorating Views
  140 +----------------
  141 +
  142 +Since the view class itself is not the view function that is added to the
  143 +routing system it does not make much sense to decorate the class itself.
  144 +Instead you either have to decorate the return value of
  145 +:meth:`~flask.views.View.as_view` by hand::
  146 +
  147 + view = rate_limited(UserAPI.as_view('users'))
  148 + app.add_url_rule('/users/', view_func=view)
  149 +
  150 +Starting with Flask 0.8 there is also an alternative way where you can
  151 +specify a list of decorators to apply in the class declaration::
  152 +
  153 + class UserAPI(MethodView):
  154 + decorators = [rate_limited]
  155 +
  156 +Due to the implicit self from the caller's perspective you cannot use
  157 +regular view decorators on the individual methods of the view however,
  158 +keep this in mind.
3  examples/flaskr/flaskr.py
@@ -50,7 +50,8 @@ def before_request():
50 50 @app.teardown_request
51 51 def teardown_request(exception):
52 52 """Closes the database again at the end of the request."""
53   - g.db.close()
  53 + if hasattr(g, 'db'):
  54 + g.db.close()
54 55
55 56
56 57 @app.route('/')
3  examples/minitwit/minitwit.py
@@ -85,7 +85,8 @@ def before_request():
85 85 @app.teardown_request
86 86 def teardown_request(exception):
87 87 """Closes the database again at the end of the request."""
88   - g.db.close()
  88 + if hasattr(g, 'db'):
  89 + g.db.close()
89 90
90 91
91 92 @app.route('/')
73 flask/app.py
@@ -231,11 +231,16 @@ class Flask(_PackageBoundObject):
231 231 'PROPAGATE_EXCEPTIONS': None,
232 232 'PRESERVE_CONTEXT_ON_EXCEPTION': None,
233 233 'SECRET_KEY': None,
234   - 'SESSION_COOKIE_NAME': 'session',
235 234 'PERMANENT_SESSION_LIFETIME': timedelta(days=31),
236 235 'USE_X_SENDFILE': False,
237 236 'LOGGER_NAME': None,
238 237 'SERVER_NAME': None,
  238 + 'APPLICATION_ROOT': None,
  239 + 'SESSION_COOKIE_NAME': 'session',
  240 + 'SESSION_COOKIE_DOMAIN': None,
  241 + 'SESSION_COOKIE_PATH': None,
  242 + 'SESSION_COOKIE_HTTPONLY': True,
  243 + 'SESSION_COOKIE_SECURE': False,
239 244 'MAX_CONTENT_LENGTH': None,
240 245 'TRAP_BAD_REQUEST_ERRORS': False,
241 246 'TRAP_HTTP_EXCEPTIONS': False
@@ -705,6 +710,8 @@ def test_client(self, use_cookies=True):
705 710 rv = c.get('/?vodka=42')
706 711 assert request.args['vodka'] == '42'
707 712
  713 + See :class:`~flask.testing.FlaskClient` for more information.
  714 +
708 715 .. versionchanged:: 0.4
709 716 added support for `with` block usage for the client.
710 717
@@ -1217,14 +1224,24 @@ def handle_exception(self, e):
1217 1224 else:
1218 1225 raise e
1219 1226
1220   - self.logger.exception('Exception on %s [%s]' % (
1221   - request.path,
1222   - request.method
1223   - ))
  1227 + self.log_exception((exc_type, exc_value, tb))
1224 1228 if handler is None:
1225 1229 return InternalServerError()
1226 1230 return handler(e)
1227 1231
  1232 + def log_exception(self, exc_info):
  1233 + """Logs an exception. This is called by :meth:`handle_exception`
  1234 + if debugging is disabled and right before the handler is called.
  1235 + The default implementation logs the exception as error on the
  1236 + :attr:`logger`.
  1237 +
  1238 + .. versionadded:: 0.8
  1239 + """
  1240 + self.logger.error('Exception on %s [%s]' % (
  1241 + request.path,
  1242 + request.method
  1243 + ), exc_info=exc_info)
  1244 +
1228 1245 def raise_routing_exception(self, request):
1229 1246 """Exceptions that are recording during routing are reraised with
1230 1247 this method. During debug we are not reraising redirect requests
@@ -1306,17 +1323,18 @@ def make_default_options_response(self):
1306 1323
1307 1324 .. versionadded:: 0.7
1308 1325 """
1309   - # This would be nicer in Werkzeug 0.7, which however currently
1310   - # is not released. Werkzeug 0.7 provides a method called
1311   - # allowed_methods() that returns all methods that are valid for
1312   - # a given path.
1313   - methods = []
1314   - try:
1315   - _request_ctx_stack.top.url_adapter.match(method='--')
1316   - except MethodNotAllowed, e:
1317   - methods = e.valid_methods
1318   - except HTTPException, e:
1319   - pass
  1326 + adapter = _request_ctx_stack.top.url_adapter
  1327 + if hasattr(adapter, 'allowed_methods'):
  1328 + methods = adapter.allowed_methods()
  1329 + else:
  1330 + # fallback for Werkzeug < 0.7
  1331 + methods = []
  1332 + try:
  1333 + adapter.match(method='--')
  1334 + except MethodNotAllowed, e:
  1335 + methods = e.valid_methods
  1336 + except HTTPException, e:
  1337 + pass
1320 1338 rv = self.response_class()
1321 1339 rv.allow.update(methods)
1322 1340 return rv
@@ -1387,7 +1405,7 @@ def preprocess_request(self):
1387 1405 This also triggers the :meth:`url_value_processor` functions before
1388 1406 the actualy :meth:`before_request` functions are called.
1389 1407 """
1390   - bp = request.blueprint
  1408 + bp = _request_ctx_stack.top.request.blueprint
1391 1409
1392 1410 funcs = self.url_value_preprocessors.get(None, ())
1393 1411 if bp is not None and bp in self.url_value_preprocessors:
@@ -1437,7 +1455,7 @@ def do_teardown_request(self):
1437 1455 tighter control over certain resources under testing environments.
1438 1456 """
1439 1457 funcs = reversed(self.teardown_request_funcs.get(None, ()))
1440   - bp = request.blueprint
  1458 + bp = _request_ctx_stack.top.request.blueprint
1441 1459 if bp is not None and bp in self.teardown_request_funcs:
1442 1460 funcs = chain(funcs, reversed(self.teardown_request_funcs[bp]))
1443 1461 exc = sys.exc_info()[1]
@@ -1482,19 +1500,12 @@ def test_request_context(self, *args, **kwargs):
1482 1500 :func:`werkzeug.test.EnvironBuilder` for more information, this
1483 1501 function accepts the same arguments).
1484 1502 """
1485   - from werkzeug.test import create_environ
1486   - environ_overrides = kwargs.setdefault('environ_overrides', {})
1487   - if self.config.get('SERVER_NAME'):
1488   - server_name = self.config.get('SERVER_NAME')
1489   - if ':' not in server_name:
1490   - http_host, http_port = server_name, '80'
1491   - else:
1492   - http_host, http_port = server_name.split(':', 1)
1493   -
1494   - environ_overrides.setdefault('SERVER_NAME', server_name)
1495   - environ_overrides.setdefault('HTTP_HOST', server_name)
1496   - environ_overrides.setdefault('SERVER_PORT', http_port)
1497   - return self.request_context(create_environ(*args, **kwargs))
  1503 + from flask.testing import make_test_environ_builder
  1504 + builder = make_test_environ_builder(self, *args, **kwargs)
  1505 + try:
  1506 + return self.request_context(builder.get_environ())
  1507 + finally:
  1508 + builder.close()
1498 1509
1499 1510 def wsgi_app(self, environ, start_response):
1500 1511 """The actual WSGI application. This is not implemented in
2  flask/ctx.py
@@ -91,7 +91,7 @@ def __init__(self, app, environ):
91 91
92 92 self.match_request()
93 93
94   - # Support for deprecated functionality. This is doing away with
  94 + # XXX: Support for deprecated functionality. This is doing away with
95 95 # Flask 1.0
96 96 blueprint = self.request.blueprint
97 97 if blueprint is not None:
3  flask/debughelpers.py
@@ -54,7 +54,8 @@ def __init__(self, request):
54 54
55 55 buf.append(' Make sure to directly send your %s-request to this URL '
56 56 'since we can\'t make browsers or HTTP clients redirect '
57   - 'with form data.' % request.method)
  57 + 'with form data reliably or without user interaction.' %
  58 + request.method)
58 59 buf.append('\n\nNote: this exception is only raised in debug mode')
59 60 AssertionError.__init__(self, ''.join(buf).encode('utf-8'))
60 61
10 flask/helpers.py
@@ -145,6 +145,13 @@ def index():
145 145
146 146 response = make_response(render_template('not_found.html'), 404)
147 147
  148 + The other use case of this function is to force the return value of a
  149 + view function into a response which is helpful with view
  150 + decorators::
  151 +
  152 + response = make_response(view_function())
  153 + response.headers['X-Parachutes'] = 'parachutes are cool'
  154 +
148 155 Internally this function does the following things:
149 156
150 157 - if no arguments are passed, it creates a new response argument
@@ -477,6 +484,8 @@ def get_root_path(import_name):
477 484 directory = os.path.dirname(sys.modules[import_name].__file__)
478 485 return os.path.abspath(directory)
479 486 except AttributeError:
  487 + # this is necessary in case we are running from the interactive
  488 + # python shell. It will never be used for production code however
480 489 return os.getcwd()
481 490
482 491
@@ -492,6 +501,7 @@ def find_package(import_name):
492 501 root_mod = sys.modules[import_name.split('.')[0]]
493 502 package_path = getattr(root_mod, '__file__', None)
494 503 if package_path is None:
  504 + # support for the interactive python shell
495 505 package_path = os.getcwd()
496 506 else:
497 507 package_path = os.path.abspath(os.path.dirname(package_path))
34 flask/sessions.py
@@ -123,10 +123,34 @@ def get_cookie_domain(self, app):
123 123 """Helpful helper method that returns the cookie domain that should
124 124 be used for the session cookie if session cookies are used.
125 125 """
  126 + if app.config['SESSION_COOKIE_DOMAIN'] is not None:
  127 + return app.config['SESSION_COOKIE_DOMAIN']
126 128 if app.config['SERVER_NAME'] is not None:
127 129 # chop of the port which is usually not supported by browsers
128 130 return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
129 131
  132 + def get_cookie_path(self, app):
  133 + """Returns the path for which the cookie should be valid. The
  134 + default implementation uses the value from the SESSION_COOKIE_PATH``
  135 + config var if it's set, and falls back to ``APPLICATION_ROOT`` or
  136 + uses ``/`` if it's `None`.
  137 + """
  138 + return app.config['SESSION_COOKIE_PATH'] or \
  139 + app.config['APPLICATION_ROOT'] or '/'
  140 +
  141 + def get_cookie_httponly(self, app):
  142 + """Returns True if the session cookie should be httponly. This
  143 + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
  144 + config var.
  145 + """
  146 + return app.config['SESSION_COOKIE_HTTPONLY']
  147 +
  148 + def get_cookie_secure(self, app):
  149 + """Returns True if the cookie should be secure. This currently
  150 + just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
  151 + """
  152 + return app.config['SESSION_COOKIE_SECURE']
  153 +
130 154 def get_expiration_time(self, app, session):
131 155 """A helper method that returns an expiration date for the session
132 156 or `None` if the session is linked to the browser session. The
@@ -169,9 +193,13 @@ def open_session(self, app, request):
169 193 def save_session(self, app, session, response):
170 194 expires = self.get_expiration_time(app, session)
171 195 domain = self.get_cookie_domain(app)
  196 + path = self.get_cookie_path(app)
  197 + httponly = self.get_cookie_httponly(app)
  198 + secure = self.get_cookie_secure(app)
172 199 if session.modified and not session:
173   - response.delete_cookie(app.session_cookie_name,
  200 + response.delete_cookie(app.session_cookie_name, path=path,
174 201 domain=domain)
175 202 else:
176   - session.save_cookie(response, app.session_cookie_name,
177   - expires=expires, httponly=True, domain=domain)
  203 + session.save_cookie(response, app.session_cookie_name, path=path,
  204 + expires=expires, httponly=httponly,
  205 + secure=secure, domain=domain)
2  flask/signals.py
@@ -34,7 +34,7 @@ def _fail(self, *args, **kwargs):
34 34 'not installed.')
35 35 send = lambda *a, **kw: None
36 36 connect = disconnect = has_receivers_for = receivers_for = \
37   - temporarily_connected_to = _fail
  37 + temporarily_connected_to = connected_to = _fail
38 38 del _fail
39 39
40 40 # the namespace for code signals. If you are not flask code, do
100 flask/testing.py
@@ -10,44 +10,91 @@
10 10 :license: BSD, see LICENSE for more details.
11 11 """
12 12
  13 +from contextlib import contextmanager
13 14 from werkzeug.test import Client, EnvironBuilder
14 15 from flask import _request_ctx_stack
15 16
16 17
  18 +def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs):
  19 + """Creates a new test builder with some application defaults thrown in."""
  20 + http_host = app.config.get('SERVER_NAME')
  21 + app_root = app.config.get('APPLICATION_ROOT')
  22 + if base_url is None:
  23 + base_url = 'http://%s/' % (http_host or 'localhost')
  24 + if app_root:
  25 + base_url += app_root.lstrip('/')
  26 + return EnvironBuilder(path, base_url, *args, **kwargs)
  27 +
  28 +
17 29 class FlaskClient(Client):
18   - """Works like a regular Werkzeug test client but has some
19   - knowledge about how Flask works to defer the cleanup of the
20   - request context stack to the end of a with body when used
21   - in a with statement.
  30 + """Works like a regular Werkzeug test client but has some knowledge about
  31 + how Flask works to defer the cleanup of the request context stack to the
  32 + end of a with body when used in a with statement. For general information
  33 + about how to use this class refer to :class:`werkzeug.test.Client`.
  34 +
  35 + Basic usage is outlined in the :ref:`testing` chapter.
22 36 """
23 37
24 38 preserve_context = context_preserved = False
25 39
  40 + @contextmanager
  41 + def session_transaction(self, *args, **kwargs):
  42 + """When used in combination with a with statement this opens a
  43 + session transaction. This can be used to modify the session that
  44 + the test client uses. Once the with block is left the session is
  45 + stored back.
  46 +
  47 + with client.session_transaction() as session:
  48 + session['value'] = 42
  49 +
  50 + Internally this is implemented by going through a temporary test
  51 + request context and since session handling could depend on
  52 + request variables this function accepts the same arguments as
  53 + :meth:`~flask.Flask.test_request_context` which are directly
  54 + passed through.
  55 + """
  56 + if self.cookie_jar is None:
  57 + raise RuntimeError('Session transactions only make sense '
  58 + 'with cookies enabled.')
  59 + app = self.application
  60 + environ_overrides = kwargs.pop('environ_overrides', {})
  61 + self.cookie_jar.inject_wsgi(environ_overrides)
  62 + outer_reqctx = _request_ctx_stack.top
  63 + with app.test_request_context(*args, **kwargs) as c:
  64 + sess = app.open_session(c.request)
  65 + if sess is None:
  66 + raise RuntimeError('Session backend did not open a session. '
  67 + 'Check the configuration')
  68 +
  69 + # Since we have to open a new request context for the session
  70 + # handling we want to make sure that we hide out own context
  71 + # from the caller. By pushing the original request context
  72 + # (or None) on top of this and popping it we get exactly that
  73 + # behavior. It's important to not use the push and pop
  74 + # methods of the actual request context object since that would
  75 + # mean that cleanup handlers are called
  76 + _request_ctx_stack.push(outer_reqctx)
  77 + try:
  78 + yield sess
  79 + finally:
  80 + _request_ctx_stack.pop()
  81 +
  82 + resp = app.response_class()
  83 + if not app.session_interface.is_null_session(sess):
  84 + app.save_session(sess, resp)
  85 + headers = resp.get_wsgi_headers(c.request.environ)
  86 + self.cookie_jar.extract_wsgi(c.request.environ, headers)
  87 +
26 88 def open(self, *args, **kwargs):
27   - if self.context_preserved:
28   - _request_ctx_stack.pop()
29   - self.context_preserved = False
  89 + self._pop_reqctx_if_necessary()
30 90 kwargs.setdefault('environ_overrides', {}) \
31 91 ['flask._preserve_context'] = self.preserve_context
32 92
33 93 as_tuple = kwargs.pop('as_tuple', False)
34 94 buffered = kwargs.pop('buffered', False)
35 95 follow_redirects = kwargs.pop('follow_redirects', False)
  96 + builder = make_test_environ_builder(self.application, *args, **kwargs)
36 97
37   - builder = EnvironBuilder(*args, **kwargs)
38   -
39   - if self.application.config.get('SERVER_NAME'):
40   - server_name = self.application.config.get('SERVER_NAME')
41   - if ':' not in server_name:
42   - http_host, http_port = server_name, None
43   - else:
44   - http_host, http_port = server_name.split(':', 1)
45   - if builder.base_url == 'http://localhost/':
46   - # Default Generated Base URL
47   - if http_port != None:
48   - builder.host = http_host + ':' + http_port
49   - else:
50   - builder.host = http_host
51 98 old = _request_ctx_stack.top
52 99 try:
53 100 return Client.open(self, builder,
@@ -58,10 +105,19 @@ def open(self, *args, **kwargs):
58 105 self.context_preserved = _request_ctx_stack.top is not old
59 106
60 107 def __enter__(self):
  108 + if self.preserve_context:
  109 + raise RuntimeError('Cannot nest client invocations')
61 110 self.preserve_context = True
62 111 return self
63 112
64 113 def __exit__(self, exc_type, exc_value, tb):
65 114 self.preserve_context = False
  115 + self._pop_reqctx_if_necessary()
  116 +
  117 + def _pop_reqctx_if_necessary(self):
66 118 if self.context_preserved:
67   - _request_ctx_stack.pop()
  119 + # we have to use _request_ctx_stack.top.pop instead of
  120 + # _request_ctx_stack.pop since we want teardown handlers
  121 + # to be executed.
  122 + _request_ctx_stack.top.pop()
  123 + self.context_preserved = False
194 flask/testsuite/__init__.py
... ... @@ -0,0 +1,194 @@
  1 +# -*- coding: utf-8 -*-
  2 +"""
  3 + flask.testsuite
  4 + ~~~~~~~~~~~~~~~
  5 +
  6 + Tests Flask itself. The majority of Flask is already tested
  7 + as part of Werkzeug.
  8 +
  9 + :copyright: (c) 2011 by Armin Ronacher.
  10 + :license: BSD, see LICENSE for more details.
  11 +"""
  12 +import os
  13 +import sys
  14 +import flask
  15 +import warnings
  16 +import unittest
  17 +from StringIO import StringIO
  18 +from functools import update_wrapper
  19 +from contextlib import contextmanager
  20 +from werkzeug.utils import import_string, find_modules
  21 +
  22 +
  23 +def add_to_path(path):
  24 + """Adds an entry to sys.path_info if it's not already there."""
  25 + if not os.path.isdir(path):
  26 + raise RuntimeError('Tried to add nonexisting path')
  27 +
  28 + def _samefile(x, y):
  29 + try:
  30 + return os.path.samefile(x, y)
  31 + except (IOError, OSError):
  32 + return False
  33 + for entry in sys.path:
  34 + try:
  35 + if os.path.samefile(path, entry):
  36 + return
  37 + except (OSError, IOError):
  38 + pass
  39 + sys.path.append(path)
  40 +
  41 +
  42 +def iter_suites():
  43 + """Yields all testsuites."""
  44 + for module in find_modules(__name__):
  45 + mod = import_string(module)
  46 + if hasattr(mod, 'suite'):
  47 + yield mod.suite()
  48 +
  49 +
  50 +def find_all_tests(suite):
  51 + """Yields all the tests and their names from a given suite."""
  52 + suites = [suite]
  53 + while suites:
  54 + s = suites.pop()
  55 + try:
  56 + suites.extend(s)
  57 + except TypeError:
  58 + yield s, '%s.%s.%s' % (
  59 + s.__class__.__module__,
  60 + s.__class__.__name__,
  61 + s._testMethodName
  62 + )
  63 +
  64 +
  65 +@contextmanager
  66 +def catch_warnings():
  67 + """Catch warnings in a with block in a list"""
  68 + # make sure deprecation warnings are active in tests
  69 + warnings.simplefilter('default', category=DeprecationWarning)
  70 +
  71 + filters = warnings.filters
  72 + warnings.filters = filters[:]
  73 + old_showwarning = warnings.showwarning
  74 + log = []
  75 + def showwarning(message, category, filename, lineno, file=None, line=None):
  76 + log.append(locals())
  77 + try:
  78 + warnings.showwarning = showwarning
  79 + yield log
  80 + finally:
  81 + warnings.filters = filters
  82 + warnings.showwarning = old_showwarning
  83 +
  84 +
  85 +@contextmanager
  86 +def catch_stderr():
  87 + """Catch stderr in a StringIO"""
  88 + old_stderr = sys.stderr
  89 + sys.stderr = rv = StringIO()
  90 + try:
  91 + yield rv
  92 + finally:
  93 + sys.stderr = old_stderr
  94 +
  95 +
  96 +def emits_module_deprecation_warning(f):
  97 + def new_f(self, *args, **kwargs):
  98 + with catch_warnings() as log:
  99 + f(self, *args, **kwargs)
  100 + self.assert_(log, 'expected deprecation warning')
  101 + for entry in log:
  102 + self.assert_('Modules are deprecated' in str(entry['message']))
  103 + return update_wrapper(new_f, f)
  104 +
  105 +
  106 +class FlaskTestCase(unittest.TestCase):
  107 + """Baseclass for all the tests that Flask uses. Use these methods
  108 + for testing instead of the camelcased ones in the baseclass for
  109 + consistency.
  110 + """
  111 +
  112 + def ensure_clean_request_context(self):
  113 + # make sure we're not leaking a request context since we are
  114 + # testing flask internally in debug mode in a few cases
  115 + self.assert_equal(flask._request_ctx_stack.top, None)
  116 +
  117 + def setup(self):
  118 + pass
  119 +
  120 + def teardown(self):
  121 + pass
  122 +
  123 + def setUp(self):
  124 + self.setup()
  125 +
  126 + def tearDown(self):
  127 + unittest.TestCase.tearDown(self)
  128 + self.ensure_clean_request_context()
  129 + self.teardown()
  130 +
  131 + def assert_equal(self, x, y):
  132 + return self.assertEqual(x, y)
  133 +
  134 +
  135 +class BetterLoader(unittest.TestLoader):
  136 + """A nicer loader that solves two problems. First of all we are setting
  137 + up tests from different sources and we're doing this programmatically
  138 + which breaks the default loading logic so this is required anyways.
  139 + Secondly this loader has a nicer interpolation for test names than the
  140 + default one so you can just do ``run-tests.py ViewTestCase`` and it
  141 + will work.
  142 + """
  143 +
  144 + def getRootSuite(self):
  145 + return suite()
  146 +
  147 + def loadTestsFromName(self, name, module=None):
  148 + root = self.getRootSuite()
  149 + if name == 'suite':
  150 + return root
  151 +
  152 + all_tests = []
  153 + for testcase, testname in find_all_tests(root):
  154 + if testname == name or \
  155 + testname.endswith('.' + name) or \
  156 + ('.' + name + '.') in testname or \
  157 + testname.startswith(name + '.'):
  158 + all_tests.append(testcase)
  159 +
  160 + if not all_tests:
  161 + raise LookupError('could not find test case for "%s"' % name)
  162 +
  163 + if len(all_tests) == 1:
  164 + return all_tests[0]
  165 + rv = unittest.TestSuite()
  166 + for test in all_tests:
  167 + rv.addTest(test)
  168 + return rv
  169 +
  170 +
  171 +def setup_path():
  172 + add_to_path(os.path.abspath(os.path.join(
  173 + os.path.dirname(__file__), 'test_apps')))
  174 +
  175 +
  176 +def suite():
  177 + """A testsuite that has all the Flask tests. You can use this
  178 + function to integrate the Flask tests into your own testsuite
  179 + in case you want to test that monkeypatches to Flask do not
  180 + break it.
  181 + """
  182 + setup_path()
  183 + suite = unittest.TestSuite()
  184 + for other_suite in iter_suites():
  185 + suite.addTest(other_suite)
  186 + return suite
  187 +
  188 +
  189 +def main():
  190 + """Runs the testsuite as command line application."""
  191 + try:
  192 + unittest.main(testLoader=BetterLoader(), defaultTest='suite')
  193 + except Exception, e:
  194 + print 'Error: %s' % e
1,051 flask/testsuite/basic.py
... ... @@ -0,0 +1,1051 @@
  1 +# -*- coding: utf-8 -*-
  2 +"""
  3 + flask.testsuite.basic
  4 + ~~~~~~~~~~~~~~~~~~~~~
  5 +
  6 + The basic functionality.
  7 +
  8 + :copyright: (c) 2011 by Armin Ronacher.
  9 + :license: BSD, see LICENSE for more details.
  10 +"""
  11 +import re
  12 +import flask
  13 +import unittest
  14 +from datetime import datetime
  15 +from threading import Thread
  16 +from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning
  17 +from werkzeug.exceptions import BadRequest, NotFound
  18 +from werkzeug.http import parse_date
  19 +
  20 +
  21 +class BasicFunctionalityTestCase(FlaskTestCase):
  22 +
  23 + def test_options_work(self):
  24 + app = flask.Flask(__name__)
  25 + @app.route('/', methods=['GET', 'POST'])
  26 + def index():
  27 + return 'Hello World'
  28 + rv = app.test_client().open('/', method='OPTIONS')
  29 + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST'])
  30 + self.assert_equal(rv.data, '')
  31 +
  32 + def test_options_on_multiple_rules(self):
  33 + app = flask.Flask(__name__)
  34 + @app.route('/', methods=['GET', 'POST'])
  35 + def index():
  36 + return 'Hello World'
  37 + @app.route('/', methods=['PUT'])
  38 + def index_put():
  39 + return 'Aha!'
  40 + rv = app.test_client().open('/', method='OPTIONS')
  41 + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'])
  42 +
  43 + def test_options_handling_disabled(self):
  44 + app = flask.Flask(__name__)
  45 + def index():
  46 + return 'Hello World!'
  47 + index.provide_automatic_options = False
  48 + app.route('/')(index)
  49 + rv = app.test_client().open('/', method='OPTIONS')
  50 + self.assert_equal(rv.status_code, 405)
  51 +
  52 + app = flask.Flask(__name__)
  53 + def index2():
  54 + return 'Hello World!'
  55 + index2.provide_automatic_options = True
  56 + app.route('/', methods=['OPTIONS'])(index2)
  57 + rv = app.test_client().open('/', method='OPTIONS')
  58 + self.assert_equal(sorted(rv.allow), ['OPTIONS'])
  59 +
  60 + def test_request_dispatching(self):
  61 + app = flask.Flask(__name__)
  62 + @app.route('/')
  63 + def index():
  64 + return flask.request.method
  65 + @app.route('/more', methods=['GET', 'POST'])
  66 + def more():
  67 + return flask.request.method
  68 +
  69 + c = app.test_client()
  70 + self.assert_equal(c.get('/').data, 'GET')
  71 + rv = c.post('/')
  72 + self.assert_equal(rv.status_code, 405)
  73 + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS'])
  74 + rv = c.head('/')
  75 + self.assert_equal(rv.status_code, 200)
  76 + self.assert_(not rv.data) # head truncates
  77 + self.assert_equal(c.post('/more').data, 'POST')
  78 + self.assert_equal(c.get('/more').data, 'GET')
  79 + rv = c.delete('/more')
  80 + self.assert_equal(rv.status_code, 405)
  81 + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST'])
  82 +
  83 + def test_url_mapping(self):
  84 + app = flask.Flask(__name__)
  85 + def index():
  86 + return flask.request.method
  87 + def more():
  88 + return flask.request.method
  89 +
  90 + app.add_url_rule('/', 'index', index)
  91 + app.add_url_rule('/more', 'more', more, methods=['GET', 'POST'])
  92 +
  93 + c = app.test_client()
  94 + self.assert_equal(c.get('/').data, 'GET')
  95 + rv = c.post('/')
  96 + self.assert_equal(rv.status_code, 405)
  97 + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS'])
  98 + rv = c.head('/')
  99 + self.assert_equal(rv.status_code, 200)
  100 + self.assert_(not rv.data) # head truncates
  101 + self.assert_equal(c.post('/more').data, 'POST')
  102 + self.assert_equal(c.get('/more').data, 'GET')
  103 + rv = c.delete('/more')
  104 + self.assert_equal(rv.status_code, 405)
  105 + self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST'])
  106 +
  107 + def test_werkzeug_routing(self):
  108 + from werkzeug.routing import Submount, Rule
  109