Skip to content
Browse files

Merge remote-tracking branch 'mitsuhiko/master'

  • Loading branch information...
2 parents 03a71e0 + ccf4641 commit a9e09ec50e24c379348bc22606e39e88e47e4a8a @s0undt3ch committed Aug 31, 2011
Showing with 3,262 additions and 2,432 deletions.
  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/moduleapp → flask/testsuite/test_apps/blueprintapp}/apps/__init__.py
  42. 0 {tests → flask/testsuite/test_apps}/blueprintapp/apps/admin/__init__.py
  43. 0 {tests/moduleapp → flask/testsuite/test_apps/blueprintapp}/apps/admin/static/css/test.css
  44. 0 {tests/moduleapp → flask/testsuite/test_apps/blueprintapp}/apps/admin/static/test.txt
  45. 0 ...s/admin/templates → 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 ...d/templates → 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/blueprintapp → flask/testsuite/test_apps/moduleapp}/apps/__init__.py
  52. 0 {tests → flask/testsuite/test_apps}/moduleapp/apps/admin/__init__.py
  53. 0 {tests/blueprintapp → flask/testsuite/test_apps/moduleapp}/apps/admin/static/css/test.css
  54. 0 {tests/blueprintapp → flask/testsuite/test_apps/moduleapp}/apps/admin/static/test.txt
  55. 0 ...apps/admin/templates/admin → flask/testsuite/test_apps/moduleapp/apps/admin/templates}/index.html
  56. 0 {tests → flask/testsuite/test_apps}/moduleapp/apps/frontend/__init__.py
  57. 0 ...tend/templates/frontend → 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
View
11 CHANGES
@@ -32,6 +32,17 @@ Relase date to be decided, codename to be chosen.
conceptionally only instance depending and outside version control so it's
the perfect place to put configuration files etc. For more information
see :ref:`instance-folders`.
+- Added the ``APPLICATION_ROOT`` configuration variable.
+- Implemented :meth:`~flask.testing.TestClient.session_transaction` to
+ easily modify sessions from the test environment.
+- Refactored test client internally. The ``APPLICATION_ROOT`` configuration
+ variable as well as ``SERVER_NAME`` are now properly used by the test client
+ as defaults.
+- Added :attr:`flask.views.View.decorators` to support simpler decorating of
+ pluggable (class based) views.
+- Fixed an issue where the test client if used with the with statement did not
+ trigger the execution of the teardown handlers.
+- Added finer control over the session cookie parameters.
Version 0.7.3
-------------
View
5 MANIFEST.in
@@ -1,4 +1,4 @@
-include Makefile CHANGES LICENSE AUTHORS
+include Makefile CHANGES LICENSE AUTHORS run-tests.py
recursive-include artwork *
recursive-include tests *
recursive-include examples *
@@ -9,5 +9,8 @@ recursive-exclude tests *.pyc
recursive-exclude tests *.pyo
recursive-exclude examples *.pyc
recursive-exclude examples *.pyo
+recursive-include flask/testsuite/static *
+recursive-include flask/testsuite/templates *
+recursive-include flask/testsuite/test_apps *
prune docs/_build
prune docs/_themes/.git
View
2 Makefile
@@ -3,7 +3,7 @@
all: clean-pyc test
test:
- python setup.py test
+ python run-tests.py
audit:
python setup.py audit
View
33 README
@@ -11,24 +11,41 @@
~ Is it ready?
- A preview release is out now, and I'm hoping for some
- input about what you want from a microframework and
- how it should look like. Consider the API to slightly
- improve over time.
+ It's still not 1.0 but it's shaping up nicely and is
+ already widely used. Consider the API to slightly
+ improve over time but we don't plan on breaking it.
~ What do I need?
- Jinja 2.4 and Werkzeug 0.6.1. `easy_install` will
- install them for you if you do `easy_install Flask==dev`.
+ Jinja 2.4 and Werkzeug 0.6.1. `pip` or `easy_install` will
+ install them for you if you do `easy_install Flask`.
I encourage you to use a virtualenv. Check the docs for
complete installation and usage instructions.
~ Where are the docs?
- Go to http://flask.pocoo.org/ for a prebuilt version of
- the current documentation. Otherwise build them yourself
+ Go to http://flask.pocoo.org/docs/ for a prebuilt version
+ of the current documentation. Otherwise build them yourself
from the sphinx sources in the docs folder.
+ ~ Where are the tests?
+
+ Good that you're asking. The tests are in the
+ flask/testsuite package. To run the tests use the
+ `run-tests.py` file:
+
+ $ python run-tests.py
+
+ If it's not enough output for you, you can use the
+ `--verbose` flag:
+
+ $ python run-tests.py --verbose
+
+ If you just want one particular testcase to run you can
+ provide it on the command line:
+
+ $ python run-tests.py test_to_run
+
~ Where can I get help?
Either use the #pocoo IRC channel on irc.freenode.net or
View
9 docs/api.rst
@@ -218,6 +218,15 @@ implementation that Flask is using.
:members:
+Test Client
+-----------
+
+.. currentmodule:: flask.testing
+
+.. autoclass:: FlaskClient
+ :members:
+
+
Application Globals
-------------------
View
54 docs/config.rst
@@ -70,13 +70,34 @@ The following configuration values are used internally by Flask:
very risky).
``SECRET_KEY`` the secret key
``SESSION_COOKIE_NAME`` the name of the session cookie
+``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If
+ this is not set, the cookie will be
+ valid for all subdomains of
+ ``SERVER_NAME``.
+``SESSION_COOKIE_PATH`` the path for the session cookie. If
+ this is not set the cookie will be valid
+ for all of ``APPLICATION_ROOT`` or if
+ that is not set for ``'/'``.
+``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set
+ with the httponly flag. Defaults to
+ `True`.
+``SESSION_COOKIE_SECURE`` controls if the cookie should be set
+ with the secure flag. Defaults to
+ `False`.
``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as
:class:`datetime.timedelta` object.
``USE_X_SENDFILE`` enable/disable x-sendfile
``LOGGER_NAME`` the name of the logger
``SERVER_NAME`` the name and port number of the server.
Required for subdomain support (e.g.:
``'localhost:5000'``)
+``APPLICATION_ROOT`` If the application does not occupy
+ a whole domain or subdomain this can
+ be set to the path where the application
+ is configured to live. This is for
+ session cookie as path value. If
+ domains are used, this should be
+ ``None``.
``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will
reject incoming requests with a
content length greater than this by
@@ -134,7 +155,10 @@ The following configuration values are used internally by Flask:
``PROPAGATE_EXCEPTIONS``, ``PRESERVE_CONTEXT_ON_EXCEPTION``
.. versionadded:: 0.8
- ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``
+ ``TRAP_BAD_REQUEST_ERRORS``, ``TRAP_HTTP_EXCEPTIONS``,
+ ``APPLICATION_ROOT``, ``SESSION_COOKIE_DOMAIN``,
+ ``SESSION_COOKIE_PATH``, ``SESSION_COOKIE_HTTPONLY``,
+ ``SESSION_COOKIE_SECURE``
Configuring from Files
----------------------
@@ -291,25 +315,37 @@ With Flask 0.8 a new attribute was introduced:
version control and be deployment specific. It's the perfect place to
drop things that either change at runtime or configuration files.
-To make it easier to put this folder into an ignore list for your version
-control system it's called ``instance`` and placed directly next to your
-package or module by default. This path can be overridden by specifying
-the `instance_path` parameter to your application::
+You can either explicitly provide the path of the instance folder when
+creating the Flask application or you can let Flask autodetect the
+instance folder. For explicit configuration use the `instance_path`
+parameter::
app = Flask(__name__, instance_path='/path/to/instance/folder')
-Default locations::
+Please keep in mind that this path *must* be absolute when provided.
+
+If the `instance_path` parameter is not provided the following default
+locations are used:
+
+- Uninstalled module::
- Module situation:
/myapp.py
/instance
- Package situation:
+- Uninstalled package::
+
/myapp
/__init__.py
/instance
-Please keep in mind that this path *must* be absolute when provided.
+- Installed module or package::
+
+ $PREFIX/lib/python2.X/site-packages/myapp
+ $PREFIX/var/myapp-instance
+
+ ``$PREFIX`` is the prefix of your Python installation. This can be
+ ``/usr`` or the path to your virtualenv. You can print the value of
+ ``sys.prefix`` to see what the prefix is set to.
Since the config object provided loading of configuration files from
relative filenames we made it possible to change the loading via filenames
View
16 docs/design.rst
@@ -79,6 +79,22 @@ Furthermore this design makes it possible to use a factory function to
create the application which is very helpful for unittesting and similar
things (:ref:`app-factories`).
+The Routing System
+------------------
+
+Flask uses the Werkzeug routing system which has was designed to
+automatically order routes by complexity. This means that you can declare
+routes in arbitrary order and they will still work as expected. This is a
+requirement if you want to properly implement decorator based routing
+since decorators could be fired in undefined order when the application is
+split into multiple modules.
+
+Another design decision with the Werkzeug routing system is that routes
+in Werkzeug try to ensure that there is that URLs are unique. Werkzeug
+will go quite far with that in that it will automatically redirect to a
+canonical URL if a route is ambiguous.
+
+
One Template Engine
-------------------
View
54 docs/foreword.rst
@@ -9,27 +9,30 @@ What does "micro" mean?
-----------------------
To me, the "micro" in microframework refers not only to the simplicity and
-small size of the framework, but also to the typically limited complexity
-and size of applications that are written with the framework. Also the
-fact that you can have an entire application in a single Python file. To
-be approachable and concise, a microframework sacrifices a few features
-that may be necessary in larger or more complex applications.
-
-For example, Flask uses thread-local objects internally so that you don't
-have to pass objects around from function to function within a request in
-order to stay threadsafe. While this is a really easy approach and saves
-you a lot of time, it might also cause some troubles for very large
-applications because changes on these thread-local objects can happen
-anywhere in the same thread.
-
-Flask provides some tools to deal with the downsides of this approach but
-it might be an issue for larger applications because in theory
-modifications on these objects might happen anywhere in the same thread.
+small size of the framework, but also the fact that it does not make much
+decisions for you. While Flask does pick a templating engine for you, we
+won't make such decisions for your datastore or other parts.
+
+For us however the term “micro” does not mean that the whole implementation
+has to fit into a single Python file.
+
+One of the design decisions with Flask was that simple tasks should be
+simple and not take up a lot of code and yet not limit yourself. Because
+of that we took a few design choices that some people might find
+surprising or unorthodox. For example, Flask uses thread-local objects
+internally so that you don't have to pass objects around from function to
+function within a request in order to stay threadsafe. While this is a
+really easy approach and saves you a lot of time, it might also cause some
+troubles for very large applications because changes on these thread-local
+objects can happen anywhere in the same thread. In order to solve these
+problems we don't hide the thread locals for you but instead embrace them
+and provide you with a lot of tools to make it as pleasant as possible to
+work with them.
Flask is also based on convention over configuration, which means that
many things are preconfigured. For example, by convention, templates and
static files are in subdirectories within the Python source tree of the
-application.
+application. While this can be changed you usually don't have to.
The main reason however why Flask is called a "microframework" is the idea
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
object relational mappers, form validation, upload handling, various open
authentication technologies and more.
-However Flask is not much code and it is built on a very solid foundation
-and with that it is very easy to adapt for large applications. If you are
-interested in that, check out the :ref:`becomingbig` chapter.
+Since Flask is based on a very solid foundation there is not a lot of code
+in Flask itself. As such it's easy to adapt even for lage applications
+and we are making sure that you can either configure it as much as
+possible by subclassing things or by forking the entire codebase. If you
+are interested in that, check out the :ref:`becomingbig` chapter.
If you are curious about the Flask design principles, head over to the
section about :ref:`design`.
-A Framework and an Example
---------------------------
-
-Flask is not only a microframework; it is also an example. Based on
-Flask, there will be a series of blog posts that explain how to create a
-framework. Flask itself is just one way to implement a framework on top
-of existing libraries. Unlike many other microframeworks, Flask does not
-try to implement everything on its own; it reuses existing code.
-
Web Development is Dangerous
----------------------------
View
10 docs/patterns/sqlite3.rst
@@ -24,7 +24,15 @@ So here is a simple example of how you can use SQLite 3 with Flask::
@app.teardown_request
def teardown_request(exception):
- g.db.close()
+ if hasattr(g, 'db'):
+ g.db.close()
+
+.. note::
+
+ Please keep in mind that the teardown request functions are always
+ executed, even if a before-request handler failed or was never
+ executed. Because of this we have to make sure here that the database
+ is there before we close it.
Connect on Demand
-----------------
View
10 docs/reqcontext.rst
@@ -131,7 +131,9 @@ understand what is actually happening. The new behavior is quite simple:
4. At the end of the request the :meth:`~flask.Flask.teardown_request`
functions are executed. This always happens, even in case of an
- unhandled exception down the road.
+ unhandled exception down the road or if a before-request handler was
+ not executed yet or at all (for example in test environments sometimes
+ you might want to not execute before-request callbacks).
Now what happens on errors? In production mode if an exception is not
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:
this runs after request
>>>
+Keep in mind that teardown callbacks are always executed, even if
+before-request callbacks were not executed yet but an exception happened.
+Certain parts of the test system might also temporarily create a request
+context without calling the before-request handlers. Make sure to write
+your teardown-request handlers in a way that they will never fail.
+
.. _notes-on-proxies:
Notes On Proxies
View
32 docs/testing.rst
@@ -273,3 +273,35 @@ is no longer available (because you are trying to use it outside of the actual r
However, keep in mind that any :meth:`~flask.Flask.after_request` functions
are already called at this point so your database connection and
everything involved is probably already closed down.
+
+
+Accessing and Modifying Sessions
+--------------------------------
+
+.. versionadded:: 0.8
+
+Sometimes it can be very helpful to access or modify the sessions from the
+test client. Generally there are two ways for this. If you just want to
+ensure that a session has certain keys set to certain values you can just
+keep the context around and access :data:`flask.session`::
+
+ with app.test_client() as c:
+ rv = c.get('/')
+ assert flask.session['foo'] == 42
+
+This however does not make it possible to also modify the session or to
+access the session before a request was fired. Starting with Flask 0.8 we
+provide a so called “session transaction” which simulates the appropriate
+calls to open a session in the context of the test client and to modify
+it. At the end of the transaction the session is stored. This works
+independently of the session backend used::
+
+ with app.test_client() as c:
+ with c.session_transaction() as sess:
+ sess['a_key'] = 'a value'
+
+ # once this is reached the session was stored
+
+Note that in this case you have to use the ``sess`` object instead of the
+:data:`flask.session` proxy. The object however itself will provide the
+same interface.
View
8 docs/upgrading.rst
@@ -36,6 +36,11 @@ longer have to handle that error to avoid an internal server error showing
up for the user. If you were catching this down explicitly in the past
as `ValueError` you will need to change this.
+Due to a bug in the test client Flask 0.7 did not trigger teardown
+handlers when the test client was used in a with statement. This was
+since fixed but might require some changes in your testsuites if you
+relied on this behavior.
+
Version 0.7
-----------
@@ -142,7 +147,8 @@ You are now encouraged to use this instead::
@app.teardown_request
def after_request(exception):
- g.db.close()
+ if hasattr(g, 'db'):
+ g.db.close()
On the upside this change greatly improves the internal code flow and
makes it easier to customize the dispatching and error handling. This
View
21 docs/views.rst
@@ -135,3 +135,24 @@ easily do that. Each HTTP method maps to a function with the same name
That way you also don't have to provide the
:attr:`~flask.views.View.methods` attribute. It's automatically set based
on the methods defined in the class.
+
+Decorating Views
+----------------
+
+Since the view class itself is not the view function that is added to the
+routing system it does not make much sense to decorate the class itself.
+Instead you either have to decorate the return value of
+:meth:`~flask.views.View.as_view` by hand::
+
+ view = rate_limited(UserAPI.as_view('users'))
+ app.add_url_rule('/users/', view_func=view)
+
+Starting with Flask 0.8 there is also an alternative way where you can
+specify a list of decorators to apply in the class declaration::
+
+ class UserAPI(MethodView):
+ decorators = [rate_limited]
+
+Due to the implicit self from the caller's perspective you cannot use
+regular view decorators on the individual methods of the view however,
+keep this in mind.
View
3 examples/flaskr/flaskr.py
@@ -50,7 +50,8 @@ def before_request():
@app.teardown_request
def teardown_request(exception):
"""Closes the database again at the end of the request."""
- g.db.close()
+ if hasattr(g, 'db'):
+ g.db.close()
@app.route('/')
View
3 examples/minitwit/minitwit.py
@@ -85,7 +85,8 @@ def before_request():
@app.teardown_request
def teardown_request(exception):
"""Closes the database again at the end of the request."""
- g.db.close()
+ if hasattr(g, 'db'):
+ g.db.close()
@app.route('/')
View
73 flask/app.py
@@ -231,11 +231,16 @@ class Flask(_PackageBoundObject):
'PROPAGATE_EXCEPTIONS': None,
'PRESERVE_CONTEXT_ON_EXCEPTION': None,
'SECRET_KEY': None,
- 'SESSION_COOKIE_NAME': 'session',
'PERMANENT_SESSION_LIFETIME': timedelta(days=31),
'USE_X_SENDFILE': False,
'LOGGER_NAME': None,
'SERVER_NAME': None,
+ 'APPLICATION_ROOT': None,
+ 'SESSION_COOKIE_NAME': 'session',
+ 'SESSION_COOKIE_DOMAIN': None,
+ 'SESSION_COOKIE_PATH': None,
+ 'SESSION_COOKIE_HTTPONLY': True,
+ 'SESSION_COOKIE_SECURE': False,
'MAX_CONTENT_LENGTH': None,
'TRAP_BAD_REQUEST_ERRORS': False,
'TRAP_HTTP_EXCEPTIONS': False
@@ -705,6 +710,8 @@ def test_client(self, use_cookies=True):
rv = c.get('/?vodka=42')
assert request.args['vodka'] == '42'
+ See :class:`~flask.testing.FlaskClient` for more information.
+
.. versionchanged:: 0.4
added support for `with` block usage for the client.
@@ -1217,14 +1224,24 @@ def handle_exception(self, e):
else:
raise e
- self.logger.exception('Exception on %s [%s]' % (
- request.path,
- request.method
- ))
+ self.log_exception((exc_type, exc_value, tb))
if handler is None:
return InternalServerError()
return handler(e)
+ def log_exception(self, exc_info):
+ """Logs an exception. This is called by :meth:`handle_exception`
+ if debugging is disabled and right before the handler is called.
+ The default implementation logs the exception as error on the
+ :attr:`logger`.
+
+ .. versionadded:: 0.8
+ """
+ self.logger.error('Exception on %s [%s]' % (
+ request.path,
+ request.method
+ ), exc_info=exc_info)
+
def raise_routing_exception(self, request):
"""Exceptions that are recording during routing are reraised with
this method. During debug we are not reraising redirect requests
@@ -1306,17 +1323,18 @@ def make_default_options_response(self):
.. versionadded:: 0.7
"""
- # This would be nicer in Werkzeug 0.7, which however currently
- # is not released. Werkzeug 0.7 provides a method called
- # allowed_methods() that returns all methods that are valid for
- # a given path.
- methods = []
- try:
- _request_ctx_stack.top.url_adapter.match(method='--')
- except MethodNotAllowed, e:
- methods = e.valid_methods
- except HTTPException, e:
- pass
+ adapter = _request_ctx_stack.top.url_adapter
+ if hasattr(adapter, 'allowed_methods'):
+ methods = adapter.allowed_methods()
+ else:
+ # fallback for Werkzeug < 0.7
+ methods = []
+ try:
+ adapter.match(method='--')
+ except MethodNotAllowed, e:
+ methods = e.valid_methods
+ except HTTPException, e:
+ pass
rv = self.response_class()
rv.allow.update(methods)
return rv
@@ -1387,7 +1405,7 @@ def preprocess_request(self):
This also triggers the :meth:`url_value_processor` functions before
the actualy :meth:`before_request` functions are called.
"""
- bp = request.blueprint
+ bp = _request_ctx_stack.top.request.blueprint
funcs = self.url_value_preprocessors.get(None, ())
if bp is not None and bp in self.url_value_preprocessors:
@@ -1437,7 +1455,7 @@ def do_teardown_request(self):
tighter control over certain resources under testing environments.
"""
funcs = reversed(self.teardown_request_funcs.get(None, ()))
- bp = request.blueprint
+ bp = _request_ctx_stack.top.request.blueprint
if bp is not None and bp in self.teardown_request_funcs:
funcs = chain(funcs, reversed(self.teardown_request_funcs[bp]))
exc = sys.exc_info()[1]
@@ -1482,19 +1500,12 @@ def test_request_context(self, *args, **kwargs):
:func:`werkzeug.test.EnvironBuilder` for more information, this
function accepts the same arguments).
"""
- from werkzeug.test import create_environ
- environ_overrides = kwargs.setdefault('environ_overrides', {})
- if self.config.get('SERVER_NAME'):
- server_name = self.config.get('SERVER_NAME')
- if ':' not in server_name:
- http_host, http_port = server_name, '80'
- else:
- http_host, http_port = server_name.split(':', 1)
-
- environ_overrides.setdefault('SERVER_NAME', server_name)
- environ_overrides.setdefault('HTTP_HOST', server_name)
- environ_overrides.setdefault('SERVER_PORT', http_port)
- return self.request_context(create_environ(*args, **kwargs))
+ from flask.testing import make_test_environ_builder
+ builder = make_test_environ_builder(self, *args, **kwargs)
+ try:
+ return self.request_context(builder.get_environ())
+ finally:
+ builder.close()
def wsgi_app(self, environ, start_response):
"""The actual WSGI application. This is not implemented in
View
2 flask/ctx.py
@@ -91,7 +91,7 @@ def __init__(self, app, environ):
self.match_request()
- # Support for deprecated functionality. This is doing away with
+ # XXX: Support for deprecated functionality. This is doing away with
# Flask 1.0
blueprint = self.request.blueprint
if blueprint is not None:
View
3 flask/debughelpers.py
@@ -54,7 +54,8 @@ def __init__(self, request):
buf.append(' Make sure to directly send your %s-request to this URL '
'since we can\'t make browsers or HTTP clients redirect '
- 'with form data.' % request.method)
+ 'with form data reliably or without user interaction.' %
+ request.method)
buf.append('\n\nNote: this exception is only raised in debug mode')
AssertionError.__init__(self, ''.join(buf).encode('utf-8'))
View
10 flask/helpers.py
@@ -145,6 +145,13 @@ def index():
response = make_response(render_template('not_found.html'), 404)
+ The other use case of this function is to force the return value of a
+ view function into a response which is helpful with view
+ decorators::
+
+ response = make_response(view_function())
+ response.headers['X-Parachutes'] = 'parachutes are cool'
+
Internally this function does the following things:
- if no arguments are passed, it creates a new response argument
@@ -477,6 +484,8 @@ def get_root_path(import_name):
directory = os.path.dirname(sys.modules[import_name].__file__)
return os.path.abspath(directory)
except AttributeError:
+ # this is necessary in case we are running from the interactive
+ # python shell. It will never be used for production code however
return os.getcwd()
@@ -492,6 +501,7 @@ def find_package(import_name):
root_mod = sys.modules[import_name.split('.')[0]]
package_path = getattr(root_mod, '__file__', None)
if package_path is None:
+ # support for the interactive python shell
package_path = os.getcwd()
else:
package_path = os.path.abspath(os.path.dirname(package_path))
View
34 flask/sessions.py
@@ -123,10 +123,34 @@ def get_cookie_domain(self, app):
"""Helpful helper method that returns the cookie domain that should
be used for the session cookie if session cookies are used.
"""
+ if app.config['SESSION_COOKIE_DOMAIN'] is not None:
+ return app.config['SESSION_COOKIE_DOMAIN']
if app.config['SERVER_NAME'] is not None:
# chop of the port which is usually not supported by browsers
return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
+ def get_cookie_path(self, app):
+ """Returns the path for which the cookie should be valid. The
+ default implementation uses the value from the SESSION_COOKIE_PATH``
+ config var if it's set, and falls back to ``APPLICATION_ROOT`` or
+ uses ``/`` if it's `None`.
+ """
+ return app.config['SESSION_COOKIE_PATH'] or \
+ app.config['APPLICATION_ROOT'] or '/'
+
+ def get_cookie_httponly(self, app):
+ """Returns True if the session cookie should be httponly. This
+ currently just returns the value of the ``SESSION_COOKIE_HTTPONLY``
+ config var.
+ """
+ return app.config['SESSION_COOKIE_HTTPONLY']
+
+ def get_cookie_secure(self, app):
+ """Returns True if the cookie should be secure. This currently
+ just returns the value of the ``SESSION_COOKIE_SECURE`` setting.
+ """
+ return app.config['SESSION_COOKIE_SECURE']
+
def get_expiration_time(self, app, session):
"""A helper method that returns an expiration date for the session
or `None` if the session is linked to the browser session. The
@@ -169,9 +193,13 @@ def open_session(self, app, request):
def save_session(self, app, session, response):
expires = self.get_expiration_time(app, session)
domain = self.get_cookie_domain(app)
+ path = self.get_cookie_path(app)
+ httponly = self.get_cookie_httponly(app)
+ secure = self.get_cookie_secure(app)
if session.modified and not session:
- response.delete_cookie(app.session_cookie_name,
+ response.delete_cookie(app.session_cookie_name, path=path,
domain=domain)
else:
- session.save_cookie(response, app.session_cookie_name,
- expires=expires, httponly=True, domain=domain)
+ session.save_cookie(response, app.session_cookie_name, path=path,
+ expires=expires, httponly=httponly,
+ secure=secure, domain=domain)
View
2 flask/signals.py
@@ -34,7 +34,7 @@ def _fail(self, *args, **kwargs):
'not installed.')
send = lambda *a, **kw: None
connect = disconnect = has_receivers_for = receivers_for = \
- temporarily_connected_to = _fail
+ temporarily_connected_to = connected_to = _fail
del _fail
# the namespace for code signals. If you are not flask code, do
View
100 flask/testing.py
@@ -10,44 +10,91 @@
:license: BSD, see LICENSE for more details.
"""
+from contextlib import contextmanager
from werkzeug.test import Client, EnvironBuilder
from flask import _request_ctx_stack
+def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs):
+ """Creates a new test builder with some application defaults thrown in."""
+ http_host = app.config.get('SERVER_NAME')
+ app_root = app.config.get('APPLICATION_ROOT')
+ if base_url is None:
+ base_url = 'http://%s/' % (http_host or 'localhost')
+ if app_root:
+ base_url += app_root.lstrip('/')
+ return EnvironBuilder(path, base_url, *args, **kwargs)
+
+
class FlaskClient(Client):
- """Works like a regular Werkzeug test client but has some
- knowledge about how Flask works to defer the cleanup of the
- request context stack to the end of a with body when used
- in a with statement.
+ """Works like a regular Werkzeug test client but has some knowledge about
+ how Flask works to defer the cleanup of the request context stack to the
+ end of a with body when used in a with statement. For general information
+ about how to use this class refer to :class:`werkzeug.test.Client`.
+
+ Basic usage is outlined in the :ref:`testing` chapter.
"""
preserve_context = context_preserved = False
+ @contextmanager
+ def session_transaction(self, *args, **kwargs):
+ """When used in combination with a with statement this opens a
+ session transaction. This can be used to modify the session that
+ the test client uses. Once the with block is left the session is
+ stored back.
+
+ with client.session_transaction() as session:
+ session['value'] = 42
+
+ Internally this is implemented by going through a temporary test
+ request context and since session handling could depend on
+ request variables this function accepts the same arguments as
+ :meth:`~flask.Flask.test_request_context` which are directly
+ passed through.
+ """
+ if self.cookie_jar is None:
+ raise RuntimeError('Session transactions only make sense '
+ 'with cookies enabled.')
+ app = self.application
+ environ_overrides = kwargs.pop('environ_overrides', {})
+ self.cookie_jar.inject_wsgi(environ_overrides)
+ outer_reqctx = _request_ctx_stack.top
+ with app.test_request_context(*args, **kwargs) as c:
+ sess = app.open_session(c.request)
+ if sess is None:
+ raise RuntimeError('Session backend did not open a session. '
+ 'Check the configuration')
+
+ # Since we have to open a new request context for the session
+ # handling we want to make sure that we hide out own context
+ # from the caller. By pushing the original request context
+ # (or None) on top of this and popping it we get exactly that
+ # behavior. It's important to not use the push and pop
+ # methods of the actual request context object since that would
+ # mean that cleanup handlers are called
+ _request_ctx_stack.push(outer_reqctx)
+ try:
+ yield sess
+ finally:
+ _request_ctx_stack.pop()
+
+ resp = app.response_class()
+ if not app.session_interface.is_null_session(sess):
+ app.save_session(sess, resp)
+ headers = resp.get_wsgi_headers(c.request.environ)
+ self.cookie_jar.extract_wsgi(c.request.environ, headers)
+
def open(self, *args, **kwargs):
- if self.context_preserved:
- _request_ctx_stack.pop()
- self.context_preserved = False
+ self._pop_reqctx_if_necessary()
kwargs.setdefault('environ_overrides', {}) \
['flask._preserve_context'] = self.preserve_context
as_tuple = kwargs.pop('as_tuple', False)
buffered = kwargs.pop('buffered', False)
follow_redirects = kwargs.pop('follow_redirects', False)
+ builder = make_test_environ_builder(self.application, *args, **kwargs)
- builder = EnvironBuilder(*args, **kwargs)
-
- if self.application.config.get('SERVER_NAME'):
- server_name = self.application.config.get('SERVER_NAME')
- if ':' not in server_name:
- http_host, http_port = server_name, None
- else:
- http_host, http_port = server_name.split(':', 1)
- if builder.base_url == 'http://localhost/':
- # Default Generated Base URL
- if http_port != None:
- builder.host = http_host + ':' + http_port
- else:
- builder.host = http_host
old = _request_ctx_stack.top
try:
return Client.open(self, builder,
@@ -58,10 +105,19 @@ def open(self, *args, **kwargs):
self.context_preserved = _request_ctx_stack.top is not old
def __enter__(self):
+ if self.preserve_context:
+ raise RuntimeError('Cannot nest client invocations')
self.preserve_context = True
return self
def __exit__(self, exc_type, exc_value, tb):
self.preserve_context = False
+ self._pop_reqctx_if_necessary()
+
+ def _pop_reqctx_if_necessary(self):
if self.context_preserved:
- _request_ctx_stack.pop()
+ # we have to use _request_ctx_stack.top.pop instead of
+ # _request_ctx_stack.pop since we want teardown handlers
+ # to be executed.
+ _request_ctx_stack.top.pop()
+ self.context_preserved = False
View
194 flask/testsuite/__init__.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.testsuite
+ ~~~~~~~~~~~~~~~
+
+ Tests Flask itself. The majority of Flask is already tested
+ as part of Werkzeug.
+
+ :copyright: (c) 2011 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+import os
+import sys
+import flask
+import warnings
+import unittest
+from StringIO import StringIO
+from functools import update_wrapper
+from contextlib import contextmanager
+from werkzeug.utils import import_string, find_modules
+
+
+def add_to_path(path):
+ """Adds an entry to sys.path_info if it's not already there."""
+ if not os.path.isdir(path):
+ raise RuntimeError('Tried to add nonexisting path')
+
+ def _samefile(x, y):
+ try:
+ return os.path.samefile(x, y)
+ except (IOError, OSError):
+ return False
+ for entry in sys.path:
+ try:
+ if os.path.samefile(path, entry):
+ return
+ except (OSError, IOError):
+ pass
+ sys.path.append(path)
+
+
+def iter_suites():
+ """Yields all testsuites."""
+ for module in find_modules(__name__):
+ mod = import_string(module)
+ if hasattr(mod, 'suite'):
+ yield mod.suite()
+
+
+def find_all_tests(suite):
+ """Yields all the tests and their names from a given suite."""
+ suites = [suite]
+ while suites:
+ s = suites.pop()
+ try:
+ suites.extend(s)
+ except TypeError:
+ yield s, '%s.%s.%s' % (
+ s.__class__.__module__,
+ s.__class__.__name__,
+ s._testMethodName
+ )
+
+
+@contextmanager
+def catch_warnings():
+ """Catch warnings in a with block in a list"""
+ # make sure deprecation warnings are active in tests
+ warnings.simplefilter('default', category=DeprecationWarning)
+
+ filters = warnings.filters
+ warnings.filters = filters[:]
+ old_showwarning = warnings.showwarning
+ log = []
+ def showwarning(message, category, filename, lineno, file=None, line=None):
+ log.append(locals())
+ try:
+ warnings.showwarning = showwarning
+ yield log
+ finally:
+ warnings.filters = filters
+ warnings.showwarning = old_showwarning
+
+
+@contextmanager
+def catch_stderr():
+ """Catch stderr in a StringIO"""
+ old_stderr = sys.stderr
+ sys.stderr = rv = StringIO()
+ try:
+ yield rv
+ finally:
+ sys.stderr = old_stderr
+
+
+def emits_module_deprecation_warning(f):
+ def new_f(self, *args, **kwargs):
+ with catch_warnings() as log:
+ f(self, *args, **kwargs)
+ self.assert_(log, 'expected deprecation warning')
+ for entry in log:
+ self.assert_('Modules are deprecated' in str(entry['message']))
+ return update_wrapper(new_f, f)
+
+
+class FlaskTestCase(unittest.TestCase):
+ """Baseclass for all the tests that Flask uses. Use these methods
+ for testing instead of the camelcased ones in the baseclass for
+ consistency.
+ """
+
+ def ensure_clean_request_context(self):
+ # make sure we're not leaking a request context since we are
+ # testing flask internally in debug mode in a few cases
+ self.assert_equal(flask._request_ctx_stack.top, None)
+
+ def setup(self):
+ pass
+
+ def teardown(self):
+ pass
+
+ def setUp(self):
+ self.setup()
+
+ def tearDown(self):
+ unittest.TestCase.tearDown(self)
+ self.ensure_clean_request_context()
+ self.teardown()
+
+ def assert_equal(self, x, y):
+ return self.assertEqual(x, y)
+
+
+class BetterLoader(unittest.TestLoader):
+ """A nicer loader that solves two problems. First of all we are setting
+ up tests from different sources and we're doing this programmatically
+ which breaks the default loading logic so this is required anyways.
+ Secondly this loader has a nicer interpolation for test names than the
+ default one so you can just do ``run-tests.py ViewTestCase`` and it
+ will work.
+ """
+
+ def getRootSuite(self):
+ return suite()
+
+ def loadTestsFromName(self, name, module=None):
+ root = self.getRootSuite()
+ if name == 'suite':
+ return root
+
+ all_tests = []
+ for testcase, testname in find_all_tests(root):
+ if testname == name or \
+ testname.endswith('.' + name) or \
+ ('.' + name + '.') in testname or \
+ testname.startswith(name + '.'):
+ all_tests.append(testcase)
+
+ if not all_tests:
+ raise LookupError('could not find test case for "%s"' % name)
+
+ if len(all_tests) == 1:
+ return all_tests[0]
+ rv = unittest.TestSuite()
+ for test in all_tests:
+ rv.addTest(test)
+ return rv
+
+
+def setup_path():
+ add_to_path(os.path.abspath(os.path.join(
+ os.path.dirname(__file__), 'test_apps')))
+
+
+def suite():
+ """A testsuite that has all the Flask tests. You can use this
+ function to integrate the Flask tests into your own testsuite
+ in case you want to test that monkeypatches to Flask do not
+ break it.
+ """
+ setup_path()
+ suite = unittest.TestSuite()
+ for other_suite in iter_suites():
+ suite.addTest(other_suite)
+ return suite
+
+
+def main():
+ """Runs the testsuite as command line application."""
+ try:
+ unittest.main(testLoader=BetterLoader(), defaultTest='suite')
+ except Exception, e:
+ print 'Error: %s' % e
View
1,051 flask/testsuite/basic.py
@@ -0,0 +1,1051 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.testsuite.basic
+ ~~~~~~~~~~~~~~~~~~~~~
+
+ The basic functionality.
+
+ :copyright: (c) 2011 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+import re
+import flask
+import unittest
+from datetime import datetime
+from threading import Thread
+from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning
+from werkzeug.exceptions import BadRequest, NotFound
+from werkzeug.http import parse_date
+
+
+class BasicFunctionalityTestCase(FlaskTestCase):
+
+ def test_options_work(self):
+ app = flask.Flask(__name__)
+ @app.route('/', methods=['GET', 'POST'])
+ def index():
+ return 'Hello World'
+ rv = app.test_client().open('/', method='OPTIONS')
+ self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST'])
+ self.assert_equal(rv.data, '')
+
+ def test_options_on_multiple_rules(self):
+ app = flask.Flask(__name__)
+ @app.route('/', methods=['GET', 'POST'])
+ def index():
+ return 'Hello World'
+ @app.route('/', methods=['PUT'])
+ def index_put():
+ return 'Aha!'
+ rv = app.test_client().open('/', method='OPTIONS')
+ self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'])
+
+ def test_options_handling_disabled(self):
+ app = flask.Flask(__name__)
+ def index():
+ return 'Hello World!'
+ index.provide_automatic_options = False
+ app.route('/')(index)
+ rv = app.test_client().open('/', method='OPTIONS')
+ self.assert_equal(rv.status_code, 405)
+
+ app = flask.Flask(__name__)
+ def index2():
+ return 'Hello World!'
+ index2.provide_automatic_options = True
+ app.route('/', methods=['OPTIONS'])(index2)
+ rv = app.test_client().open('/', method='OPTIONS')
+ self.assert_equal(sorted(rv.allow), ['OPTIONS'])
+
+ def test_request_dispatching(self):
+ app = flask.Flask(__name__)
+ @app.route('/')
+ def index():
+ return flask.request.method
+ @app.route('/more', methods=['GET', 'POST'])
+ def more():
+ return flask.request.method
+
+ c = app.test_client()
+ self.assert_equal(c.get('/').data, 'GET')
+ rv = c.post('/')
+ self.assert_equal(rv.status_code, 405)
+ self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS'])
+ rv = c.head('/')
+ self.assert_equal(rv.status_code, 200)
+ self.assert_(not rv.data) # head truncates
+ self.assert_equal(c.post('/more').data, 'POST')
+ self.assert_equal(c.get('/more').data, 'GET')
+ rv = c.delete('/more')
+ self.assert_equal(rv.status_code, 405)
+ self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST'])
+
+ def test_url_mapping(self):
+ app = flask.Flask(__name__)
+ def index():
+ return flask.request.method
+ def more():
+ return flask.request.method
+
+ app.add_url_rule('/', 'index', index)
+ app.add_url_rule('/more', 'more', more, methods=['GET', 'POST'])
+
+ c = app.test_client()
+ self.assert_equal(c.get('/').data, 'GET')
+ rv = c.post('/')
+ self.assert_equal(rv.status_code, 405)
+ self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS'])
+ rv = c.head('/')
+ self.assert_equal(rv.status_code, 200)
+ self.assert_(not rv.data) # head truncates
+ self.assert_equal(c.post('/more').data, 'POST')
+ self.assert_equal(c.get('/more').data, 'GET')
+ rv = c.delete('/more')
+ self.assert_equal(rv.status_code, 405)
+ self.assert_equal(sorted(rv.allow), ['GET', 'HEAD', 'OPTIONS', 'POST'])
+
+ def test_werkzeug_routing(self):
+ from werkzeug.routing import Submount, Rule
+ app = flask.Flask(__name__)
+ app.url_map.add(Submount('/foo', [
+ Rule('/bar', endpoint='bar'),
+ Rule('/', endpoint='index')
+ ]))
+ def bar():
+ return 'bar'
+ def index():
+ return 'index'
+ app.view_functions['bar'] = bar
+ app.view_functions['index'] = index
+
+ c = app.test_client()
+ self.assert_equal(c.get('/foo/').data, 'index')
+ self.assert_equal(c.get('/foo/bar').data, 'bar')
+
+ def test_endpoint_decorator(self):
+ from werkzeug.routing import Submount, Rule
+ app = flask.Flask(__name__)
+ app.url_map.add(Submount('/foo', [
+ Rule('/bar', endpoint='bar'),
+ Rule('/', endpoint='index')
+ ]))
+
+ @app.endpoint('bar')
+ def bar():
+ return 'bar'
+
+ @app.endpoint('index')
+ def index():
+ return 'index'
+
+ c = app.test_client()
+ self.assert_equal(c.get('/foo/').data, 'index')
+ self.assert_equal(c.get('/foo/bar').data, 'bar')
+
+ def test_session(self):
+ app = flask.Flask(__name__)
+ app.secret_key = 'testkey'
+ @app.route('/set', methods=['POST'])
+ def set():
+ flask.session['value'] = flask.request.form['value']
+ return 'value set'
+ @app.route('/get')
+ def get():
+ return flask.session['value']
+
+ c = app.test_client()
+ self.assert_equal(c.post('/set', data={'value': '42'}).data, 'value set')
+ self.assert_equal(c.get('/get').data, '42')
+
+ def test_session_using_server_name(self):
+ app = flask.Flask(__name__)
+ app.config.update(
+ SECRET_KEY='foo',
+ SERVER_NAME='example.com'
+ )
+ @app.route('/')
+ def index():
+ flask.session['testing'] = 42
+ return 'Hello World'
+ rv = app.test_client().get('/', 'http://example.com/')
+ self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower())
+ self.assert_('httponly' in rv.headers['set-cookie'].lower())
+
+ def test_session_using_server_name_and_port(self):
+ app = flask.Flask(__name__)
+ app.config.update(
+ SECRET_KEY='foo',
+ SERVER_NAME='example.com:8080'
+ )
+ @app.route('/')
+ def index():
+ flask.session['testing'] = 42
+ return 'Hello World'
+ rv = app.test_client().get('/', 'http://example.com:8080/')
+ self.assert_('domain=.example.com' in rv.headers['set-cookie'].lower())
+ self.assert_('httponly' in rv.headers['set-cookie'].lower())
+
+ def test_session_using_application_root(self):
+ class PrefixPathMiddleware(object):
+ def __init__(self, app, prefix):
+ self.app = app
+ self.prefix = prefix
+ def __call__(self, environ, start_response):
+ environ['SCRIPT_NAME'] = self.prefix
+ return self.app(environ, start_response)
+
+ app = flask.Flask(__name__)
+ app.wsgi_app = PrefixPathMiddleware(app.wsgi_app, '/bar')
+ app.config.update(
+ SECRET_KEY='foo',
+ APPLICATION_ROOT='/bar'
+ )
+ @app.route('/')
+ def index():
+ flask.session['testing'] = 42
+ return 'Hello World'
+ rv = app.test_client().get('/', 'http://example.com:8080/')
+ self.assert_('path=/bar' in rv.headers['set-cookie'].lower())
+
+ def test_session_using_session_settings(self):
+ app = flask.Flask(__name__)
+ app.config.update(
+ SECRET_KEY='foo',
+ SERVER_NAME='www.example.com:8080',
+ APPLICATION_ROOT='/test',
+ SESSION_COOKIE_DOMAIN='.example.com',
+ SESSION_COOKIE_HTTPONLY=False,
+ SESSION_COOKIE_SECURE=True,
+ SESSION_COOKIE_PATH='/'
+ )
+ @app.route('/')
+ def index():
+ flask.session['testing'] = 42
+ return 'Hello World'
+ rv = app.test_client().get('/', 'http://www.example.com:8080/test/')
+ cookie = rv.headers['set-cookie'].lower()
+ self.assert_('domain=.example.com' in cookie)
+ self.assert_('path=/;' in cookie)
+ self.assert_('secure' in cookie)
+ self.assert_('httponly' not in cookie)
+
+ def test_missing_session(self):
+ app = flask.Flask(__name__)
+ def expect_exception(f, *args, **kwargs):
+ try:
+ f(*args, **kwargs)
+ except RuntimeError, e:
+ self.assert_(e.args and 'session is unavailable' in e.args[0])
+ else:
+ self.assert_(False, 'expected exception')
+ with app.test_request_context():
+ self.assert_(flask.session.get('missing_key') is None)
+ expect_exception(flask.session.__setitem__, 'foo', 42)
+ expect_exception(flask.session.pop, 'foo')
+
+ def test_session_expiration(self):
+ permanent = True
+ app = flask.Flask(__name__)
+ app.secret_key = 'testkey'
+ @app.route('/')
+ def index():
+ flask.session['test'] = 42
+ flask.session.permanent = permanent
+ return ''
+
+ @app.route('/test')
+ def test():
+ return unicode(flask.session.permanent)
+
+ client = app.test_client()
+ rv = client.get('/')
+ self.assert_('set-cookie' in rv.headers)
+ match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie'])
+ expires = parse_date(match.group())
+ expected = datetime.utcnow() + app.permanent_session_lifetime
+ self.assert_equal(expires.year, expected.year)
+ self.assert_equal(expires.month, expected.month)
+ self.assert_equal(expires.day, expected.day)
+
+ rv = client.get('/test')
+ self.assert_equal(rv.data, 'True')
+
+ permanent = False
+ rv = app.test_client().get('/')
+ self.assert_('set-cookie' in rv.headers)
+ match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie'])
+ self.assert_(match is None)
+
+ def test_flashes(self):
+ app = flask.Flask(__name__)
+ app.secret_key = 'testkey'
+
+ with app.test_request_context():
+ self.assert_(not flask.session.modified)
+ flask.flash('Zap')
+ flask.session.modified = False
+ flask.flash('Zip')
+ self.assert_(flask.session.modified)
+ self.assert_equal(list(flask.get_flashed_messages()), ['Zap', 'Zip'])
+
+ def test_extended_flashing(self):
+ app = flask.Flask(__name__)
+ app.secret_key = 'testkey'
+
+ @app.route('/')
+ def index():
+ flask.flash(u'Hello World')
+ flask.flash(u'Hello World', 'error')
+ flask.flash(flask.Markup(u'<em>Testing</em>'), 'warning')
+ return ''
+
+ @app.route('/test')
+ def test():
+ messages = flask.get_flashed_messages(with_categories=True)
+ self.assert_equal(len(messages), 3)
+ self.assert_equal(messages[0], ('message', u'Hello World'))
+ self.assert_equal(messages[1], ('error', u'Hello World'))
+ self.assert_equal(messages[2], ('warning', flask.Markup(u'<em>Testing</em>')))
+ return ''
+ messages = flask.get_flashed_messages()
+ self.assert_equal(len(messages), 3)
+ self.assert_equal(messages[0], u'Hello World')
+ self.assert_equal(messages[1], u'Hello World')
+ self.assert_equal(messages[2], flask.Markup(u'<em>Testing</em>'))
+
+ c = app.test_client()
+ c.get('/')
+ c.get('/test')
+
+ def test_request_processing(self):
+ app = flask.Flask(__name__)
+ evts = []
+ @app.before_request
+ def before_request():
+ evts.append('before')
+ @app.after_request
+ def after_request(response):
+ response.data += '|after'
+ evts.append('after')
+ return response
+ @app.route('/')
+ def index():
+ self.assert_('before' in evts)
+ self.assert_('after' not in evts)
+ return 'request'
+ self.assert_('after' not in evts)
+ rv = app.test_client().get('/').data
+ self.assert_('after' in evts)
+ self.assert_equal(rv, 'request|after')
+
+ def test_teardown_request_handler(self):
+ called = []
+ app = flask.Flask(__name__)
+ @app.teardown_request
+ def teardown_request(exc):
+ called.append(True)
+ return "Ignored"
+ @app.route('/')
+ def root():
+ return "Response"
+ rv = app.test_client().get('/')
+ self.assert_equal(rv.status_code, 200)
+ self.assert_('Response' in rv.data)
+ self.assert_equal(len(called), 1)
+
+ def test_teardown_request_handler_debug_mode(self):
+ called = []
+ app = flask.Flask(__name__)
+ app.testing = True
+ @app.teardown_request
+ def teardown_request(exc):
+ called.append(True)
+ return "Ignored"
+ @app.route('/')
+ def root():
+ return "Response"
+ rv = app.test_client().get('/')
+ self.assert_equal(rv.status_code, 200)
+ self.assert_('Response' in rv.data)
+ self.assert_equal(len(called), 1)
+
+ def test_teardown_request_handler_error(self):
+ called = []
+ app = flask.Flask(__name__)
+ @app.teardown_request
+ def teardown_request1(exc):
+ self.assert_equal(type(exc), ZeroDivisionError)
+ called.append(True)
+ # This raises a new error and blows away sys.exc_info(), so we can
+ # test that all teardown_requests get passed the same original
+ # exception.
+ try:
+ raise TypeError
+ except:
+ pass
+ @app.teardown_request
+ def teardown_request2(exc):
+ self.assert_equal(type(exc), ZeroDivisionError)
+ called.append(True)
+ # This raises a new error and blows away sys.exc_info(), so we can
+ # test that all teardown_requests get passed the same original
+ # exception.
+ try:
+ raise TypeError
+ except:
+ pass
+ @app.route('/')
+ def fails():
+ 1/0
+ rv = app.test_client().get('/')
+ self.assert_equal(rv.status_code, 500)
+ self.assert_('Internal Server Error' in rv.data)
+ self.assert_equal(len(called), 2)
+
+ def test_before_after_request_order(self):
+ called = []
+ app = flask.Flask(__name__)
+ @app.before_request
+ def before1():
+ called.append(1)
+ @app.before_request
+ def before2():
+ called.append(2)
+ @app.after_request
+ def after1(response):
+ called.append(4)
+ return response
+ @app.after_request
+ def after2(response):
+ called.append(3)
+ return response
+ @app.teardown_request
+ def finish1(exc):
+ called.append(6)
+ @app.teardown_request
+ def finish2(exc):
+ called.append(5)
+ @app.route('/')
+ def index():
+ return '42'
+ rv = app.test_client().get('/')
+ self.assert_equal(rv.data, '42')
+ self.assert_equal(called, [1, 2, 3, 4, 5, 6])
+
+ def test_error_handling(self):
+ app = flask.Flask(__name__)
+ @app.errorhandler(404)
+ def not_found(e):
+ return 'not found', 404
+ @app.errorhandler(500)
+ def internal_server_error(e):
+ return 'internal server error', 500
+ @app.route('/')
+ def index():
+ flask.abort(404)
+ @app.route('/error')
+ def error():
+ 1 // 0
+ c = app.test_client()
+ rv = c.get('/')
+ self.assert_equal(rv.status_code, 404)
+ self.assert_equal(rv.data, 'not found')
+ rv = c.get('/error')
+ self.assert_equal(rv.status_code, 500)
+ self.assert_equal('internal server error', rv.data)
+
+ def test_before_request_and_routing_errors(self):
+ app = flask.Flask(__name__)
+ @app.before_request
+ def attach_something():
+ flask.g.something = 'value'
+ @app.errorhandler(404)
+ def return_something(error):
+ return flask.g.something, 404
+ rv = app.test_client().get('/')
+ self.assert_equal(rv.status_code, 404)
+ self.assert_equal(rv.data, 'value')
+
+ def test_user_error_handling(self):
+ class MyException(Exception):
+ pass
+
+ app = flask.Flask(__name__)
+ @app.errorhandler(MyException)
+ def handle_my_exception(e):
+ self.assert_(isinstance(e, MyException))
+ return '42'
+ @app.route('/')
+ def index():
+ raise MyException()
+
+ c = app.test_client()
+ self.assert_equal(c.get('/').data, '42')
+
+ def test_trapping_of_bad_request_key_errors(self):
+ app = flask.Flask(__name__)
+ app.testing = True
+ @app.route('/fail')
+ def fail():
+ flask.request.form['missing_key']
+ c = app.test_client()
+ self.assert_equal(c.get('/fail').status_code, 400)
+
+ app.config['TRAP_BAD_REQUEST_ERRORS'] = True
+ c = app.test_client()
+ try:
+ c.get('/fail')
+ except KeyError, e:
+ self.assert_(isinstance(e, BadRequest))
+ else:
+ self.fail('Expected exception')
+
+ def test_trapping_of_all_http_exceptions(self):
+ app = flask.Flask(__name__)
+ app.testing = True
+ app.config['TRAP_HTTP_EXCEPTIONS'] = True
+ @app.route('/fail')
+ def fail():
+ flask.abort(404)
+
+ c = app.test_client()
+ try:
+ c.get('/fail')
+ except NotFound, e:
+ pass
+ else:
+ self.fail('Expected exception')
+
+ def test_enctype_debug_helper(self):
+ from flask.debughelpers import DebugFilesKeyError
+ app = flask.Flask(__name__)
+ app.debug = True
+ @app.route('/fail', methods=['POST'])
+ def index():
+ return flask.request.files['foo'].filename
+
+ # with statement is important because we leave an exception on the
+ # stack otherwise and we want to ensure that this is not the case
+ # to not negatively affect other tests.
+ with app.test_client() as c:
+ try:
+ c.post('/fail', data={'foo': 'index.txt'})
+ except DebugFilesKeyError, e:
+ self.assert_('no file contents were transmitted' in str(e))
+ self.assert_('This was submitted: "index.txt"' in str(e))
+ else:
+ self.fail('Expected exception')
+
+ def test_teardown_on_pop(self):
+ buffer = []
+ app = flask.Flask(__name__)
+ @app.teardown_request
+ def end_of_request(exception):
+ buffer.append(exception)
+
+ ctx = app.test_request_context()
+ ctx.push()
+ self.assert_equal(buffer, [])
+ ctx.pop()
+ self.assert_equal(buffer, [None])
+
+ def test_response_creation(self):
+ app = flask.Flask(__name__)
+ @app.route('/unicode')
+ def from_unicode():
+ return u'Hällo Wörld'
+ @app.route('/string')
+ def from_string():
+ return u'Hällo Wörld'.encode('utf-8')
+ @app.route('/args')
+ def from_tuple():
+ return 'Meh', 400, {'X-Foo': 'Testing'}, 'text/plain'
+ c = app.test_client()
+ self.assert_equal(c.get('/unicode').data, u'Hällo Wörld'.encode('utf-8'))
+ self.assert_equal(c.get('/string').data, u'Hällo Wörld'.encode('utf-8'))
+ rv = c.get('/args')
+ self.assert_equal(rv.data, 'Meh')
+ self.assert_equal(rv.headers['X-Foo'], 'Testing')
+ self.assert_equal(rv.status_code, 400)
+ self.assert_equal(rv.mimetype, 'text/plain')
+
+ def test_make_response(self):
+ app = flask.Flask(__name__)
+ with app.test_request_context():
+ rv = flask.make_response()
+ self.assert_equal(rv.status_code, 200)
+ self.assert_equal(rv.data, '')
+ self.assert_equal(rv.mimetype, 'text/html')
+
+ rv = flask.make_response('Awesome')
+ self.assert_equal(rv.status_code, 200)
+ self.assert_equal(rv.data, 'Awesome')
+ self.assert_equal(rv.mimetype, 'text/html')
+
+ rv = flask.make_response('W00t', 404)
+ self.assert_equal(rv.status_code, 404)
+ self.assert_equal(rv.data, 'W00t')
+ self.assert_equal(rv.mimetype, 'text/html')
+
+ def test_url_generation(self):
+ app = flask.Flask(__name__)
+ @app.route('/hello/<name>', methods=['POST'])
+ def hello():
+ pass
+ with app.test_request_context():
+ self.assert_equal(flask.url_for('hello', name='test x'), '/hello/test%20x')
+ self.assert_equal(flask.url_for('hello', name='test x', _external=True),
+ 'http://localhost/hello/test%20x')
+
+ def test_custom_converters(self):
+ from werkzeug.routing import BaseConverter
+ class ListConverter(BaseConverter):
+ def to_python(self, value):
+ return value.split(',')
+ def to_url(self, value):
+ base_to_url = super(ListConverter, self).to_url
+ return ','.join(base_to_url(x) for x in value)
+ app = flask.Flask(__name__)
+ app.url_map.converters['list'] = ListConverter
+ @app.route('/<list:args>')
+ def index(args):
+ return '|'.join(args)
+ c = app.test_client()
+ self.assert_equal(c.get('/1,2,3').data, '1|2|3')
+
+ def test_static_files(self):
+ app = flask.Flask(__name__)
+ rv = app.test_client().get('/static/index.html')
+ self.assert_equal(rv.status_code, 200)
+ self.assert_equal(rv.data.strip(), '<h1>Hello World!</h1>')
+ with app.test_request_context():
+ self.assert_equal(flask.url_for('static', filename='index.html'),
+ '/static/index.html')
+
+ def test_none_response(self):
+ app = flask.Flask(__name__)
+ @app.route('/')
+ def test():
+ return None
+ try:
+ app.test_client().get('/')
+ except ValueError, e:
+ self.assert_equal(str(e), 'View function did not return a response')
+ pass
+ else:
+ self.assert_("Expected ValueError")
+
+ def test_request_locals(self):
+ self.assert_equal(repr(flask.g), '<LocalProxy unbound>')
+ self.assertFalse(flask.g)
+
+ def test_proper_test_request_context(self):
+ app = flask.Flask(__name__)
+ app.config.update(
+ SERVER_NAME='localhost.localdomain:5000'
+ )
+
+ @app.route('/')
+ def index():
+ return None
+
+ @app.route('/', subdomain='foo')
+ def sub():
+ return None
+
+ with app.test_request_context('/'):
+ self.assert_equal(flask.url_for('index', _external=True), 'http://localhost.localdomain:5000/')
+
+ with app.test_request_context('/'):
+ self.assert_equal(flask.url_for('sub', _external=True), 'http://foo.localhost.localdomain:5000/')
+
+ try:
+ with app.test_request_context('/', environ_overrides={'HTTP_HOST': 'localhost'}):
+ pass
+ except Exception, e:
+ self.assert_(isinstance(e, ValueError))
+ self.assert_equal(str(e), "the server name provided " +
+ "('localhost.localdomain:5000') does not match the " + \
+ "server name from the WSGI environment ('localhost')")
+
+ try:
+ app.config.update(SERVER_NAME='localhost')
+ with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost'}):
+ pass
+ except ValueError, e:
+ raise ValueError(
+ "No ValueError exception should have been raised \"%s\"" % e
+ )
+
+ try:
+ app.config.update(SERVER_NAME='localhost:80')
+ with app.test_request_context('/', environ_overrides={'SERVER_NAME': 'localhost:80'}):
+ pass
+ except ValueError, e:
+ raise ValueError(
+ "No ValueError exception should have been raised \"%s\"" % e
+ )
+
+ def test_test_app_proper_environ(self):
+ app = flask.Flask(__name__)
+ app.config.update(
+ SERVER_NAME='localhost.localdomain:5000'
+ )
+ @app.route('/')
+ def index():
+ return 'Foo'
+
+ @app.route('/', subdomain='foo')
+ def subdomain():
+ return 'Foo SubDomain'
+
+ try:
+ rv = app.test_client().get('/')
+ self.assert_equal(rv.data, 'Foo')
+ except ValueError, e:
+ raise ValueError(
+ "No ValueError exception should have been raised \"%s\"" % e
+ )
+
+ try:
+ rv = app.test_client().get('/', 'http://localhost.localdomain:5000')
+ self.assert_equal(rv.data, 'Foo')
+ except ValueError, e:
+ raise ValueError(
+ "No ValueError exception should have been raised \"%s\"" % e
+ )
+
+ try:
+ rv = app.test_client().get('/', 'https://localhost.localdomain:5000')
+ self.assert_equal(rv.data, 'Foo')
+ except ValueError, e:
+ raise ValueError(
+ "No ValueError exception should have been raised \"%s\"" % e
+ )
+
+ try:
+ app.config.update(SERVER_NAME='localhost.localdomain')
+ rv = app.test_client().get('/', 'https://localhost.localdomain')
+ self.assert_equal(rv.data, 'Foo')
+ except ValueError, e:
+ raise ValueError(
+ "No ValueError exception should have been raised \"%s\"" % e
+ )
+
+ try:
+ app.config.update(SERVER_NAME='localhost.localdomain:443')
+ rv = app.test_client().get('/', 'https://localhost.localdomain')
+ self.assert_equal(rv.data, 'Foo')
+ except ValueError, e:
+ self.assert_equal(str(e), "the server name provided " +
+ "('localhost.localdomain:443') does not match the " + \
+ "server name from the WSGI environment ('localhost.localdomain')")
+
+ try:
+ app.config.update(SERVER_NAME='localhost.localdomain')
+ app.test_client().get('/', 'http://foo.localhost')
+ except ValueError, e:
+ self.assert_equal(str(e), "the server name provided " + \
+ "('localhost.localdomain') does not match the " + \
+ "server name from the WSGI environment ('foo.localhost')")
+
+ try:
+ rv = app.test_client().get('/', 'http://foo.localhost.localdomain')
+ self.assert_equal(rv.data, 'Foo SubDomain')
+ except ValueError, e:
+ raise ValueError(
+ "No ValueError exception should have been raised \"%s\"" % e
+ )
+
+ def test_exception_propagation(self):
+ def apprunner(configkey):
+ app = flask.Flask(__name__)
+ @app.route('/')
+ def index():
+ 1/0
+ c = app.test_client()
+ if config_key is not None:
+ app.config[config_key] = True
+ try:
+ resp = c.get('/')
+ except Exception:
+ pass
+ else:
+ self.fail('expected exception')
+ else:
+ self.assert_equal(c.get('/').status_code, 500)
+
+ # we have to run this test in an isolated thread because if the
+ # debug flag is set to true and an exception happens the context is
+ # not torn down. This causes other tests that run after this fail
+ # when they expect no exception on the stack.
+ for config_key in 'TESTING', 'PROPAGATE_EXCEPTIONS', 'DEBUG', None:
+ t = Thread(target=apprunner, args=(config_key,))
+ t.start()
+ t.join()
+
+ def test_max_content_length(self):
+ app = flask.Flask(__name__)
+ app.config['MAX_CONTENT_LENGTH'] = 64
+ @app.before_request
+ def always_first():
+ flask.request.form['myfile']
+ self.assert_(False)
+ @app.route('/accept', methods=['POST'])
+ def accept_file():
+ flask.request.form['myfile']
+ self.assert_(False)
+ @app.errorhandler(413)
+ def catcher(error):
+ return '42'
+
+ c = app.test_client()
+ rv = c.post('/accept', data={'myfile': 'foo' * 100})
+ self.assert_equal(rv.data, '42')
+
+ def test_url_processors(self):
+ app = flask.Flask(__name__)
+
+ @app.url_defaults
+ def add_language_code(endpoint, values):
+ if flask.g.lang_code is not None and \
+ app.url_map.is_endpoint_expecting(endpoint, 'lang_code'):
+ values.setdefault('lang_code', flask.g.lang_code)
+
+ @app.url_value_preprocessor
+ def pull_lang_code(endpoint, values):
+ flask.g.lang_code = values.pop('lang_code', None)
+
+ @app.route('/<lang_code>/')
+ def index():
+ return flask.url_for('about')
+
+ @app.route('/<lang_code>/about')
+ def about():
+ return flask.url_for('something_else')
+
+ @app.route('/foo')
+ def something_else():
+ return flask.url_for('about', lang_code='en')
+
+ c = app.test_client()
+
+ self.assert_equal(c.get('/de/').data, '/de/about')
+ self.assert_equal(c.get('/de/about').data, '/foo')
+ self.assert_equal(c.get('/foo').data, '/en/about')
+
+ def test_debug_mode_complains_after_first_request(self):
+ app = flask.Flask(__name__)
+ app.debug = True
+ @app.route('/')
+ def index():
+ return 'Awesome'
+ self.assert_(not app.got_first_request)
+ self.assert_equal(app.test_client().get('/').data, 'Awesome')
+ try:
+ @app.route('/foo')
+ def broken():
+ return 'Meh'
+ except AssertionError, e:
+ self.assert_('A setup function was called' in str(e))
+ else:
+ self.fail('Expected exception')
+
+ app.debug = False
+ @app.route('/foo')
+ def working():
+ return 'Meh'
+ self.assert_equal(app.test_client().get('/foo').data, 'Meh')
+ self.assert_(app.got_first_request)
+
+ def test_before_first_request_functions(self):
+ got = []
+ app = flask.Flask(__name__)
+ @app.before_first_request
+ def foo():
+ got.append(42)
+ c = app.test_client()
+ c.get('/')
+ self.assert_equal(got, [42])
+ c.get('/')
+ self.assert_equal(got, [42])
+ self.assert_(app.got_first_request)
+
+ def test_routing_redirect_debugging(self):
+ app = flask.Flask(__name__)
+ app.debug = True
+ @app.route('/foo/', methods=['GET', 'POST'])
+ def foo():
+ return 'success'
+ with app.test_client() as c:
+ try:
+ c.post('/foo', data={})
+ except AssertionError, e:
+ self.assert_('http://localhost/foo/' in str(e))
+ self.assert_('Make sure to directly send your POST-request '
+ 'to this URL' in str(e))
+ else:
+ self.fail('Expected exception')
+
+ rv = c.get('/foo', data={}, follow_redirects=True)
+ self.assert_equal(rv.data, 'success')
+
+ app.debug = False
+ with app.test_client() as c:
+ rv = c.post('/foo', data={}, follow_redirects=True)
+ self.assert_equal(rv.data, 'success')
+
+ def test_route_decorator_custom_endpoint(self):
+ app = flask.Flask(__name__)
+ app.debug = True
+
+ @app.route('/foo/')
+ def foo():
+ return flask.request.endpoint
+
+ @app.route('/bar/', endpoint='bar')
+ def for_bar():
+ return flask.request.endpoint
+
+ @app.route('/bar/123', endpoint='123')
+ def for_bar_foo():
+ return flask.request.endpoint
+
+ with app.test_request_context():
+ assert flask.url_for('foo') == '/foo/'
+ assert flask.url_for('bar') == '/bar/'
+ assert flask.url_for('123') == '/bar/123'
+
+ c = app.test_client()
+ self.assertEqual(c.get('/foo/').data, 'foo')
+ self.assertEqual(c.get('/bar/').data, 'bar')
+ self.assertEqual(c.get('/bar/123').data, '123')
+
+
+class ContextTestCase(FlaskTestCase):
+
+ def test_context_binding(self):
+ app = flask.Flask(__name__)
+ @app.route('/')
+ def index():
+ return 'Hello %s!' % flask.request.args['name']
+ @app.route('/meh')
+ def meh():
+ return flask.request.url
+
+ with app.test_request_context('/?name=World'):
+ self.assert_equal(index(), 'Hello World!')
+ with app.test_request_context('/meh'):
+ self.assert_equal(meh(), 'http://localhost/meh')
+ self.assert_(flask._request_ctx_stack.top is None)
+
+ def test_context_test(self):
+ app = flask.Flask(__name__)
+ self.assert_(not flask.request)
+ self.assert_(not flask.has_request_context())
+ ctx = app.test_request_context()
+ ctx.push()
+ try:
+ self.assert_(flask.request)
+ self.assert_(flask.has_request_context())
+ finally:
+ ctx.pop()
+
+ def test_manual_context_binding(self):
+ app = flask.Flask(__name__)
+ @app.route('/')
+ def index():
+ return 'Hello %s!' % flask.request.args['name']
+
+ ctx = app.test_request_context('/?name=World')
+ ctx.push()
+ self.assert_equal(index(), 'Hello World!')
+ ctx.pop()
+ try:
+ index()
+ except RuntimeError:
+ pass
+ else:
+ self.assert_(0, 'expected runtime error')
+
+
+class SubdomainTestCase(FlaskTestCase):
+
+ def test_basic_support(self):
+ app = flask.Flask(__name__)
+ app.config['SERVER_NAME'] = 'localhost'
+ @app.route('/')
+ def normal_index():
+ return 'normal index'
+ @app.route('/', subdomain='test')
+ def test_index():
+ return 'test index'
+
+ c = app.test_client()
+ rv = c.get('/', 'http://localhost/')
+ self.assert_equal(rv.data, 'normal index')
+
+ rv = c.get('/', 'http://test.localhost/')
+ self.assert_equal(rv.data, 'test index')