From 671c67aac96ae2cdf9c554ba4e52c167862be6fe Mon Sep 17 00:00:00 2001 From: Mihir Singh Date: Wed, 16 Apr 2014 17:38:41 -0400 Subject: [PATCH 001/131] Append a 'Vary: Cookie' header to the response when the session has been accessed --- flask/sessions.py | 58 ++++++++++++++++++++++++++++++++--------------- 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index 82ba3506..99e3980d 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -51,6 +51,13 @@ class SessionMixin(object): #: The default mixin implementation just hardcodes `True` in. modified = True + #: the accessed variable indicates whether or not the session object has + #: been accessed in that request. This allows flask to append a `Vary: + #: Cookie` header to the response if the session is being accessed. This + #: allows caching proxy servers, like Varnish, to use both the URL and the + #: session cookie as keys when caching pages, preventing multiple users + #: from being served the same cache. + accessed = True class TaggedJSONSerializer(object): """A customized JSON serializer that supports a few extra types that @@ -112,9 +119,18 @@ class SecureCookieSession(CallbackDict, SessionMixin): def __init__(self, initial=None): def on_update(self): self.modified = True + self.accessed = True CallbackDict.__init__(self, initial, on_update) self.modified = False + self.accessed = False + def __getitem__(self, key): + self.accessed = True + return super(SecureCookieSession, self).__getitem__(key) + + def get(self, key, default=None): + self.accessed = True + return super(SecureCookieSession, self).get(key, default) class NullSession(SecureCookieSession): """Class used to generate nicer error messages if sessions are not @@ -334,24 +350,30 @@ class SecureCookieSessionInterface(SessionInterface): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) - # Delete case. If there is no session we bail early. - # If the session was modified to be empty we remove the - # whole cookie. - if not session: - if session.modified: - response.delete_cookie(app.session_cookie_name, - domain=domain, path=path) - return - - # Modification case. There are upsides and downsides to - # emitting a set-cookie header each request. The behavior - # is controlled by the :meth:`should_set_cookie` method - # which performs a quick check to figure out if the cookie - # should be set or not. This is controlled by the - # SESSION_REFRESH_EACH_REQUEST config flag as well as - # the permanent flag on the session itself. - if not self.should_set_cookie(app, session): - return + if session.accessed: + + response.headers.add('Vary', 'Cookie') + + else: + + # Delete case. If there is no session we bail early. + # If the session was modified to be empty we remove the + # whole cookie. + if not session: + if session.modified: + response.delete_cookie(app.session_cookie_name, + domain=domain, path=path) + return + + # Modification case. There are upsides and downsides to + # emitting a set-cookie header each request. The behavior + # is controlled by the :meth:`should_set_cookie` method + # which performs a quick check to figure out if the cookie + # should be set or not. This is controlled by the + # SESSION_REFRESH_EACH_REQUEST config flag as well as + # the permanent flag on the session itself. + if not self.should_set_cookie(app, session): + return httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) From 5935ee495c3e51d26fa2b33de09d532b310cd263 Mon Sep 17 00:00:00 2001 From: Mihir Singh Date: Wed, 16 Apr 2014 17:39:11 -0400 Subject: [PATCH 002/131] Add tests for the `Vary: Cookie` header --- flask/testsuite/basic.py | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index bc0838ad..4ab0bbb3 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -396,6 +396,56 @@ class BasicFunctionalityTestCase(FlaskTestCase): app.config['SESSION_REFRESH_EACH_REQUEST'] = False run_test(expect_header=False) + def test_session_vary_cookie(self): + + app = flask.Flask(__name__) + app.secret_key = 'testkey' + + @app.route('/set-session') + def set_session(): + + flask.session['test'] = 'test' + return '' + + @app.route('/get-session') + def get_session(): + + s = flask.session.get('test') + return '' + + @app.route('/get-session-with-dictionary') + def get_session_with_dictionary(): + + s = flask.session['test'] + return '' + + @app.route('/no-vary-header') + def no_vary_header(): + + return '' + + c = app.test_client() + + rv = c.get('/set-session') + + self.assert_in('Vary', rv.headers) + self.assert_equal('Cookie', rv.headers['Vary']) + + rv = c.get('/get-session') + + self.assert_in('Vary', rv.headers) + self.assert_equal('Cookie', rv.headers['Vary']) + + rv = c.get('/get-session-with-dictionary') + + self.assert_in('Vary', rv.headers) + self.assert_equal('Cookie', rv.headers['Vary']) + + rv = c.get('/no-vary-header') + + self.assert_not_in('Vary', rv.headers) + + def test_flashes(self): app = flask.Flask(__name__) app.secret_key = 'testkey' From f05aeea6dd525d735b5e008aa176b1b839b08cca Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Sat, 25 Oct 2014 15:53:09 -0700 Subject: [PATCH 003/131] Correct the order of suggested syntax for extension imports According to https://github.com/mitsuhiko/flask/issues/1092#issuecomment-47118613 and https://github.com/mitsuhiko/flask/pull/1085#issuecomment-45466907 , the correct order to attempt to import extensions should be flask_foo, then flask.ext.foo, then flaskext_foo. --- docs/extensiondev.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 918187fe..e0c8f5f8 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -410,8 +410,8 @@ deprecated ``flaskext.foo``. Flask 0.8 introduces a redirect import system that lets uses import from ``flask.ext.foo`` and it will try ``flask_foo`` first and if that fails ``flaskext.foo``. -Flask extensions should urge users to import from ``flask.ext.foo`` -instead of ``flask_foo`` or ``flaskext_foo`` so that extensions can +Flask extensions should urge users to import from ``flask_foo`` +instead of ``flask.ext.foo`` or ``flaskext_foo`` so that extensions can transition to the new package name without affecting users. From 063a8da4f8b9237f425ade907ce97339eff52f56 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Sun, 26 Oct 2014 01:09:56 +0200 Subject: [PATCH 004/131] Add changelog for #1218 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 284c95d0..0e72642a 100644 --- a/CHANGES +++ b/CHANGES @@ -66,6 +66,8 @@ Version 1.0 - Don't leak exception info of already catched exceptions to context teardown handlers (pull request ``#1393``). - Allow custom Jinja environment subclasses (pull request ``#1422``). +- Updated extension dev guidelines. + Version 0.10.2 -------------- From 43d6b8a5fc5f4efd9dd1ce8c73a0c15979defd22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sat, 26 Apr 2014 02:01:12 +0200 Subject: [PATCH 005/131] Drop Extension==dev requirement pip doesn't install links included in the description of projects anymore. Therefore ==dev install doesn't work anymore. --- docs/extensiondev.rst | 70 ++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index e0c8f5f8..395227ec 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -355,43 +355,39 @@ new releases. These approved extensions are listed on the `Flask Extension Registry`_ and marked appropriately. If you want your own extension to be approved you have to follow these guidelines: -0. An approved Flask extension requires a maintainer. In the event an - extension author would like to move beyond the project, the project should - find a new maintainer including full source hosting transition and PyPI - access. If no maintainer is available, give access to the Flask core team. -1. An approved Flask extension must provide exactly one package or module - named ``flask_extensionname``. They might also reside inside a - ``flaskext`` namespace packages though this is discouraged now. -2. It must ship a testing suite that can either be invoked with ``make test`` - or ``python setup.py test``. For test suites invoked with ``make - test`` the extension has to ensure that all dependencies for the test - are installed automatically. If tests are invoked with ``python setup.py - test``, test dependencies can be specified in the :file:`setup.py` file. The - test suite also has to be part of the distribution. -3. APIs of approved extensions will be checked for the following - characteristics: - - - an approved extension has to support multiple applications - running in the same Python process. - - it must be possible to use the factory pattern for creating - applications. - -4. The license must be BSD/MIT/WTFPL licensed. -5. The naming scheme for official extensions is *Flask-ExtensionName* or - *ExtensionName-Flask*. -6. Approved extensions must define all their dependencies in the - :file:`setup.py` file unless a dependency cannot be met because it is not - available on PyPI. -7. The extension must have documentation that uses one of the two Flask - themes for Sphinx documentation. -8. The setup.py description (and thus the PyPI description) has to - link to the documentation, website (if there is one) and there - must be a link to automatically install the development version - (``PackageName==dev``). -9. The ``zip_safe`` flag in the setup script must be set to ``False``, - even if the extension would be safe for zipping. -10. An extension currently has to support Python 2.6 as well as - Python 2.7 +0. An approved Flask extension requires a maintainer. In the event an + extension author would like to move beyond the project, the project should + find a new maintainer including full source hosting transition and PyPI + access. If no maintainer is available, give access to the Flask core team. +1. An approved Flask extension must provide exactly one package or module + named ``flask_extensionname``. They might also reside inside a + ``flaskext`` namespace packages though this is discouraged now. +2. It must ship a testing suite that can either be invoked with ``make test`` + or ``python setup.py test``. For test suites invoked with ``make + test`` the extension has to ensure that all dependencies for the test + are installed automatically. If tests are invoked with ``python setup.py + test``, test dependencies can be specified in the `setup.py` file. The + test suite also has to be part of the distribution. +3. APIs of approved extensions will be checked for the following + characteristics: + + - an approved extension has to support multiple applications + running in the same Python process. + - it must be possible to use the factory pattern for creating + applications. + +4. The license must be BSD/MIT/WTFPL licensed. +5. The naming scheme for official extensions is *Flask-ExtensionName* or + *ExtensionName-Flask*. +6. Approved extensions must define all their dependencies in the + `setup.py` file unless a dependency cannot be met because it is not + available on PyPI. +7. The extension must have documentation that uses one of the two Flask + themes for Sphinx documentation. +8. The ``zip_safe`` flag in the setup script must be set to ``False``, + even if the extension would be safe for zipping. +9. An extension currently has to support Python 2.6 as well as + Python 2.7 .. _ext-import-transition: From 3185f445c908a3926f17deb574cb9b1f0b049fe8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sat, 26 Apr 2014 02:04:21 +0200 Subject: [PATCH 006/131] Drop Python 2.6 minimum requirement for extensions Python 2.6 is not supported by python-dev anymore and does not get any security updates. Even though Flask supports 2.6 at the moment, I think it's not necessary for any extensions that are going to be approved in the future to support 2.6. --- docs/extensiondev.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 395227ec..8b18e726 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -386,8 +386,7 @@ extension to be approved you have to follow these guidelines: themes for Sphinx documentation. 8. The ``zip_safe`` flag in the setup script must be set to ``False``, even if the extension would be safe for zipping. -9. An extension currently has to support Python 2.6 as well as - Python 2.7 +9. An extension currently has to support Python 2.7. .. _ext-import-transition: From b12d9762e7fdb4877ea0cb750628149609e3ecb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sat, 26 Apr 2014 02:06:09 +0200 Subject: [PATCH 007/131] Require Python 3.3 and higher for extensions Flask and several extensions already supports Python 3.3 and higher. By requiring approved extensions to support Python 3.3 as well we can quickly achieve better Python 3 adoption and make using Python 3 easier for users. The effort of supporting both Python 2.7 and Python 3.3 is small enough that it shouldn't be a problem to require this from extension authors. --- docs/extensiondev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 8b18e726..5b20528e 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -386,7 +386,7 @@ extension to be approved you have to follow these guidelines: themes for Sphinx documentation. 8. The ``zip_safe`` flag in the setup script must be set to ``False``, even if the extension would be safe for zipping. -9. An extension currently has to support Python 2.7. +9. An extension currently has to support Python 2.7, Python 3.3 and higher. .. _ext-import-transition: From b9938d0182abac79289963b2df507756bf86a50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sun, 27 Apr 2014 00:26:39 +0200 Subject: [PATCH 008/131] Don't allow namespace packages anymore --- docs/extensiondev.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 5b20528e..000892e6 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -360,8 +360,7 @@ extension to be approved you have to follow these guidelines: find a new maintainer including full source hosting transition and PyPI access. If no maintainer is available, give access to the Flask core team. 1. An approved Flask extension must provide exactly one package or module - named ``flask_extensionname``. They might also reside inside a - ``flaskext`` namespace packages though this is discouraged now. + named ``flask_extensionname``. 2. It must ship a testing suite that can either be invoked with ``make test`` or ``python setup.py test``. For test suites invoked with ``make test`` the extension has to ensure that all dependencies for the test From 2b58e6120ceadf363d12cc12889bfc79c18d5066 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Neuha=CC=88user?= Date: Sun, 27 Apr 2014 00:27:37 +0200 Subject: [PATCH 009/131] Fix wording --- docs/extensiondev.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 000892e6..e8028ffc 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -375,7 +375,7 @@ extension to be approved you have to follow these guidelines: - it must be possible to use the factory pattern for creating applications. -4. The license must be BSD/MIT/WTFPL licensed. +4. The extension must be BSD/MIT/WTFPL licensed. 5. The naming scheme for official extensions is *Flask-ExtensionName* or *ExtensionName-Flask*. 6. Approved extensions must define all their dependencies in the From f80ea4fe5d3f194c567d3fd279e1e84050a12b10 Mon Sep 17 00:00:00 2001 From: Jeff Widman Date: Sat, 25 Oct 2014 18:56:10 -0700 Subject: [PATCH 010/131] Some grammar and typo fixes --- docs/extensiondev.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index e8028ffc..2f8aae39 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -48,7 +48,7 @@ that people can easily install the development version into their virtualenv without having to download the library by hand. Flask extensions must be licensed under a BSD, MIT or more liberal license -to be able to be enlisted in the Flask Extension Registry. Keep in mind +in order to be listed in the Flask Extension Registry. Keep in mind that the Flask Extension Registry is a moderated place and libraries will be reviewed upfront if they behave as required. @@ -154,10 +154,10 @@ What to use depends on what you have in mind. For the SQLite 3 extension we will use the class-based approach because it will provide users with an object that handles opening and closing database connections. -What's important about classes is that they encourage to be shared around -on module level. In that case, the object itself must not under any +When designing your classes, it's important to make them easily reusable +at the module level. This means the object itself must not under any circumstances store any application specific state and must be shareable -between different application. +between different applications. The Extension Code ------------------ @@ -334,10 +334,10 @@ development. If you want to learn more, it's a very good idea to check out existing extensions on the `Flask Extension Registry`_. If you feel lost there is still the `mailinglist`_ and the `IRC channel`_ to get some ideas for nice looking APIs. Especially if you do something nobody before -you did, it might be a very good idea to get some more input. This not -only to get an idea about what people might want to have from an -extension, but also to avoid having multiple developers working on pretty -much the same side by side. +you did, it might be a very good idea to get some more input. This not only +generates useful feedback on what people might want from an extension, but +also avoids having multiple developers working in isolation on pretty much the +same problem. Remember: good API design is hard, so introduce your project on the mailinglist, and let other developers give you a helping hand with From 011b129b6bc615c7a24de626dd63b6a311fa6ce6 Mon Sep 17 00:00:00 2001 From: Jimmy McCarthy Date: Mon, 8 Jun 2015 16:31:35 -0500 Subject: [PATCH 011/131] Add kwarg to disable auto OPTIONS on add_url_rule Adds support for a kwarg `provide_automatic_options` on `add_url_rule`, which lets you turn off the automatic OPTIONS response on a per-URL basis even if your view functions are functions, not classes (so you can't provide attrs on them). --- flask/app.py | 6 ++++-- tests/test_basic.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index f0a8b69b..5b186577 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1013,8 +1013,10 @@ class Flask(_PackageBoundObject): # starting with Flask 0.8 the view_func object can disable and # force-enable the automatic options handling. - provide_automatic_options = getattr(view_func, - 'provide_automatic_options', None) + provide_automatic_options = options.pop( + 'provide_automatic_options', getattr(view_func, + 'provide_automatic_options', None)) + if provide_automatic_options is None: if 'OPTIONS' not in methods: diff --git a/tests/test_basic.py b/tests/test_basic.py index 695e346e..08e03aca 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1604,3 +1604,44 @@ def test_run_server_port(monkeypatch): hostname, port = 'localhost', 8000 app.run(hostname, port, debug=True) assert rv['result'] == 'running on %s:%s ...' % (hostname, port) + + +def test_disable_automatic_options(): + # Issue 1488: Add support for a kwarg to add_url_rule to disable the auto OPTIONS response + app = flask.Flask(__name__) + + def index(): + return flask.request.method + + def more(): + return flask.request.method + + app.add_url_rule('/', 'index', index, provide_automatic_options=False) + app.add_url_rule('/more', 'more', more, methods=['GET', 'POST'], provide_automatic_options=False) + + c = app.test_client() + assert c.get('/').data == b'GET' + rv = c.post('/') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD'] + # Older versions of Werkzeug.test.Client don't have an options method + if hasattr(c, 'options'): + rv = c.options('/') + else: + rv = c.open('/', method='OPTIONS') + assert rv.status_code == 405 + + rv = c.head('/') + assert rv.status_code == 200 + assert not rv.data # head truncates + assert c.post('/more').data == b'POST' + assert c.get('/more').data == b'GET' + rv = c.delete('/more') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + # Older versions of Werkzeug.test.Client don't have an options method + if hasattr(c, 'options'): + rv = c.options('/more') + else: + rv = c.open('/more', method='OPTIONS') + assert rv.status_code == 405 From a1360196447c8dad1b20fc611bd96210c0bfca28 Mon Sep 17 00:00:00 2001 From: Jimmy McCarthy Date: Tue, 7 Jul 2015 13:21:51 -0500 Subject: [PATCH 012/131] Updated changelog --- CHANGES | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGES b/CHANGES index b33c3795..d1529526 100644 --- a/CHANGES +++ b/CHANGES @@ -70,6 +70,9 @@ Version 1.0 - ``flask.g`` now has ``pop()`` and ``setdefault`` methods. - Turn on autoescape for ``flask.templating.render_template_string`` by default (pull request ``#1515``). +- Added support for `provide_automatic_options` in `**kwargs` on + :meth:`add_url_rule` to turn off automatic OPTIONS when the `view_func` + argument is not a class (pull request ``#1489``). Version 0.10.2 -------------- From 5505827a4bd759a24696258983ec14c768358af8 Mon Sep 17 00:00:00 2001 From: Jimmy McCarthy Date: Mon, 14 Sep 2015 13:25:52 -0500 Subject: [PATCH 013/131] provide_automatic_options as explicit arg In add_url_rule, break provide_automatic_options out to an explicit kwarg, and add notes to the docstring. --- flask/app.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/flask/app.py b/flask/app.py index e03ff221..de147e9e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -944,7 +944,8 @@ class Flask(_PackageBoundObject): return iter(self._blueprint_order) @setupmethod - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + def add_url_rule(self, rule, endpoint=None, view_func=None, + provide_automatic_options=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the endpoint. @@ -984,6 +985,10 @@ class Flask(_PackageBoundObject): endpoint :param view_func: the function to call when serving a request to the provided endpoint + :param provide_automatic_options: controls whether ``OPTIONS`` should + be provided automatically. If this + is not set, will check attributes on + the view or list of methods. :param options: the options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object. A change to Werkzeug is handling of method options. methods @@ -1013,10 +1018,9 @@ class Flask(_PackageBoundObject): # starting with Flask 0.8 the view_func object can disable and # force-enable the automatic options handling. - provide_automatic_options = options.pop( - 'provide_automatic_options', getattr(view_func, - 'provide_automatic_options', None)) - + if provide_automatic_options is None: + provide_automatic_options = getattr(view_func, + 'provide_automatic_options', None) if provide_automatic_options is None: if 'OPTIONS' not in methods: From a23ce4697155ae236c139c96c6d9dd65207a0cb6 Mon Sep 17 00:00:00 2001 From: Jimmy McCarthy Date: Mon, 14 Sep 2015 13:29:16 -0500 Subject: [PATCH 014/131] Update change log --- CHANGES | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGES b/CHANGES index cac78f73..b459fc1d 100644 --- a/CHANGES +++ b/CHANGES @@ -70,9 +70,9 @@ Version 1.0 - ``flask.g`` now has ``pop()`` and ``setdefault`` methods. - Turn on autoescape for ``flask.templating.render_template_string`` by default (pull request ``#1515``). -- Added support for `provide_automatic_options` in `**kwargs` on - :meth:`add_url_rule` to turn off automatic OPTIONS when the `view_func` - argument is not a class (pull request ``#1489``). +- Added support for `provide_automatic_options` in :meth:`add_url_rule` to + turn off automatic OPTIONS when the `view_func` argument is not a class + (pull request ``#1489``). Version 0.10.2 -------------- From 826d7475cdab43b85d79f99e8eae5dabb01baf7b Mon Sep 17 00:00:00 2001 From: Aviv Cohn Date: Tue, 5 Jan 2016 01:10:35 +0200 Subject: [PATCH 015/131] Clarified the docstring in method Flask.preprocess_request. The doc now clearly states that the method invokes two set of hook functions, and how these are filtered before execution. --- flask/app.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/flask/app.py b/flask/app.py index 91139773..33b543dd 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1791,16 +1791,18 @@ class Flask(_PackageBoundObject): raise error def preprocess_request(self): - """Called before the actual request dispatching and will - call each :meth:`before_request` decorated function, passing no - arguments. - If any of these functions returns a value, it's handled as - if it was the return value from the view and further - request handling is stopped. + """Called before the request dispatching. - This also triggers the :meth:`url_value_processor` functions before - the actual :meth:`before_request` functions are called. + Triggers two set of hook functions that should be invoked prior to request dispatching: + :attr:`url_value_preprocessors` and :attr:`before_request_funcs` + (the latter are functions decorated with :meth:`before_request` decorator). + In both cases, the method triggers only the functions that are either global + or registered to the current blueprint. + + If any function in :attr:`before_request_funcs` returns a value, it's handled as if it was + the return value from the view function, and further request handling is stopped. """ + bp = _request_ctx_stack.top.request.blueprint funcs = self.url_value_preprocessors.get(None, ()) From 954b7ef7bbc261ac455feb122b56e897653de826 Mon Sep 17 00:00:00 2001 From: Randy Liou Date: Fri, 3 Jun 2016 15:50:38 -0700 Subject: [PATCH 016/131] Enhance code coverage for Blueprint.endpoint Add basic test for the endpoint decorator for the Blueprint object. --- tests/test_blueprints.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index a3309037..de293e7f 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -355,6 +355,25 @@ def test_route_decorator_custom_endpoint_with_dots(): rv = c.get('/py/bar/123') assert rv.status_code == 404 + +def test_endpoint_decorator(): + from werkzeug.routing import Rule + app = flask.Flask(__name__) + app.url_map.add(Rule('/foo', endpoint='bar')) + + bp = flask.Blueprint('bp', __name__) + + @bp.endpoint('bar') + def foobar(): + return flask.request.endpoint + + app.register_blueprint(bp, url_prefix='/bp_prefix') + + c = app.test_client() + assert c.get('/foo').data == b'bar' + assert c.get('/bp_prefix/bar').status_code == 404 + + def test_template_filter(): bp = flask.Blueprint('bp', __name__) @bp.app_template_filter() From 501b8590dd8b262c93d52c92ab4b743af1f5d317 Mon Sep 17 00:00:00 2001 From: RamiC Date: Wed, 8 Jun 2016 12:03:26 +0300 Subject: [PATCH 017/131] Allow per blueprint json encoder decoder re #1710 --- flask/blueprints.py | 7 +++++++ flask/json.py | 6 ++++-- tests/test_helpers.py | 39 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/flask/blueprints.py b/flask/blueprints.py index 586a1b0b..62675204 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -89,6 +89,13 @@ class Blueprint(_PackageBoundObject): warn_on_modifications = False _got_registered_once = False + #: Blueprint local JSON decoder class to use. + # Set to None to use the :class:`~flask.app.Flask.json_encoder`. + json_encoder = None + #: Blueprint local JSON decoder class to use. + # Set to None to use the :class:`~flask.app.Flask.json_decoder`. + json_decoder = None + def __init__(self, name, import_name, static_folder=None, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, diff --git a/flask/json.py b/flask/json.py index b9ce4a08..ad0e7bdf 100644 --- a/flask/json.py +++ b/flask/json.py @@ -94,7 +94,8 @@ class JSONDecoder(_json.JSONDecoder): def _dump_arg_defaults(kwargs): """Inject default arguments for dump functions.""" if current_app: - kwargs.setdefault('cls', current_app.json_encoder) + bp = current_app.blueprints.get(request.blueprint, None) + kwargs.setdefault('cls', bp.json_encoder if bp else current_app.json_encoder) if not current_app.config['JSON_AS_ASCII']: kwargs.setdefault('ensure_ascii', False) kwargs.setdefault('sort_keys', current_app.config['JSON_SORT_KEYS']) @@ -106,7 +107,8 @@ def _dump_arg_defaults(kwargs): def _load_arg_defaults(kwargs): """Inject default arguments for load functions.""" if current_app: - kwargs.setdefault('cls', current_app.json_decoder) + bp = current_app.blueprints.get(request.blueprint, None) + kwargs.setdefault('cls', bp.json_decoder if bp else current_app.json_decoder) else: kwargs.setdefault('cls', JSONDecoder) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 3ff5900b..e893004a 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -90,12 +90,12 @@ class TestJSON(object): app = flask.Flask(__name__) app.config['JSON_AS_ASCII'] = True - with app.app_context(): + with app.test_request_context(): rv = flask.json.dumps(u'\N{SNOWMAN}') assert rv == '"\\u2603"' app.config['JSON_AS_ASCII'] = False - with app.app_context(): + with app.test_request_context(): rv = flask.json.dumps(u'\N{SNOWMAN}') assert rv == u'"\u2603"' @@ -234,6 +234,41 @@ class TestJSON(object): }), content_type='application/json') assert rv.data == b'"<42>"' + def test_blueprint_json_customization(self): + class X(object): + def __init__(self, val): + self.val = val + class MyEncoder(flask.json.JSONEncoder): + def default(self, o): + if isinstance(o, X): + return '<%d>' % o.val + return flask.json.JSONEncoder.default(self, o) + class MyDecoder(flask.json.JSONDecoder): + def __init__(self, *args, **kwargs): + kwargs.setdefault('object_hook', self.object_hook) + flask.json.JSONDecoder.__init__(self, *args, **kwargs) + def object_hook(self, obj): + if len(obj) == 1 and '_foo' in obj: + return X(obj['_foo']) + return obj + + blue = flask.Blueprint('blue', __name__) + blue.json_encoder = MyEncoder + blue.json_decoder = MyDecoder + @blue.route('/bp', methods=['POST']) + def index(): + return flask.json.dumps(flask.request.get_json()['x']) + + app = flask.Flask(__name__) + app.testing = True + app.register_blueprint(blue) + + c = app.test_client() + rv = c.post('/bp', data=flask.json.dumps({ + 'x': {'_foo': 42} + }), content_type='application/json') + assert rv.data == b'"<42>"' + def test_modified_url_encoding(self): class ModifiedRequest(flask.Request): url_charset = 'euc-kr' From 4305ebdf6692956a7e21a7195b90505d92f559d8 Mon Sep 17 00:00:00 2001 From: RamiC Date: Wed, 8 Jun 2016 12:50:43 +0300 Subject: [PATCH 018/131] Check for a request ctx before using the request. Use the app json coder when blueprint json coder is set to none. Revert the failling test to using an app_context re #1710 --- flask/json.py | 15 +++++++++++---- tests/test_helpers.py | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/flask/json.py b/flask/json.py index ad0e7bdf..fd39e539 100644 --- a/flask/json.py +++ b/flask/json.py @@ -13,6 +13,7 @@ import uuid from datetime import date from .globals import current_app, request from ._compat import text_type, PY2 +from .ctx import has_request_context from werkzeug.http import http_date from jinja2 import Markup @@ -94,8 +95,11 @@ class JSONDecoder(_json.JSONDecoder): def _dump_arg_defaults(kwargs): """Inject default arguments for dump functions.""" if current_app: - bp = current_app.blueprints.get(request.blueprint, None) - kwargs.setdefault('cls', bp.json_encoder if bp else current_app.json_encoder) + bp = current_app.blueprints.get(request.blueprint, + None) if has_request_context() else None + kwargs.setdefault('cls', + bp.json_encoder if bp and bp.json_encoder + else current_app.json_encoder) if not current_app.config['JSON_AS_ASCII']: kwargs.setdefault('ensure_ascii', False) kwargs.setdefault('sort_keys', current_app.config['JSON_SORT_KEYS']) @@ -107,8 +111,11 @@ def _dump_arg_defaults(kwargs): def _load_arg_defaults(kwargs): """Inject default arguments for load functions.""" if current_app: - bp = current_app.blueprints.get(request.blueprint, None) - kwargs.setdefault('cls', bp.json_decoder if bp else current_app.json_decoder) + bp = current_app.blueprints.get(request.blueprint, + None) if has_request_context() else None + kwargs.setdefault('cls', + bp.json_decoder if bp and bp.json_decoder + else current_app.json_decoder) else: kwargs.setdefault('cls', JSONDecoder) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index e893004a..142f555f 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -90,12 +90,12 @@ class TestJSON(object): app = flask.Flask(__name__) app.config['JSON_AS_ASCII'] = True - with app.test_request_context(): + with app.app_context(): rv = flask.json.dumps(u'\N{SNOWMAN}') assert rv == '"\\u2603"' app.config['JSON_AS_ASCII'] = False - with app.test_request_context(): + with app.app_context(): rv = flask.json.dumps(u'\N{SNOWMAN}') assert rv == u'"\u2603"' From 5cadd4a348b4d42e22ddb2bfad93f74a4f34cc59 Mon Sep 17 00:00:00 2001 From: Anton Sarukhanov Date: Wed, 8 Jun 2016 08:26:01 -0400 Subject: [PATCH 019/131] Added make target for test coverage, documented make commands --- .gitignore | 2 ++ CONTRIBUTING.rst | 25 ++++++++++++++++--------- Makefile | 5 +++++ test-requirements.txt | 2 ++ 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index 9bf4f063..e754af19 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ _mailinglist .tox .cache/ .idea/ +.coverage +htmlcov diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index ca7b4af2..f4d1ef9c 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -31,18 +31,12 @@ Submitting patches - Try to follow `PEP8 `_, but you may ignore the line-length-limit if following it would make the code uglier. - -Running the testsuite ---------------------- +Getting Started +=============== You probably want to set up a `virtualenv `_. -The minimal requirement for running the testsuite is ``py.test``. You can -install it with:: - - pip install pytest - Clone this repository:: git clone https://github.com/pallets/flask.git @@ -52,11 +46,21 @@ Install Flask as an editable package using the current source:: cd flask pip install --editable . +Running the testsuite +--------------------- + +The minimal requirement for running the testsuite is ``pytest``. You can +install it with:: + + pip install pytest + Then you can run the testsuite with:: py.test -With only py.test installed, a large part of the testsuite will get skipped +**Shortcut**: ``make test`` will ensure ``pytest`` is installed, and run it. + +With only pytest installed, a large part of the testsuite will get skipped though. Whether this is relevant depends on which part of Flask you're working on. Travis is set up to run the full testsuite when you submit your pull request anyways. @@ -69,6 +73,8 @@ of ``pytest``. You can install it with:: The ``tox`` command will then run all tests against multiple combinations Python versions and dependency versions. +**Shortcut**: ``make tox-test`` will ensure ``tox`` is installed, and run it. + Running test coverage --------------------- Generating a report of lines that do not have unit test coverage can indicate where @@ -87,3 +93,4 @@ Generate a HTML report can be done using this command:: Full docs on ``coverage.py`` are here: https://coverage.readthedocs.io +**Shortcut**: ``make cov`` will ensure ``pytest-cov`` is installed, run it, display the results, *and* save the HTML report. diff --git a/Makefile b/Makefile index 9bcdebc2..3fcb765d 100644 --- a/Makefile +++ b/Makefile @@ -7,8 +7,13 @@ test: FLASK_DEBUG= py.test tests examples tox-test: + pip install -r test-requirements.txt -q tox +cov: + pip install -r test-requirements.txt -q + FLASK_DEBUG= py.test --cov-report term --cov-report html --cov=flask --cov=examples tests examples + audit: python setup.py audit diff --git a/test-requirements.txt b/test-requirements.txt index e079f8a6..8be82dd0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1 +1,3 @@ +tox pytest +pytest-cov From 16471795117bb2d200888e49af5b621377bc1436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20Gy=C3=B6rgy?= Date: Sun, 26 Apr 2015 09:40:20 +0200 Subject: [PATCH 020/131] Added routes command, which shows all the endpoints registered for the app. Orderable by rules, endpoints and methods. Shows up in the builtin command list. --- flask/cli.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/flask/cli.py b/flask/cli.py index 3796c083..2565657f 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -304,6 +304,7 @@ class FlaskGroup(AppGroup): if add_default_commands: self.add_command(run_command) self.add_command(shell_command) + self.add_command(routes_command) self._loaded_plugin_commands = False @@ -461,6 +462,33 @@ def shell_command(): code.interact(banner=banner, local=ctx) +@click.command('routes', short_help='Show routes for the app.') +@click.option('-r', 'order_by', flag_value='rule', default=True, help='Order by route') +@click.option('-e', 'order_by', flag_value='endpoint', help='Order by endpoint') +@click.option('-m', 'order_by', flag_value='methods', help='Order by methods') +@with_appcontext +def routes_command(order_by): + """Show all routes with endpoints and methods.""" + from flask.globals import _app_ctx_stack + app = _app_ctx_stack.top.app + + order_key = lambda rule: getattr(rule, order_by) + sorted_rules = sorted(app.url_map.iter_rules(), key=order_key) + + max_rule = max(len(rule.rule) for rule in sorted_rules) + max_ep = max(len(rule.endpoint) for rule in sorted_rules) + max_meth = max(len(', '.join(rule.methods)) for rule in sorted_rules) + + columnformat = '{:<%s} {:<%s} {:<%s}' % (max_rule, max_ep, max_meth) + click.echo(columnformat.format('Route', 'Endpoint', 'Methods')) + under_count = max_rule + max_ep + max_meth + 4 + click.echo('-' * under_count) + + for rule in sorted_rules: + methods = ', '.join(rule.methods) + click.echo(columnformat.format(rule.rule, rule.endpoint, methods)) + + cli = FlaskGroup(help="""\ This shell command acts as general utility script for Flask applications. From b8e826c16bbbada9128b70357c218ef79425499d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20Gy=C3=B6rgy?= Date: Sat, 25 Jun 2016 13:17:33 +0200 Subject: [PATCH 021/131] Added tests, fixed some minor alignment problems. --- flask/cli.py | 3 +++ tests/test_apps/cliapp/routesapp.py | 18 +++++++++++++ tests/test_cli.py | 39 ++++++++++++++++++++++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 tests/test_apps/cliapp/routesapp.py diff --git a/flask/cli.py b/flask/cli.py index 2565657f..e4992598 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -476,8 +476,11 @@ def routes_command(order_by): sorted_rules = sorted(app.url_map.iter_rules(), key=order_key) max_rule = max(len(rule.rule) for rule in sorted_rules) + max_rule = max(max_rule, len('Route')) max_ep = max(len(rule.endpoint) for rule in sorted_rules) + max_ep = max(max_ep, len('Endpoint')) max_meth = max(len(', '.join(rule.methods)) for rule in sorted_rules) + max_meth = max(max_meth, len('Methods')) columnformat = '{:<%s} {:<%s} {:<%s}' % (max_rule, max_ep, max_meth) click.echo(columnformat.format('Route', 'Endpoint', 'Methods')) diff --git a/tests/test_apps/cliapp/routesapp.py b/tests/test_apps/cliapp/routesapp.py new file mode 100644 index 00000000..84060546 --- /dev/null +++ b/tests/test_apps/cliapp/routesapp.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import, print_function + +from flask import Flask + + +noroute_app = Flask('noroute app') +simpleroute_app = Flask('simpleroute app') +only_POST_route_app = Flask('GET route app') + + +@simpleroute_app.route('/simpleroute') +def simple(): + pass + + +@only_POST_route_app.route('/only-post', methods=['POST']) +def only_post(): + pass diff --git a/tests/test_cli.py b/tests/test_cli.py index 4a3d0831..7df00167 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -20,7 +20,7 @@ import pytest from click.testing import CliRunner from flask import Flask, current_app -from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \ +from flask.cli import cli, AppGroup, FlaskGroup, NoAppException, ScriptInfo, \ find_best_app, locate_app, with_appcontext, prepare_exec_for_file, \ find_default_import_path @@ -170,3 +170,40 @@ def test_flaskgroup(): result = runner.invoke(cli, ['test']) assert result.exit_code == 0 assert result.output == 'flaskgroup\n' + + +class TestRoutes: + def test_no_route(self, monkeypatch): + monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:noroute_app') + runner = CliRunner() + result = runner.invoke(cli, ['routes'], catch_exceptions=False) + assert result.exit_code == 0 + assert result.output == """\ +Route Endpoint Methods +----------------------------------------------------- +/static/ static HEAD, OPTIONS, GET +""" + + def test_simple_route(self, monkeypatch): + monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:simpleroute_app') + runner = CliRunner() + result = runner.invoke(cli, ['routes'], catch_exceptions=False) + assert result.exit_code == 0 + assert result.output == """\ +Route Endpoint Methods +----------------------------------------------------- +/simpleroute simple HEAD, OPTIONS, GET +/static/ static HEAD, OPTIONS, GET +""" + + def test_only_POST_route(self, monkeypatch): + monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:only_POST_route_app') + runner = CliRunner() + result = runner.invoke(cli, ['routes'], catch_exceptions=False) + assert result.exit_code == 0 + assert result.output == """\ +Route Endpoint Methods +------------------------------------------------------ +/only-post only_post POST, OPTIONS +/static/ static HEAD, OPTIONS, GET +""" From 1b764cff93696a75c6248b6f956426cbefa097bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kiss=20Gy=C3=B6rgy?= Date: Sat, 25 Jun 2016 13:24:43 +0200 Subject: [PATCH 022/131] Added runner fixture --- tests/test_cli.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/test_cli.py b/tests/test_cli.py index 7df00167..db82ae8e 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -25,6 +25,11 @@ from flask.cli import cli, AppGroup, FlaskGroup, NoAppException, ScriptInfo, \ find_default_import_path +@pytest.fixture +def runner(): + return CliRunner() + + def test_cli_name(test_apps): """Make sure the CLI object's name is the app's name and not the app itself""" from cliapp.app import testapp @@ -108,7 +113,7 @@ def test_scriptinfo(test_apps): assert obj.load_app() == app -def test_with_appcontext(): +def test_with_appcontext(runner): """Test of with_appcontext.""" @click.command() @with_appcontext @@ -117,13 +122,12 @@ def test_with_appcontext(): obj = ScriptInfo(create_app=lambda info: Flask("testapp")) - runner = CliRunner() result = runner.invoke(testcmd, obj=obj) assert result.exit_code == 0 assert result.output == 'testapp\n' -def test_appgroup(): +def test_appgroup(runner): """Test of with_appcontext.""" @click.group(cls=AppGroup) def cli(): @@ -143,7 +147,6 @@ def test_appgroup(): obj = ScriptInfo(create_app=lambda info: Flask("testappgroup")) - runner = CliRunner() result = runner.invoke(cli, ['test'], obj=obj) assert result.exit_code == 0 assert result.output == 'testappgroup\n' @@ -153,7 +156,7 @@ def test_appgroup(): assert result.output == 'testappgroup\n' -def test_flaskgroup(): +def test_flaskgroup(runner): """Test FlaskGroup.""" def create_app(info): return Flask("flaskgroup") @@ -166,16 +169,14 @@ def test_flaskgroup(): def test(): click.echo(current_app.name) - runner = CliRunner() result = runner.invoke(cli, ['test']) assert result.exit_code == 0 assert result.output == 'flaskgroup\n' class TestRoutes: - def test_no_route(self, monkeypatch): + def test_no_route(self, runner, monkeypatch): monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:noroute_app') - runner = CliRunner() result = runner.invoke(cli, ['routes'], catch_exceptions=False) assert result.exit_code == 0 assert result.output == """\ @@ -184,9 +185,8 @@ Route Endpoint Methods /static/ static HEAD, OPTIONS, GET """ - def test_simple_route(self, monkeypatch): + def test_simple_route(self, runner, monkeypatch): monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:simpleroute_app') - runner = CliRunner() result = runner.invoke(cli, ['routes'], catch_exceptions=False) assert result.exit_code == 0 assert result.output == """\ @@ -196,9 +196,8 @@ Route Endpoint Methods /static/ static HEAD, OPTIONS, GET """ - def test_only_POST_route(self, monkeypatch): + def test_only_POST_route(self, runner, monkeypatch): monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:only_POST_route_app') - runner = CliRunner() result = runner.invoke(cli, ['routes'], catch_exceptions=False) assert result.exit_code == 0 assert result.output == """\ From 516ce59f95a3b5d2fffbcde8abfdf1951e748361 Mon Sep 17 00:00:00 2001 From: Antoine Catton Date: Tue, 28 Jun 2016 17:20:25 +0200 Subject: [PATCH 023/131] Add the ability to combine MethodViews --- flask/views.py | 6 +++++- tests/test_views.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/flask/views.py b/flask/views.py index 6e249180..922bb132 100644 --- a/flask/views.py +++ b/flask/views.py @@ -102,12 +102,16 @@ class View(object): return view +def get_methods(cls): + return getattr(cls, 'methods', []) or [] + + class MethodViewType(type): def __new__(cls, name, bases, d): rv = type.__new__(cls, name, bases, d) if 'methods' not in d: - methods = set(rv.methods or []) + methods = set(m for b in bases for m in get_methods(b)) for key in d: if key in http_method_funcs: methods.add(key.upper()) diff --git a/tests/test_views.py b/tests/test_views.py index 8a66bd53..65981dbd 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -160,3 +160,45 @@ def test_endpoint_override(): # But these tests should still pass. We just log a warning. common_test(app) + +def test_multiple_inheritance(): + app = flask.Flask(__name__) + + class GetView(flask.views.MethodView): + def get(self): + return 'GET' + + class DeleteView(flask.views.MethodView): + def delete(self): + return 'DELETE' + + class GetDeleteView(GetView, DeleteView): + pass + + app.add_url_rule('/', view_func=GetDeleteView.as_view('index')) + + c = app.test_client() + assert c.get('/').data == b'GET' + assert c.delete('/').data == b'DELETE' + assert sorted(GetDeleteView.methods) == ['DELETE', 'GET'] + +def test_remove_method_from_parent(): + app = flask.Flask(__name__) + + class GetView(flask.views.MethodView): + def get(self): + return 'GET' + + class OtherView(flask.views.MethodView): + def post(self): + return 'POST' + + class View(GetView, OtherView): + methods = ['GET'] + + app.add_url_rule('/', view_func=View.as_view('index')) + + c = app.test_client() + assert c.get('/').data == b'GET' + assert c.post('/').status_code == 405 + assert sorted(View.methods) == ['GET'] From 0c459762ea15960886fd0c2960382212c280eee3 Mon Sep 17 00:00:00 2001 From: John Still Date: Sat, 2 Jul 2016 17:03:36 -0500 Subject: [PATCH 024/131] clarify blueprint 404 error handling in docs --- docs/blueprints.rst | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/blueprints.rst b/docs/blueprints.rst index 89d3701e..98a3d630 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -177,11 +177,11 @@ the `template_folder` parameter to the :class:`Blueprint` constructor:: admin = Blueprint('admin', __name__, template_folder='templates') For static files, the path can be absolute or relative to the blueprint -resource folder. +resource folder. -The template folder is added to the search path of templates but with a lower -priority than the actual application's template folder. That way you can -easily override templates that a blueprint provides in the actual application. +The template folder is added to the search path of templates but with a lower +priority than the actual application's template folder. That way you can +easily override templates that a blueprint provides in the actual application. This also means that if you don't want a blueprint template to be accidentally overridden, make sure that no other blueprint or actual application template has the same relative path. When multiple blueprints provide the same relative @@ -194,7 +194,7 @@ want to render the template ``'admin/index.html'`` and you have provided this: :file:`yourapplication/admin/templates/admin/index.html`. The reason for the extra ``admin`` folder is to avoid getting our template overridden by a template named ``index.html`` in the actual application template -folder. +folder. To further reiterate this: if you have a blueprint named ``admin`` and you want to render a template called :file:`index.html` which is specific to this @@ -245,4 +245,22 @@ Here is an example for a "404 Page Not Found" exception:: def page_not_found(e): return render_template('pages/404.html') +Most errorhandlers will simply work as expected; however, there is a caveat +concerning handlers for 404 and 405 exceptions. These errorhandlers are only +invoked from an appropriate ``raise`` statement or a call to ``abort`` in another +of the blueprint's view functions; they are not invoked by, e.g., an invalid URL +access. This is because the blueprint does not "own" a certain URL space, so +the application instance has no way of knowing which blueprint errorhandler it +should run if given an invalid URL. If you would like to execute different +handling strategies for these errors based on URL prefixes, they may be defined +at the application level using the ``request`` proxy object:: + + @app.errorhandler(404) + @app.errorhandler(405) + def _handle_api_error(ex): + if request.path.startswith('/api/'): + return jsonify_error(ex) + else: + return ex + More information on error handling see :ref:`errorpages`. From 55f9af72e35449718d405cc140c4705c12239ff2 Mon Sep 17 00:00:00 2001 From: Ioan Vancea Date: Thu, 28 Jul 2016 16:34:48 +0200 Subject: [PATCH 025/131] Added a missing module to import statement --- docs/patterns/deferredcallbacks.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/deferredcallbacks.rst b/docs/patterns/deferredcallbacks.rst index 886ae40a..a79dc913 100644 --- a/docs/patterns/deferredcallbacks.rst +++ b/docs/patterns/deferredcallbacks.rst @@ -60,7 +60,7 @@ At any time during a request, we can register a function to be called at the end of the request. For example you can remember the current language of the user in a cookie in the before-request function:: - from flask import request + from flask import request, after_this_request @app.before_request def detect_user_language(): From 2b03eca1b791c1f88a46b42c83b62214eb0f04c2 Mon Sep 17 00:00:00 2001 From: Geoffrey Bauduin Date: Tue, 11 Oct 2016 15:27:48 +0200 Subject: [PATCH 026/131] Updated Celery pattern The given pattern caused Celery to lose the current Context --- docs/patterns/celery.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst index 673d953b..548da29b 100644 --- a/docs/patterns/celery.rst +++ b/docs/patterns/celery.rst @@ -44,7 +44,7 @@ This is all that is necessary to properly integrate Celery with Flask:: abstract = True def __call__(self, *args, **kwargs): with app.app_context(): - return TaskBase.__call__(self, *args, **kwargs) + return self.run(*args, **kwargs) celery.Task = ContextTask return celery From 08d6de73a893615ef316a9ad88710bd7e6589bf2 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 3 Nov 2016 23:01:10 -0700 Subject: [PATCH 027/131] switch to packaged sphinx theme --- .gitmodules | 3 -- docs/_themes | 1 - docs/conf.py | 14 +++----- docs/flaskext.py | 86 ------------------------------------------------ 4 files changed, 4 insertions(+), 100 deletions(-) delete mode 100644 .gitmodules delete mode 160000 docs/_themes delete mode 100644 docs/flaskext.py diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 3d7df149..00000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "docs/_themes"] - path = docs/_themes - url = https://github.com/mitsuhiko/flask-sphinx-themes.git diff --git a/docs/_themes b/docs/_themes deleted file mode 160000 index 3d964b66..00000000 --- a/docs/_themes +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3d964b660442e23faedf801caed6e3c7bd42d5c9 diff --git a/docs/conf.py b/docs/conf.py index b37427a8..111a9089 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -19,7 +19,6 @@ import pkg_resources # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.join(os.path.dirname(__file__), '_themes')) sys.path.append(os.path.dirname(__file__)) # -- General configuration ----------------------------------------------------- @@ -110,7 +109,7 @@ exclude_patterns = ['_build'] # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -html_theme_path = ['_themes'] +# html_theme_path = ['_themes'] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". @@ -263,19 +262,14 @@ intersphinx_mapping = { } try: - __import__('flask_theme_support') - pygments_style = 'flask_theme_support.FlaskyStyle' + __import__('flask_sphinx_themes') html_theme = 'flask' html_theme_options = { 'touch_icon': 'touch-icon.png' } except ImportError: - print('-' * 74) - print('Warning: Flask themes unavailable. Building with default theme') - print('If you want the Flask themes, run this command and build again:') - print() - print(' git submodule update --init') - print('-' * 74) + print('Flask theme unavailable; using the default theme.') + print('Install using `pip install Flask-Sphinx-Themes`.') # unwrap decorators diff --git a/docs/flaskext.py b/docs/flaskext.py deleted file mode 100644 index 33f47449..00000000 --- a/docs/flaskext.py +++ /dev/null @@ -1,86 +0,0 @@ -# flasky extensions. flasky pygments style based on tango style -from pygments.style import Style -from pygments.token import Keyword, Name, Comment, String, Error, \ - Number, Operator, Generic, Whitespace, Punctuation, Other, Literal - - -class FlaskyStyle(Style): - background_color = "#f8f8f8" - default_style = "" - - styles = { - # No corresponding class for the following: - #Text: "", # class: '' - Whitespace: "underline #f8f8f8", # class: 'w' - Error: "#a40000 border:#ef2929", # class: 'err' - Other: "#000000", # class 'x' - - Comment: "italic #8f5902", # class: 'c' - Comment.Preproc: "noitalic", # class: 'cp' - - Keyword: "bold #004461", # class: 'k' - Keyword.Constant: "bold #004461", # class: 'kc' - Keyword.Declaration: "bold #004461", # class: 'kd' - Keyword.Namespace: "bold #004461", # class: 'kn' - Keyword.Pseudo: "bold #004461", # class: 'kp' - Keyword.Reserved: "bold #004461", # class: 'kr' - Keyword.Type: "bold #004461", # class: 'kt' - - Operator: "#582800", # class: 'o' - Operator.Word: "bold #004461", # class: 'ow' - like keywords - - Punctuation: "bold #000000", # class: 'p' - - # because special names such as Name.Class, Name.Function, etc. - # are not recognized as such later in the parsing, we choose them - # to look the same as ordinary variables. - Name: "#000000", # class: 'n' - Name.Attribute: "#c4a000", # class: 'na' - to be revised - Name.Builtin: "#004461", # class: 'nb' - Name.Builtin.Pseudo: "#3465a4", # class: 'bp' - Name.Class: "#000000", # class: 'nc' - to be revised - Name.Constant: "#000000", # class: 'no' - to be revised - Name.Decorator: "#888", # class: 'nd' - to be revised - Name.Entity: "#ce5c00", # class: 'ni' - Name.Exception: "bold #cc0000", # class: 'ne' - Name.Function: "#000000", # class: 'nf' - Name.Property: "#000000", # class: 'py' - Name.Label: "#f57900", # class: 'nl' - Name.Namespace: "#000000", # class: 'nn' - to be revised - Name.Other: "#000000", # class: 'nx' - Name.Tag: "bold #004461", # class: 'nt' - like a keyword - Name.Variable: "#000000", # class: 'nv' - to be revised - Name.Variable.Class: "#000000", # class: 'vc' - to be revised - Name.Variable.Global: "#000000", # class: 'vg' - to be revised - Name.Variable.Instance: "#000000", # class: 'vi' - to be revised - - Number: "#990000", # class: 'm' - - Literal: "#000000", # class: 'l' - Literal.Date: "#000000", # class: 'ld' - - String: "#4e9a06", # class: 's' - String.Backtick: "#4e9a06", # class: 'sb' - String.Char: "#4e9a06", # class: 'sc' - String.Doc: "italic #8f5902", # class: 'sd' - like a comment - String.Double: "#4e9a06", # class: 's2' - String.Escape: "#4e9a06", # class: 'se' - String.Heredoc: "#4e9a06", # class: 'sh' - String.Interpol: "#4e9a06", # class: 'si' - String.Other: "#4e9a06", # class: 'sx' - String.Regex: "#4e9a06", # class: 'sr' - String.Single: "#4e9a06", # class: 's1' - String.Symbol: "#4e9a06", # class: 'ss' - - Generic: "#000000", # class: 'g' - Generic.Deleted: "#a40000", # class: 'gd' - Generic.Emph: "italic #000000", # class: 'ge' - Generic.Error: "#ef2929", # class: 'gr' - Generic.Heading: "bold #000080", # class: 'gh' - Generic.Inserted: "#00A000", # class: 'gi' - Generic.Output: "#888", # class: 'go' - Generic.Prompt: "#745334", # class: 'gp' - Generic.Strong: "bold #000000", # class: 'gs' - Generic.Subheading: "bold #800080", # class: 'gu' - Generic.Traceback: "bold #a40000", # class: 'gt' - } From af11098057b022c172c1142f02e50cc3c4b065b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sergio=20Di=CC=81az=20Sa=CC=81nchez?= Date: Tue, 28 Feb 2017 00:13:45 +0100 Subject: [PATCH 028/131] Updated documentation for being able to use init_db method --- docs/testing.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 0737936e..f4ee64ec 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -33,7 +33,7 @@ In order to test the application, we add a second module (:file:`flaskr_tests.py`) and create a unittest skeleton there:: import os - import flaskr + from flaskr import flaskr import unittest import tempfile @@ -208,7 +208,7 @@ temporarily. With this you can access the :class:`~flask.request`, functions. Here is a full example that demonstrates this approach:: import flask - + app = flask.Flask(__name__) with app.test_request_context('/?name=Peter'): From 2995366dde63b8acd1f246bcd5a4cf7d61f0c1fa Mon Sep 17 00:00:00 2001 From: Larivact Date: Fri, 17 Mar 2017 05:41:20 +0100 Subject: [PATCH 029/131] Clarify APPLICATION_ROOT #1714 --- docs/config.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/config.rst b/docs/config.rst index 75ce239a..714b54c8 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -132,13 +132,13 @@ The following configuration values are used internally by Flask: by default enables URL generation without a request context but with an application context. -``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``. +``APPLICATION_ROOT`` The path value used for the session + cookie if ``SESSION_COOKIE_PATH`` isn't + set. If it's also ``None`` ``'/'`` is used. + Note that to actually serve your Flask + app under a subpath you need to tell + your WSGI container the ``SCRIPT_NAME`` + WSGI environment variable. ``MAX_CONTENT_LENGTH`` If set to a value in bytes, Flask will reject incoming requests with a content length greater than this by From 889c0ed1964f48557e532f95f9003c5ee6e2b727 Mon Sep 17 00:00:00 2001 From: Runar Trollet Kristoffersen Date: Sun, 19 Mar 2017 18:01:23 +0100 Subject: [PATCH 030/131] Issue #2212: documentation: virtualenv and python3 --- docs/installation.rst | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 96c363f5..38094ded 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -40,6 +40,12 @@ installations of Python, one for each project. It doesn't actually install separate copies of Python, but it does provide a clever way to keep different project environments isolated. Let's see how virtualenv works. + +.. admonition:: A note on python3 and virtualenv + + If you are planning on using python3 with the virtualenv, you don't need to + install ``virtualenv``. Python3 has built-in support for virtual environments. + If you are on Mac OS X or Linux, chances are that the following command will work for you:: @@ -55,24 +61,43 @@ install it first. Check the :ref:`windows-easy-install` section for more information about how to do that. Once you have it installed, run the same commands as above, but without the ``sudo`` prefix. +Creating a virtual environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Once you have virtualenv installed, just fire up a shell and create -your own environment. I usually create a project folder and a :file:`venv` +your own environment. I usually create a project folder and a :file:`virtenv` folder within:: $ mkdir myproject $ cd myproject - $ virtualenv venv - New python executable in venv/bin/python + +There is a little change in how you create a virtualenv depending on which python-version you are currently using. + +**Python2** + +:: + + $ virtualenv virtenv + New python executable in virtenv/bin/python Installing setuptools, pip............done. +**Python 3.6 and above** + +:: + + $ python3 -m venv virtenv + +Activating a virtual environment +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + Now, whenever you want to work on a project, you only have to activate the corresponding environment. On OS X and Linux, do the following:: - $ . venv/bin/activate + $ . virtenv/bin/activate If you are a Windows user, the following command is for you:: - $ venv\Scripts\activate + $ virtenv\Scripts\activate Either way, you should now be using your virtualenv (notice how the prompt of your shell has changed to show the active environment). @@ -115,10 +140,10 @@ Get the git checkout in a new virtualenv and run in development mode:: $ git clone https://github.com/pallets/flask.git Initialized empty Git repository in ~/dev/flask/.git/ $ cd flask - $ virtualenv venv - New python executable in venv/bin/python + $ virtualenv virtenv + New python executable in virtenv/bin/python Installing setuptools, pip............done. - $ . venv/bin/activate + $ . virtenv/bin/activate $ python setup.py develop ... Finished processing dependencies for Flask From 0049922f2e690a6d58f335ca9196c95f1de84370 Mon Sep 17 00:00:00 2001 From: Antonio Larrosa Date: Thu, 23 Mar 2017 17:30:48 +0100 Subject: [PATCH 031/131] Fix send_file to work with non-ascii filenames This commit implements https://tools.ietf.org/html/rfc2231#section-4 in order to support sending unicode characters. Tested on both Firefox and Chromium under Linux. This adds unidecode as a dependency, which might be relaxed by using .encode('latin-1', 'ignore') but wouldn't be as useful. Also, added a test for the correct headers to be added. Previously, using a filename parameter to send_file with unicode characters, it failed with the next error since HTTP headers don't allow non latin-1 characters. Error on request: Traceback (most recent call last): File "/usr/lib/python3.6/site-packages/werkzeug/serving.py", line 193, in run_wsgi execute(self.server.app) File "/usr/lib/python3.6/site-packages/werkzeug/serving.py", line 186, in execute write(b'') File "/usr/lib/python3.6/site-packages/werkzeug/serving.py", line 152, in write self.send_header(key, value) File "/usr/lib64/python3.6/http/server.py", line 509, in send_header ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) UnicodeEncodeError: 'latin-1' codec can't encode character '\uff0f' in position 58: ordinal not in range(256) Fixes #1286 --- flask/helpers.py | 6 +++++- setup.py | 1 + tests/test_helpers.py | 11 +++++++++++ tox.ini | 1 + 4 files changed, 18 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 2f446327..b2ea2ce9 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -41,6 +41,7 @@ from .signals import message_flashed from .globals import session, _request_ctx_stack, _app_ctx_stack, \ current_app, request from ._compat import string_types, text_type +from unidecode import unidecode # sentinel @@ -534,8 +535,11 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if attachment_filename is None: raise TypeError('filename unavailable, required for ' 'sending as attachment') + filename_dict = { + 'filename': unidecode(attachment_filename), + 'filename*': "UTF-8''%s" % url_quote(attachment_filename)} headers.add('Content-Disposition', 'attachment', - filename=attachment_filename) + **filename_dict) if current_app.use_x_sendfile and filename: if file is not None: diff --git a/setup.py b/setup.py index 08995073..2ffe72ed 100644 --- a/setup.py +++ b/setup.py @@ -75,6 +75,7 @@ setup( 'Jinja2>=2.4', 'itsdangerous>=0.21', 'click>=2.0', + 'unidecode', ], classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tests/test_helpers.py b/tests/test_helpers.py index fd448fb8..362b6cfa 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -560,6 +560,17 @@ class TestSendfile(object): assert options['filename'] == 'index.txt' rv.close() + def test_attachment_with_utf8_filename(self): + app = flask.Flask(__name__) + with app.test_request_context(): + with open(os.path.join(app.root_path, 'static/index.html')) as f: + rv = flask.send_file(f, as_attachment=True, + attachment_filename='Ñandú/pingüino.txt') + value, options = \ + parse_options_header(rv.headers['Content-Disposition']) + assert options == {'filename': 'Nandu/pinguino.txt', 'filename*': "UTF-8''%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt"} + rv.close() + def test_static_file(self): app = flask.Flask(__name__) # default cache timeout is 12 hours diff --git a/tox.ini b/tox.ini index 764b4030..3db7b91f 100644 --- a/tox.ini +++ b/tox.ini @@ -27,6 +27,7 @@ deps= devel: git+https://github.com/pallets/itsdangerous.git devel: git+https://github.com/jek/blinker.git simplejson: simplejson + unidecode [testenv:docs] deps = sphinx From 6ef45f30ab0e95d80ee2a29168f098e9037b9e0b Mon Sep 17 00:00:00 2001 From: Antonio Larrosa Date: Fri, 24 Mar 2017 20:05:01 +0100 Subject: [PATCH 032/131] Fix previous commits to work with python 2 and python 3 Also, parse_options_header seems to interpret filename* so we better test the actual value used in the headers (and since it's valid in any order, use a set to compare) --- flask/helpers.py | 2 +- tests/test_helpers.py | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index b2ea2ce9..e4fb8c43 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -536,7 +536,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, raise TypeError('filename unavailable, required for ' 'sending as attachment') filename_dict = { - 'filename': unidecode(attachment_filename), + 'filename': unidecode(text_type(attachment_filename)), 'filename*': "UTF-8''%s" % url_quote(attachment_filename)} headers.add('Content-Disposition', 'attachment', **filename_dict) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 362b6cfa..f7affb2c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -565,10 +565,11 @@ class TestSendfile(object): with app.test_request_context(): with open(os.path.join(app.root_path, 'static/index.html')) as f: rv = flask.send_file(f, as_attachment=True, - attachment_filename='Ñandú/pingüino.txt') - value, options = \ - parse_options_header(rv.headers['Content-Disposition']) - assert options == {'filename': 'Nandu/pinguino.txt', 'filename*': "UTF-8''%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt"} + attachment_filename=u'Ñandú/pingüino.txt') + content_disposition = set(rv.headers['Content-Disposition'].split(';')) + assert content_disposition == set(['attachment', + ' filename="Nandu/pinguino.txt"', + " filename*=UTF-8''%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt"]) rv.close() def test_static_file(self): From bf023e7dc0bae78ff0ab14dc9f10a87e2b56f676 Mon Sep 17 00:00:00 2001 From: Antonio Larrosa Date: Thu, 30 Mar 2017 17:32:21 +0200 Subject: [PATCH 033/131] Remove unidecode dependency and use unicodedata instead I found a way to remove the unidecode dependency without sacrificing much by using unicodedata.normalize . --- flask/helpers.py | 6 ++++-- setup.py | 1 - tox.ini | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index e4fb8c43..aa8be315 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -14,6 +14,7 @@ import sys import pkgutil import posixpath import mimetypes +import unicodedata from time import time from zlib import adler32 from threading import RLock @@ -41,7 +42,6 @@ from .signals import message_flashed from .globals import session, _request_ctx_stack, _app_ctx_stack, \ current_app, request from ._compat import string_types, text_type -from unidecode import unidecode # sentinel @@ -536,7 +536,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, raise TypeError('filename unavailable, required for ' 'sending as attachment') filename_dict = { - 'filename': unidecode(text_type(attachment_filename)), + 'filename': (unicodedata.normalize('NFKD', + text_type(attachment_filename)).encode('ascii', + 'ignore')), 'filename*': "UTF-8''%s" % url_quote(attachment_filename)} headers.add('Content-Disposition', 'attachment', **filename_dict) diff --git a/setup.py b/setup.py index 2ffe72ed..08995073 100644 --- a/setup.py +++ b/setup.py @@ -75,7 +75,6 @@ setup( 'Jinja2>=2.4', 'itsdangerous>=0.21', 'click>=2.0', - 'unidecode', ], classifiers=[ 'Development Status :: 4 - Beta', diff --git a/tox.ini b/tox.ini index 3db7b91f..764b4030 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,6 @@ deps= devel: git+https://github.com/pallets/itsdangerous.git devel: git+https://github.com/jek/blinker.git simplejson: simplejson - unidecode [testenv:docs] deps = sphinx From ae1ac2053bcf0c77de32fc7915e36f8ca2f5c961 Mon Sep 17 00:00:00 2001 From: Adam Geitgey Date: Tue, 4 Apr 2017 13:26:40 -0700 Subject: [PATCH 034/131] Correct imports in file upload example (#2230) The example code uses `flash` but doesn't import it. So the code as written doesn't work. This simply adds `flash` to the list of imports in the sample code. --- docs/patterns/fileuploads.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 3a42d325..1c4b0d36 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -21,7 +21,7 @@ specific upload folder and displays a file to the user. Let's look at the bootstrapping code for our application:: import os - from flask import Flask, request, redirect, url_for + from flask import Flask, flash, request, redirect, url_for from werkzeug.utils import secure_filename UPLOAD_FOLDER = '/path/to/the/uploads' From d76d68cd381df4a8c3e2e636c98b5bfa02ab37e4 Mon Sep 17 00:00:00 2001 From: asilversempirical Date: Thu, 6 Apr 2017 11:26:01 -0400 Subject: [PATCH 035/131] Update out of date jsonify documentation https://github.com/pallets/flask/pull/2193 changed the conditions for when jsonify pretty prints, but this comment wasn't updated. --- flask/json.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/flask/json.py b/flask/json.py index 0ba9d717..825dcdf1 100644 --- a/flask/json.py +++ b/flask/json.py @@ -236,11 +236,10 @@ def jsonify(*args, **kwargs): Added support for serializing top-level arrays. This introduces a security risk in ancient browsers. See :ref:`json-security` for details. - This function's response will be pretty printed if it was not requested - with ``X-Requested-With: XMLHttpRequest`` to simplify debugging unless - the ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to false. - Compressed (not pretty) formatting currently means no indents and no - spaces after separators. + This function's response will be pretty printed if the + ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to True or the + Flask app is running in debug mode. Compressed (not pretty) formatting + currently means no indents and no spaces after separators. .. versionadded:: 0.2 """ From ec18fe94775aaa3d54238c6c75b569491803134e Mon Sep 17 00:00:00 2001 From: Grey Li Date: Fri, 7 Apr 2017 22:10:43 +0800 Subject: [PATCH 036/131] Add example for virtualenv integration in cli docs (#2234) --- docs/cli.rst | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/cli.rst b/docs/cli.rst index 2ca0e83e..d0b033f6 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -56,6 +56,18 @@ If you are constantly working with a virtualenv you can also put the bottom of the file. That way every time you activate your virtualenv you automatically also activate the correct application name. +Edit the activate script for the shell you use. For example: + +Unix Bash: ``venv/bin/activate``:: + + FLASK_APP=hello + export FLASK_APP + +Windows CMD.exe: ``venv\Scripts\activate.bat``:: + + set "FLASK_APP=hello" + :END + Debug Flag ---------- From 00d6e339ec789e7b92007297e840a671a5e38a7b Mon Sep 17 00:00:00 2001 From: jab Date: Fri, 7 Apr 2017 10:31:54 -0400 Subject: [PATCH 037/131] Change Flask.__init__ to accept two new keyword arguments, host_matching and static_host. (#1560) This enables host_matching to be set properly by the time the constructor adds the static route, and enables the static route to be properly associated with the required host. Previously, you could only enable host_matching once your app was already instantiated (e.g. app.url_map.host_matching = True), but at that point the constructor would have already added the static route without host matching and an associated host, leaving the static route in a broken state. Fixes #1559. --- AUTHORS | 1 + CHANGES | 5 +++++ flask/app.py | 28 +++++++++++++++++++++------- tests/test_basic.py | 19 +++++++++++++++++++ 4 files changed, 46 insertions(+), 7 deletions(-) diff --git a/AUTHORS b/AUTHORS index cc157dc4..33210243 100644 --- a/AUTHORS +++ b/AUTHORS @@ -21,6 +21,7 @@ Patches and Suggestions - Florent Xicluna - Georg Brandl - Jeff Widman @jeffwidman +- Joshua Bronson @jab - Justin Quick - Kenneth Reitz - Keyan Pishdadian diff --git a/CHANGES b/CHANGES index 9500ee17..440cb260 100644 --- a/CHANGES +++ b/CHANGES @@ -14,6 +14,11 @@ Major release, unreleased - Change default configuration `JSONIFY_PRETTYPRINT_REGULAR=False`. jsonify() method returns compressed response by default, and pretty response in debug mode. +- Change Flask.__init__ to accept two new keyword arguments, ``host_matching`` + and ``static_host``. This enables ``host_matching`` to be set properly by the + time the constructor adds the static route, and enables the static route to + be properly associated with the required host. (``#1559``) + Version 0.12.1 -------------- diff --git a/flask/app.py b/flask/app.py index 6617b02b..87621aee 100644 --- a/flask/app.py +++ b/flask/app.py @@ -123,6 +123,9 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.11 The `root_path` parameter was added. + .. versionadded:: 0.13 + The `host_matching` and `static_host` parameters were added. + :param import_name: the name of the application package :param static_url_path: can be used to specify a different path for the static files on the web. Defaults to the name @@ -130,6 +133,13 @@ class Flask(_PackageBoundObject): :param static_folder: the folder with static files that should be served at `static_url_path`. Defaults to the ``'static'`` folder in the root path of the application. + folder in the root path of the application. Defaults + to None. + :param host_matching: sets the app's ``url_map.host_matching`` to the given + given value. Defaults to False. + :param static_host: the host to use when adding the static route. Defaults + to None. Required when using ``host_matching=True`` + with a ``static_folder`` configured. :param template_folder: the folder that contains the templates that should be used by the application. Defaults to ``'templates'`` folder in the root path of the @@ -337,7 +347,8 @@ class Flask(_PackageBoundObject): session_interface = SecureCookieSessionInterface() def __init__(self, import_name, static_path=None, static_url_path=None, - static_folder='static', template_folder='templates', + static_folder='static', static_host=None, + host_matching=False, template_folder='templates', instance_path=None, instance_relative_config=False, root_path=None): _PackageBoundObject.__init__(self, import_name, @@ -525,19 +536,22 @@ class Flask(_PackageBoundObject): #: app.url_map.converters['list'] = ListConverter self.url_map = Map() + self.url_map.host_matching = host_matching + # tracks internally if the application already handled at least one # request. self._got_first_request = False self._before_request_lock = Lock() - # register the static folder for the application. Do that even - # if the folder does not exist. First of all it might be created - # while the server is running (usually happens during development) - # but also because google appengine stores static files somewhere - # else when mapped with the .yml file. + # Add a static route using the provided static_url_path, static_host, + # and static_folder iff there is a configured static_folder. + # Note we do this without checking if static_folder exists. + # For one, it might be created while the server is running (e.g. during + # development). Also, Google App Engine stores static files somewhere if self.has_static_folder: + assert bool(static_host) == host_matching, 'Invalid static_host/host_matching combination' self.add_url_rule(self.static_url_path + '/', - endpoint='static', + endpoint='static', host=static_host, view_func=self.send_static_file) #: The click command line context for this application. Commands diff --git a/tests/test_basic.py b/tests/test_basic.py index ffc12dc1..76efd6e2 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1188,6 +1188,25 @@ def test_static_url_path(): assert flask.url_for('static', filename='index.html') == '/foo/index.html' +def test_static_route_with_host_matching(): + app = flask.Flask(__name__, host_matching=True, static_host='example.com') + c = app.test_client() + rv = c.get('http://example.com/static/index.html') + assert rv.status_code == 200 + rv.close() + with app.test_request_context(): + rv = flask.url_for('static', filename='index.html', _external=True) + assert rv == 'http://example.com/static/index.html' + # Providing static_host without host_matching=True should error. + with pytest.raises(Exception): + flask.Flask(__name__, static_host='example.com') + # Providing host_matching=True with static_folder but without static_host should error. + with pytest.raises(Exception): + flask.Flask(__name__, host_matching=True) + # Providing host_matching=True without static_host but with static_folder=None should not error. + flask.Flask(__name__, host_matching=True, static_folder=None) + + def test_none_response(): app = flask.Flask(__name__) app.testing = True From d50a5db5ed07a14733b8cbfc0962b793209014dd Mon Sep 17 00:00:00 2001 From: Antonio Larrosa Date: Fri, 7 Apr 2017 20:34:52 +0200 Subject: [PATCH 038/131] Keep using only filename if it's valid ascii --- flask/helpers.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index aa8be315..19d33420 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -14,7 +14,7 @@ import sys import pkgutil import posixpath import mimetypes -import unicodedata +from unicodedata import normalize from time import time from zlib import adler32 from threading import RLock @@ -535,13 +535,22 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if attachment_filename is None: raise TypeError('filename unavailable, required for ' 'sending as attachment') - filename_dict = { - 'filename': (unicodedata.normalize('NFKD', - text_type(attachment_filename)).encode('ascii', - 'ignore')), - 'filename*': "UTF-8''%s" % url_quote(attachment_filename)} + normalized = normalize('NFKD', text_type(attachment_filename)) + + try: + normalized.encode('ascii') + except UnicodeEncodeError: + filenames = { + 'filename': normalized.encode('ascii', 'ignore'), + 'filename*': "UTF-8''%s" % url_quote(attachment_filename), + } + else: + filenames = { + 'filename': attachment_filename, + } + headers.add('Content-Disposition', 'attachment', - **filename_dict) + **filenames) if current_app.use_x_sendfile and filename: if file is not None: From c1973016eacfb22fdec837394d8b89c0ca35ad15 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 7 Apr 2017 18:02:31 -0700 Subject: [PATCH 039/131] style cleanup break out header parts in test test for no filename* parameter for ascii header --- flask/helpers.py | 19 ++++++++++++------- tests/test_helpers.py | 25 ++++++++++++++++--------- 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 19d33420..420467ad 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -14,10 +14,10 @@ import sys import pkgutil import posixpath import mimetypes -from unicodedata import normalize from time import time from zlib import adler32 from threading import RLock +import unicodedata from werkzeug.routing import BuildError from functools import update_wrapper @@ -478,6 +478,11 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. versionchanged:: 0.12 The `attachment_filename` is preferred over `filename` for MIME-type detection. + + .. versionchanged:: 0.13 + UTF-8 filenames, as specified in `RFC 2231`_, are supported. + + .. _RFC 2231: https://tools.ietf.org/html/rfc2231#section-4 :param filename_or_fp: the filename of the file to send in `latin-1`. This is relative to the :attr:`~Flask.root_path` @@ -535,7 +540,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, if attachment_filename is None: raise TypeError('filename unavailable, required for ' 'sending as attachment') - normalized = normalize('NFKD', text_type(attachment_filename)) + + normalized = unicodedata.normalize( + 'NFKD', text_type(attachment_filename) + ) try: normalized.encode('ascii') @@ -545,12 +553,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, 'filename*': "UTF-8''%s" % url_quote(attachment_filename), } else: - filenames = { - 'filename': attachment_filename, - } + filenames = {'filename': attachment_filename} - headers.add('Content-Disposition', 'attachment', - **filenames) + headers.add('Content-Disposition', 'attachment', **filenames) if current_app.use_x_sendfile and filename: if file is not None: diff --git a/tests/test_helpers.py b/tests/test_helpers.py index f7affb2c..1aeff065 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -540,10 +540,11 @@ class TestSendfile(object): value, options = \ parse_options_header(rv.headers['Content-Disposition']) assert value == 'attachment' + assert options['filename'] == 'index.html' + assert 'filename*' not in options rv.close() with app.test_request_context(): - assert options['filename'] == 'index.html' rv = flask.send_file('static/index.html', as_attachment=True) value, options = parse_options_header(rv.headers['Content-Disposition']) assert value == 'attachment' @@ -562,15 +563,21 @@ class TestSendfile(object): def test_attachment_with_utf8_filename(self): app = flask.Flask(__name__) + with app.test_request_context(): - with open(os.path.join(app.root_path, 'static/index.html')) as f: - rv = flask.send_file(f, as_attachment=True, - attachment_filename=u'Ñandú/pingüino.txt') - content_disposition = set(rv.headers['Content-Disposition'].split(';')) - assert content_disposition == set(['attachment', - ' filename="Nandu/pinguino.txt"', - " filename*=UTF-8''%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt"]) - rv.close() + rv = flask.send_file( + 'static/index.html', as_attachment=True, + attachment_filename=u'Ñandú/pingüino.txt' + ) + value, options = parse_options_header( + rv.headers['Content-Disposition'] + ) + rv.close() + + assert value == 'attachment' + assert sorted(options.keys()) == ('filename', 'filename*') + assert options['filename'] == 'Nandu/pinguino.txt' + assert options['filename*'] == 'UTF-8''%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt' def test_static_file(self): app = flask.Flask(__name__) From f790ab7177c9959b560d238f4be9c124d635e70a Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 8 Apr 2017 10:33:06 -0700 Subject: [PATCH 040/131] need to test against raw header parsing prefers the last value parsed for the option --- tests/test_helpers.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 1aeff065..d93b443e 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -541,7 +541,7 @@ class TestSendfile(object): parse_options_header(rv.headers['Content-Disposition']) assert value == 'attachment' assert options['filename'] == 'index.html' - assert 'filename*' not in options + assert 'filename*' not in rv.headers['Content-Disposition'] rv.close() with app.test_request_context(): @@ -565,20 +565,15 @@ class TestSendfile(object): app = flask.Flask(__name__) with app.test_request_context(): - rv = flask.send_file( - 'static/index.html', as_attachment=True, - attachment_filename=u'Ñandú/pingüino.txt' - ) - value, options = parse_options_header( - rv.headers['Content-Disposition'] - ) + rv = flask.send_file('static/index.html', as_attachment=True, attachment_filename=u'Ñandú/pingüino.txt') + content_disposition = set(rv.headers['Content-Disposition'].split('; ')) + assert content_disposition == set(( + 'attachment', + 'filename="Nandu/pinguino.txt"', + "filename*=UTF-8''%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt" + )) rv.close() - assert value == 'attachment' - assert sorted(options.keys()) == ('filename', 'filename*') - assert options['filename'] == 'Nandu/pinguino.txt' - assert options['filename*'] == 'UTF-8''%C3%91and%C3%BA%EF%BC%8Fping%C3%BCino.txt' - def test_static_file(self): app = flask.Flask(__name__) # default cache timeout is 12 hours From aafb80c527eb5d6d361d7ba08258376d13679820 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 8 Apr 2017 11:08:08 -0700 Subject: [PATCH 041/131] add changelog for #2223 --- CHANGES | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGES b/CHANGES index 440cb260..1f687409 100644 --- a/CHANGES +++ b/CHANGES @@ -18,7 +18,9 @@ Major release, unreleased and ``static_host``. This enables ``host_matching`` to be set properly by the time the constructor adds the static route, and enables the static route to be properly associated with the required host. (``#1559``) +- ``send_file`` supports Unicode in ``attachment_filename``. (`#2223`_) +.. _#2223: https://github.com/pallets/flask/pull/2223 Version 0.12.1 -------------- From e13eaeeaf2a3502815bcf04a86227dae87ad9128 Mon Sep 17 00:00:00 2001 From: ka7 Date: Tue, 11 Apr 2017 21:44:32 +0200 Subject: [PATCH 042/131] Fix typo in docs (#2237) --- docs/patterns/packages.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 1bb84f8c..cc149839 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -65,7 +65,7 @@ that tells Flask where to find the application instance:: export FLASK_APP=yourapplication If you are outside of the project directory make sure to provide the exact -path to your application directory. Similiarly you can turn on "debug +path to your application directory. Similarly you can turn on "debug mode" with this environment variable:: export FLASK_DEBUG=true From 09b49104f39f111b58d60a819551cca4bde305cc Mon Sep 17 00:00:00 2001 From: David Lord Date: Wed, 12 Apr 2017 09:18:07 -0700 Subject: [PATCH 043/131] filename can be latin-1, not just ascii only normalize basic name when utf-8 header is needed ref #2223 --- flask/helpers.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 420467ad..bfdcf224 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -484,7 +484,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. _RFC 2231: https://tools.ietf.org/html/rfc2231#section-4 - :param filename_or_fp: the filename of the file to send in `latin-1`. + :param filename_or_fp: the filename of the file to send. This is relative to the :attr:`~Flask.root_path` if a relative path is specified. Alternatively a file object might be provided in @@ -541,15 +541,12 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, raise TypeError('filename unavailable, required for ' 'sending as attachment') - normalized = unicodedata.normalize( - 'NFKD', text_type(attachment_filename) - ) - try: - normalized.encode('ascii') + attachment_filename = attachment_filename.encode('latin-1') except UnicodeEncodeError: filenames = { - 'filename': normalized.encode('ascii', 'ignore'), + 'filename': unicodedata.normalize( + 'NFKD', attachment_filename).encode('latin-1', 'ignore'), 'filename*': "UTF-8''%s" % url_quote(attachment_filename), } else: From bf6910a639fffc6896c5aefe362edaf6f6b704fe Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 13 Apr 2017 14:55:56 -0700 Subject: [PATCH 044/131] get mtime in utc --- tests/test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index d93b443e..d3906860 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -517,7 +517,7 @@ class TestSendfile(object): assert rv.status_code == 416 rv.close() - last_modified = datetime.datetime.fromtimestamp(os.path.getmtime( + last_modified = datetime.datetime.utcfromtimestamp(os.path.getmtime( os.path.join(app.root_path, 'static/index.html'))).replace( microsecond=0) From 1caa9de6286c5575accb05b942531080142242d3 Mon Sep 17 00:00:00 2001 From: accraze Date: Mon, 19 Dec 2016 08:03:37 -0800 Subject: [PATCH 045/131] Added missing testing config fixes #1302 --- docs/testing.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/testing.rst b/docs/testing.rst index 0737936e..edaa597c 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -98,8 +98,10 @@ test method to our class, like this:: def setUp(self): self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() + flaskr.app.config['TESTING'] = True self.app = flaskr.app.test_client() - flaskr.init_db() + with flaskr.app.app_context(): + flaskr.init_db() def tearDown(self): os.close(self.db_fd) From 03857cc48a1aff7af4c2bbc18a5740d1dc903c0b Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 13 Apr 2017 16:32:44 -0700 Subject: [PATCH 046/131] use app.testing property instead of config --- docs/testing.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index edaa597c..6fd7b504 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -41,7 +41,7 @@ In order to test the application, we add a second module def setUp(self): self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() - flaskr.app.config['TESTING'] = True + flaskr.app.testing = True self.app = flaskr.app.test_client() with flaskr.app.app_context(): flaskr.init_db() @@ -98,7 +98,7 @@ test method to our class, like this:: def setUp(self): self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() - flaskr.app.config['TESTING'] = True + flaskr.app.testing = True self.app = flaskr.app.test_client() with flaskr.app.app_context(): flaskr.init_db() @@ -210,7 +210,7 @@ temporarily. With this you can access the :class:`~flask.request`, functions. Here is a full example that demonstrates this approach:: import flask - + app = flask.Flask(__name__) with app.test_request_context('/?name=Peter'): From cfd3e50ab6447a69019fc008c260f8c078c4ac7d Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 13 Apr 2017 16:32:44 -0700 Subject: [PATCH 047/131] use app.testing property instead of config --- docs/testing.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 0737936e..6fd7b504 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -41,7 +41,7 @@ In order to test the application, we add a second module def setUp(self): self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() - flaskr.app.config['TESTING'] = True + flaskr.app.testing = True self.app = flaskr.app.test_client() with flaskr.app.app_context(): flaskr.init_db() @@ -98,8 +98,10 @@ test method to our class, like this:: def setUp(self): self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() + flaskr.app.testing = True self.app = flaskr.app.test_client() - flaskr.init_db() + with flaskr.app.app_context(): + flaskr.init_db() def tearDown(self): os.close(self.db_fd) @@ -208,7 +210,7 @@ temporarily. With this you can access the :class:`~flask.request`, functions. Here is a full example that demonstrates this approach:: import flask - + app = flask.Flask(__name__) with app.test_request_context('/?name=Peter'): From 4ff84d537aa386fde36182ed797a79e3b582be75 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 13 Apr 2017 14:55:56 -0700 Subject: [PATCH 048/131] get mtime in utc --- tests/test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 3e2ea8cd..b1241ce9 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -515,7 +515,7 @@ class TestSendfile(object): assert rv.status_code == 416 rv.close() - last_modified = datetime.datetime.fromtimestamp(os.path.getmtime( + last_modified = datetime.datetime.utcfromtimestamp(os.path.getmtime( os.path.join(app.root_path, 'static/index.html'))).replace( microsecond=0) From 7481844c98e4536ae01764aa08eec681c493ef2e Mon Sep 17 00:00:00 2001 From: Sobolev Nikita Date: Wed, 19 Apr 2017 08:46:33 +0300 Subject: [PATCH 049/131] Fix typo in app.py (#2248) --- flask/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/app.py b/flask/app.py index 87621aee..c26387f0 100644 --- a/flask/app.py +++ b/flask/app.py @@ -544,7 +544,7 @@ class Flask(_PackageBoundObject): self._before_request_lock = Lock() # Add a static route using the provided static_url_path, static_host, - # and static_folder iff there is a configured static_folder. + # and static_folder if there is a configured static_folder. # Note we do this without checking if static_folder exists. # For one, it might be created while the server is running (e.g. during # development). Also, Google App Engine stores static files somewhere From 19fbe3a18f36b02f22f8f545e015f60343c844ac Mon Sep 17 00:00:00 2001 From: rocambolesque Date: Fri, 9 Sep 2016 12:11:18 +0200 Subject: [PATCH 050/131] Add scheme to url_build error handler parameters --- CHANGES | 3 +++ flask/helpers.py | 1 + 2 files changed, 4 insertions(+) diff --git a/CHANGES b/CHANGES index 1f687409..d8f2577a 100644 --- a/CHANGES +++ b/CHANGES @@ -19,7 +19,10 @@ Major release, unreleased time the constructor adds the static route, and enables the static route to be properly associated with the required host. (``#1559``) - ``send_file`` supports Unicode in ``attachment_filename``. (`#2223`_) +- Pass ``_scheme`` argument from ``url_for`` to ``handle_build_error``. + (`#2017`_) +.. _#2017: https://github.com/pallets/flask/pull/2017 .. _#2223: https://github.com/pallets/flask/pull/2223 Version 0.12.1 diff --git a/flask/helpers.py b/flask/helpers.py index bfdcf224..828f5840 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -331,6 +331,7 @@ def url_for(endpoint, **values): values['_external'] = external values['_anchor'] = anchor values['_method'] = method + values['_scheme'] = scheme return appctx.app.handle_url_build_error(error, endpoint, values) if anchor is not None: From e50767cfca081681f527c54d3f6652d11e95e758 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 20 Apr 2017 08:52:37 -0700 Subject: [PATCH 051/131] add test for build error special values --- tests/test_basic.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/test_basic.py b/tests/test_basic.py index 76efd6e2..abce3ad4 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -1131,6 +1131,23 @@ def test_build_error_handler_reraise(): pytest.raises(BuildError, flask.url_for, 'not.existing') +def test_url_for_passes_special_values_to_build_error_handler(): + app = flask.Flask(__name__) + + @app.url_build_error_handlers.append + def handler(error, endpoint, values): + assert values == { + '_external': False, + '_anchor': None, + '_method': None, + '_scheme': None, + } + return 'handled' + + with app.test_request_context(): + flask.url_for('/') + + def test_custom_converters(): from werkzeug.routing import BaseConverter From 97e2cd0a5a5f2b663d7a82edbaede2fe2cfb0679 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 21 Apr 2017 07:16:09 -0700 Subject: [PATCH 052/131] update changelog move test next to existing test, rename reword / reflow param doc --- CHANGES | 7 ++-- flask/app.py | 11 +++--- tests/test_basic.py | 91 ++++++++++++++++++++++++--------------------- 3 files changed, 58 insertions(+), 51 deletions(-) diff --git a/CHANGES b/CHANGES index 34f7f13e..dba0112e 100644 --- a/CHANGES +++ b/CHANGES @@ -21,7 +21,11 @@ Major release, unreleased - ``send_file`` supports Unicode in ``attachment_filename``. (`#2223`_) - Pass ``_scheme`` argument from ``url_for`` to ``handle_build_error``. (`#2017`_) +- Add support for ``provide_automatic_options`` in ``add_url_rule`` to disable + adding OPTIONS method when the ``view_func`` argument is not a class. + (`#1489`_). +.. _#1489: https://github.com/pallets/flask/pull/1489 .. _#2017: https://github.com/pallets/flask/pull/2017 .. _#2223: https://github.com/pallets/flask/pull/2223 @@ -146,9 +150,6 @@ Released on May 29th 2016, codename Absinthe. - ``flask.g`` now has ``pop()`` and ``setdefault`` methods. - Turn on autoescape for ``flask.templating.render_template_string`` by default (pull request ``#1515``). -- Added support for `provide_automatic_options` in :meth:`add_url_rule` to - turn off automatic OPTIONS when the `view_func` argument is not a class - (pull request ``#1489``). - ``flask.ext`` is now deprecated (pull request ``#1484``). - ``send_from_directory`` now raises BadRequest if the filename is invalid on the server OS (pull request ``#1763``). diff --git a/flask/app.py b/flask/app.py index 32f5086c..a054d23c 100644 --- a/flask/app.py +++ b/flask/app.py @@ -980,8 +980,7 @@ class Flask(_PackageBoundObject): return iter(self._blueprint_order) @setupmethod - def add_url_rule(self, rule, endpoint=None, view_func=None, - provide_automatic_options=None, **options): + def add_url_rule(self, rule, endpoint=None, view_func=None, provide_automatic_options=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the endpoint. @@ -1021,10 +1020,10 @@ class Flask(_PackageBoundObject): endpoint :param view_func: the function to call when serving a request to the provided endpoint - :param provide_automatic_options: controls whether ``OPTIONS`` should - be provided automatically. If this - is not set, will check attributes on - the view or list of methods. + :param provide_automatic_options: controls whether the ``OPTIONS`` + method should be added automatically. This can also be controlled + by setting the ``view_func.provide_automatic_options = False`` + before adding the rule. :param options: the options to be forwarded to the underlying :class:`~werkzeug.routing.Rule` object. A change to Werkzeug is handling of method options. methods diff --git a/tests/test_basic.py b/tests/test_basic.py index 3fe69588..677b4be8 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -50,7 +50,7 @@ def test_options_on_multiple_rules(): assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] -def test_options_handling_disabled(): +def test_provide_automatic_options_attr(): app = flask.Flask(__name__) def index(): @@ -70,6 +70,54 @@ def test_options_handling_disabled(): assert sorted(rv.allow) == ['OPTIONS'] +def test_provide_automatic_options_kwarg(): + app = flask.Flask(__name__) + + def index(): + return flask.request.method + + def more(): + return flask.request.method + + app.add_url_rule('/', view_func=index, provide_automatic_options=False) + app.add_url_rule( + '/more', view_func=more, methods=['GET', 'POST'], + provide_automatic_options=False + ) + + c = app.test_client() + assert c.get('/').data == b'GET' + + rv = c.post('/') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD'] + + # Older versions of Werkzeug.test.Client don't have an options method + if hasattr(c, 'options'): + rv = c.options('/') + else: + rv = c.open('/', method='OPTIONS') + + assert rv.status_code == 405 + + rv = c.head('/') + assert rv.status_code == 200 + assert not rv.data # head truncates + assert c.post('/more').data == b'POST' + assert c.get('/more').data == b'GET' + + rv = c.delete('/more') + assert rv.status_code == 405 + assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] + + if hasattr(c, 'options'): + rv = c.options('/more') + else: + rv = c.open('/more', method='OPTIONS') + + assert rv.status_code == 405 + + def test_request_dispatching(): app = flask.Flask(__name__) @@ -1751,44 +1799,3 @@ def test_run_from_config(monkeypatch, host, port, expect_host, expect_port): app = flask.Flask(__name__) app.config['SERVER_NAME'] = 'pocoo.org:8080' app.run(host, port) - - -def test_disable_automatic_options(): - # Issue 1488: Add support for a kwarg to add_url_rule to disable the auto OPTIONS response - app = flask.Flask(__name__) - - def index(): - return flask.request.method - - def more(): - return flask.request.method - - app.add_url_rule('/', 'index', index, provide_automatic_options=False) - app.add_url_rule('/more', 'more', more, methods=['GET', 'POST'], provide_automatic_options=False) - - c = app.test_client() - assert c.get('/').data == b'GET' - rv = c.post('/') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD'] - # Older versions of Werkzeug.test.Client don't have an options method - if hasattr(c, 'options'): - rv = c.options('/') - else: - rv = c.open('/', method='OPTIONS') - assert rv.status_code == 405 - - rv = c.head('/') - assert rv.status_code == 200 - assert not rv.data # head truncates - assert c.post('/more').data == b'POST' - assert c.get('/more').data == b'GET' - rv = c.delete('/more') - assert rv.status_code == 405 - assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] - # Older versions of Werkzeug.test.Client don't have an options method - if hasattr(c, 'options'): - rv = c.options('/more') - else: - rv = c.open('/more', method='OPTIONS') - assert rv.status_code == 405 From 648344d4e8878e4aafcc5413f984b21b86173246 Mon Sep 17 00:00:00 2001 From: David Lord Date: Fri, 21 Apr 2017 10:32:00 -0700 Subject: [PATCH 053/131] use mro to collect methods ignore methods attr unless explicitly set add changelog --- CHANGES | 2 ++ flask/views.py | 44 ++++++++++++++++++++++---------------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/CHANGES b/CHANGES index dba0112e..4fd46d71 100644 --- a/CHANGES +++ b/CHANGES @@ -24,8 +24,10 @@ Major release, unreleased - Add support for ``provide_automatic_options`` in ``add_url_rule`` to disable adding OPTIONS method when the ``view_func`` argument is not a class. (`#1489`_). +- ``MethodView`` can inherit method handlers from base classes. (`#1936`_) .. _#1489: https://github.com/pallets/flask/pull/1489 +.. _#1936: https://github.com/pallets/flask/pull/1936 .. _#2017: https://github.com/pallets/flask/pull/2017 .. _#2223: https://github.com/pallets/flask/pull/2223 diff --git a/flask/views.py b/flask/views.py index 757c2a4d..848ccb0b 100644 --- a/flask/views.py +++ b/flask/views.py @@ -102,38 +102,35 @@ class View(object): return view -def get_methods(cls): - return getattr(cls, 'methods', []) or [] - - class MethodViewType(type): + """Metaclass for :class:`MethodView` that determines what methods the view + defines. + """ + + def __init__(cls, name, bases, d): + super(MethodViewType, cls).__init__(name, bases, d) - def __new__(cls, name, bases, d): - rv = type.__new__(cls, name, bases, d) if 'methods' not in d: - methods = set(m for b in bases for m in get_methods(b)) - for key in d: - if key in http_method_funcs: + methods = set() + + for key in http_method_funcs: + if hasattr(cls, key): methods.add(key.upper()) - # If we have no method at all in there we don't want to - # add a method list. (This is for instance the case for - # the base class or another subclass of a base method view - # that does not introduce new methods). + + # If we have no method at all in there we don't want to add a + # method list. This is for instance the case for the base class + # or another subclass of a base method view that does not introduce + # new methods. if methods: - rv.methods = sorted(methods) - return rv + cls.methods = methods class MethodView(with_metaclass(MethodViewType, View)): - """Like a regular class-based view but that dispatches requests to - particular methods. For instance if you implement a method called - :meth:`get` it means it will respond to ``'GET'`` requests and - the :meth:`dispatch_request` implementation will automatically - forward your request to that. Also :attr:`options` is set for you - automatically:: + """A class-based view that dispatches request methods to the corresponding + class methods. For example, if you implement a ``get`` method, it will be + used to handle ``GET`` requests. :: class CounterAPI(MethodView): - def get(self): return session.get('counter', 0) @@ -143,11 +140,14 @@ class MethodView(with_metaclass(MethodViewType, View)): app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter')) """ + def dispatch_request(self, *args, **kwargs): meth = getattr(self, request.method.lower(), None) + # If the request method is HEAD and we don't have a handler for it # retry with GET. if meth is None and request.method == 'HEAD': meth = getattr(self, 'get', None) + assert meth is not None, 'Unimplemented method %r' % request.method return meth(*args, **kwargs) From 13754b6d117eb6847cca45475412fdd05025f1b8 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 22 Apr 2017 13:39:54 -0700 Subject: [PATCH 054/131] ensure error while opening session pops context errors will be handled by the app error handlers closes #1538, closes #1528 --- CHANGES | 3 +++ flask/app.py | 2 +- tests/test_reqctx.py | 25 +++++++++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 4fd46d71..d7f15d66 100644 --- a/CHANGES +++ b/CHANGES @@ -25,11 +25,14 @@ Major release, unreleased adding OPTIONS method when the ``view_func`` argument is not a class. (`#1489`_). - ``MethodView`` can inherit method handlers from base classes. (`#1936`_) +- Errors caused while opening the session at the beginning of the request are + handled by the app's error handlers. (`#2254`_) .. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1936: https://github.com/pallets/flask/pull/1936 .. _#2017: https://github.com/pallets/flask/pull/2017 .. _#2223: https://github.com/pallets/flask/pull/2223 +.. _#2254: https://github.com/pallets/flask/pull/2254 Version 0.12.1 -------------- diff --git a/flask/app.py b/flask/app.py index a054d23c..1943cfcf 100644 --- a/flask/app.py +++ b/flask/app.py @@ -2002,10 +2002,10 @@ class Flask(_PackageBoundObject): exception context to start the response """ ctx = self.request_context(environ) - ctx.push() error = None try: try: + ctx.push() response = self.full_dispatch_request() except Exception as e: error = e diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py index 4b2b1f87..48823fb2 100644 --- a/tests/test_reqctx.py +++ b/tests/test_reqctx.py @@ -12,6 +12,7 @@ import pytest import flask +from flask.sessions import SessionInterface try: from greenlet import greenlet @@ -193,3 +194,27 @@ def test_greenlet_context_copying_api(): result = greenlets[0].run() assert result == 42 + + +def test_session_error_pops_context(): + class SessionError(Exception): + pass + + class FailingSessionInterface(SessionInterface): + def open_session(self, app, request): + raise SessionError() + + class CustomFlask(flask.Flask): + session_interface = FailingSessionInterface() + + app = CustomFlask(__name__) + + @app.route('/') + def index(): + # shouldn't get here + assert False + + response = app.test_client().get('/') + assert response.status_code == 500 + assert not flask.request + assert not flask.current_app From 46f83665ef74880bbc4a1c1f17ccededa7aaa939 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 24 Apr 2017 10:09:50 -0700 Subject: [PATCH 055/131] clean up blueprint json support add changelog for #1898 --- CHANGES | 3 +++ flask/blueprints.py | 4 ++-- flask/json.py | 24 ++++++++++++++---------- tests/test_helpers.py | 16 +++++++++++----- 4 files changed, 30 insertions(+), 17 deletions(-) diff --git a/CHANGES b/CHANGES index d7f15d66..ae592ba8 100644 --- a/CHANGES +++ b/CHANGES @@ -27,8 +27,11 @@ Major release, unreleased - ``MethodView`` can inherit method handlers from base classes. (`#1936`_) - Errors caused while opening the session at the beginning of the request are handled by the app's error handlers. (`#2254`_) +- Blueprints gained ``json_encoder`` and ``json_decoder`` attributes to + override the app's encoder and decoder. (`#1898`_) .. _#1489: https://github.com/pallets/flask/pull/1489 +.. _#1898: https://github.com/pallets/flask/pull/1898 .. _#1936: https://github.com/pallets/flask/pull/1936 .. _#2017: https://github.com/pallets/flask/pull/2017 .. _#2223: https://github.com/pallets/flask/pull/2223 diff --git a/flask/blueprints.py b/flask/blueprints.py index 62675204..57d77512 100644 --- a/flask/blueprints.py +++ b/flask/blueprints.py @@ -90,10 +90,10 @@ class Blueprint(_PackageBoundObject): _got_registered_once = False #: Blueprint local JSON decoder class to use. - # Set to None to use the :class:`~flask.app.Flask.json_encoder`. + #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_encoder`. json_encoder = None #: Blueprint local JSON decoder class to use. - # Set to None to use the :class:`~flask.app.Flask.json_decoder`. + #: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_decoder`. json_decoder = None def __init__(self, name, import_name, static_folder=None, diff --git a/flask/json.py b/flask/json.py index 77b5fce1..bf8a8843 100644 --- a/flask/json.py +++ b/flask/json.py @@ -92,13 +92,16 @@ class JSONDecoder(_json.JSONDecoder): def _dump_arg_defaults(kwargs): """Inject default arguments for dump functions.""" if current_app: - bp = current_app.blueprints.get(request.blueprint, - None) if has_request_context() else None - kwargs.setdefault('cls', - bp.json_encoder if bp and bp.json_encoder - else current_app.json_encoder) + bp = current_app.blueprints.get(request.blueprint) if request else None + kwargs.setdefault( + 'cls', + bp.json_encoder if bp and bp.json_encoder + else current_app.json_encoder + ) + if not current_app.config['JSON_AS_ASCII']: kwargs.setdefault('ensure_ascii', False) + kwargs.setdefault('sort_keys', current_app.config['JSON_SORT_KEYS']) else: kwargs.setdefault('sort_keys', True) @@ -108,11 +111,12 @@ def _dump_arg_defaults(kwargs): def _load_arg_defaults(kwargs): """Inject default arguments for load functions.""" if current_app: - bp = current_app.blueprints.get(request.blueprint, - None) if has_request_context() else None - kwargs.setdefault('cls', - bp.json_decoder if bp and bp.json_decoder - else current_app.json_decoder) + bp = current_app.blueprints.get(request.blueprint) if request else None + kwargs.setdefault( + 'cls', + bp.json_decoder if bp and bp.json_decoder + else current_app.json_decoder + ) else: kwargs.setdefault('cls', JSONDecoder) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index c811e1b7..325713c0 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -271,30 +271,36 @@ class TestJSON(object): class X(object): def __init__(self, val): self.val = val + class MyEncoder(flask.json.JSONEncoder): def default(self, o): if isinstance(o, X): return '<%d>' % o.val + return flask.json.JSONEncoder.default(self, o) + class MyDecoder(flask.json.JSONDecoder): def __init__(self, *args, **kwargs): kwargs.setdefault('object_hook', self.object_hook) flask.json.JSONDecoder.__init__(self, *args, **kwargs) + def object_hook(self, obj): if len(obj) == 1 and '_foo' in obj: return X(obj['_foo']) + return obj - blue = flask.Blueprint('blue', __name__) - blue.json_encoder = MyEncoder - blue.json_decoder = MyDecoder - @blue.route('/bp', methods=['POST']) + bp = flask.Blueprint('bp', __name__) + bp.json_encoder = MyEncoder + bp.json_decoder = MyDecoder + + @bp.route('/bp', methods=['POST']) def index(): return flask.json.dumps(flask.request.get_json()['x']) app = flask.Flask(__name__) app.testing = True - app.register_blueprint(blue) + app.register_blueprint(bp) c = app.test_client() rv = c.post('/bp', data=flask.json.dumps({ From 697f7b9365304c45216e6c8dd307d931ea1506d0 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 24 Apr 2017 14:11:49 -0700 Subject: [PATCH 056/131] refactor make_response to be easier to follow * be explicit about how tuples are unpacked * allow bytes for status value * allow Headers for headers value * use TypeError instead of ValueError * errors are more descriptive * document that view must not return None * update documentation about return values * test more response types * test error messages closes #1676 --- CHANGES | 4 ++ flask/app.py | 170 ++++++++++++++++++++++++++++---------------- tests/test_basic.py | 143 +++++++++++++++++++++++++------------ 3 files changed, 207 insertions(+), 110 deletions(-) diff --git a/CHANGES b/CHANGES index ae592ba8..11ac6430 100644 --- a/CHANGES +++ b/CHANGES @@ -29,6 +29,9 @@ Major release, unreleased handled by the app's error handlers. (`#2254`_) - Blueprints gained ``json_encoder`` and ``json_decoder`` attributes to override the app's encoder and decoder. (`#1898`_) +- ``Flask.make_response`` raises ``TypeError`` instead of ``ValueError`` for + bad response types. The error messages have been improved to describe why the + type is invalid. (`#2256`_) .. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1898: https://github.com/pallets/flask/pull/1898 @@ -36,6 +39,7 @@ Major release, unreleased .. _#2017: https://github.com/pallets/flask/pull/2017 .. _#2223: https://github.com/pallets/flask/pull/2223 .. _#2254: https://github.com/pallets/flask/pull/2254 +.. _#2256: https://github.com/pallets/flask/pull/2256 Version 0.12.1 -------------- diff --git a/flask/app.py b/flask/app.py index 1943cfcf..a938344b 100644 --- a/flask/app.py +++ b/flask/app.py @@ -10,30 +10,30 @@ """ import os import sys -from threading import Lock from datetime import timedelta -from itertools import chain from functools import update_wrapper +from itertools import chain +from threading import Lock -from werkzeug.datastructures import ImmutableDict -from werkzeug.routing import Map, Rule, RequestRedirect, BuildError -from werkzeug.exceptions import HTTPException, InternalServerError, \ - MethodNotAllowed, BadRequest, default_exceptions - -from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - locked_cached_property, _endpoint_from_view_func, find_package, \ - get_debug_flag -from . import json, cli -from .wrappers import Request, Response -from .config import ConfigAttribute, Config -from .ctx import RequestContext, AppContext, _AppCtxGlobals -from .globals import _request_ctx_stack, request, session, g +from werkzeug.datastructures import ImmutableDict, Headers +from werkzeug.exceptions import BadRequest, HTTPException, \ + InternalServerError, MethodNotAllowed, default_exceptions +from werkzeug.routing import BuildError, Map, RequestRedirect, Rule + +from . import cli, json +from ._compat import integer_types, reraise, string_types, text_type +from .config import Config, ConfigAttribute +from .ctx import AppContext, RequestContext, _AppCtxGlobals +from .globals import _request_ctx_stack, g, request, session +from .helpers import _PackageBoundObject, \ + _endpoint_from_view_func, find_package, get_debug_flag, \ + get_flashed_messages, locked_cached_property, url_for from .sessions import SecureCookieSessionInterface +from .signals import appcontext_tearing_down, got_request_exception, \ + request_finished, request_started, request_tearing_down from .templating import DispatchingJinjaLoader, Environment, \ - _default_template_ctx_processor -from .signals import request_started, request_finished, got_request_exception, \ - request_tearing_down, appcontext_tearing_down -from ._compat import reraise, string_types, text_type, integer_types + _default_template_ctx_processor +from .wrappers import Request, Response # a lock used for logger initialization _logger_lock = Lock() @@ -1715,62 +1715,106 @@ class Flask(_PackageBoundObject): return False def make_response(self, rv): - """Converts the return value from a view function to a real - response object that is an instance of :attr:`response_class`. - - The following types are allowed for `rv`: - - .. tabularcolumns:: |p{3.5cm}|p{9.5cm}| - - ======================= =========================================== - :attr:`response_class` the object is returned unchanged - :class:`str` a response object is created with the - string as body - :class:`unicode` a response object is created with the - string encoded to utf-8 as body - a WSGI function the function is called as WSGI application - and buffered as response object - :class:`tuple` A tuple in the form ``(response, status, - headers)`` or ``(response, headers)`` - where `response` is any of the - types defined here, `status` is a string - or an integer and `headers` is a list or - a dictionary with header values. - ======================= =========================================== - - :param rv: the return value from the view function + """Convert the return value from a view function to an instance of + :attr:`response_class`. + + :param rv: the return value from the view function. The view function + must return a response. Returning ``None``, or the view ending + without returning, is not allowed. The following types are allowed + for ``view_rv``: + + ``str`` (``unicode`` in Python 2) + A response object is created with the string encoded to UTF-8 + as the body. + + ``bytes`` (``str`` in Python 2) + A response object is created with the bytes as the body. + + ``tuple`` + Either ``(body, status, headers)``, ``(body, status)``, or + ``(body, headers)``, where ``body`` is any of the other types + allowed here, ``status`` is a string or an integer, and + ``headers`` is a dictionary or a list of ``(key, value)`` + tuples. If ``body`` is a :attr:`response_class` instance, + ``status`` overwrites the exiting value and ``headers`` are + extended. + + :attr:`response_class` + The object is returned unchanged. + + other :class:`~werkzeug.wrappers.Response` class + The object is coerced to :attr:`response_class`. + + :func:`callable` + The function is called as a WSGI application. The result is + used to create a response object. .. versionchanged:: 0.9 Previously a tuple was interpreted as the arguments for the response object. """ - status_or_headers = headers = None - if isinstance(rv, tuple): - rv, status_or_headers, headers = rv + (None,) * (3 - len(rv)) - if rv is None: - raise ValueError('View function did not return a response') + status = headers = None + + # unpack tuple returns + if isinstance(rv, (tuple, list)): + len_rv = len(rv) - if isinstance(status_or_headers, (dict, list)): - headers, status_or_headers = status_or_headers, None + # a 3-tuple is unpacked directly + if len_rv == 3: + rv, status, headers = rv + # decide if a 2-tuple has status or headers + elif len_rv == 2: + if isinstance(rv[1], (Headers, dict, tuple, list)): + rv, headers = rv + else: + rv, status = rv + # other sized tuples are not allowed + else: + raise TypeError( + 'The view function did not return a valid response tuple.' + ' The tuple must have the form (body, status, headers),' + ' (body, status), or (body, headers).' + ) + # the body must not be None + if rv is None: + raise TypeError( + 'The view function did not return a valid response. The' + ' function either returned None or ended without a return' + ' statement.' + ) + + # make sure the body is an instance of the response class if not isinstance(rv, self.response_class): - # When we create a response object directly, we let the constructor - # set the headers and status. We do this because there can be - # some extra logic involved when creating these objects with - # specific values (like default content type selection). if isinstance(rv, (text_type, bytes, bytearray)): - rv = self.response_class(rv, headers=headers, - status=status_or_headers) - headers = status_or_headers = None + # let the response class set the status and headers instead of + # waiting to do it manually, so that the class can handle any + # special logic + rv = self.response_class(rv, status=status, headers=headers) + status = headers = None else: - rv = self.response_class.force_type(rv, request.environ) - - if status_or_headers is not None: - if isinstance(status_or_headers, string_types): - rv.status = status_or_headers + # evaluate a WSGI callable, or coerce a different response + # class to the correct type + try: + rv = self.response_class.force_type(rv, request.environ) + except TypeError as e: + new_error = TypeError( + '{e}\nThe view function did not return a valid' + ' response. The return type must be a string, tuple,' + ' Response instance, or WSGI callable, but it was a' + ' {rv.__class__.__name__}.'.format(e=e, rv=rv) + ) + reraise(TypeError, new_error, sys.exc_info()[2]) + + # prefer the status if it was provided + if status is not None: + if isinstance(status, (text_type, bytes, bytearray)): + rv.status = status else: - rv.status_code = status_or_headers + rv.status_code = status + + # extend existing headers with provided headers if headers: rv.headers.extend(headers) diff --git a/tests/test_basic.py b/tests/test_basic.py index 677b4be8..163b83cf 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -975,64 +975,129 @@ def test_enctype_debug_helper(): assert 'This was submitted: "index.txt"' in str(e.value) -def test_response_creation(): +def test_response_types(): app = flask.Flask(__name__) + app.testing = True - @app.route('/unicode') - def from_unicode(): + @app.route('/text') + def from_text(): return u'Hällo Wörld' - @app.route('/string') - def from_string(): + @app.route('/bytes') + def from_bytes(): return u'Hällo Wörld'.encode('utf-8') - @app.route('/args') - def from_tuple(): + @app.route('/full_tuple') + def from_full_tuple(): return 'Meh', 400, { 'X-Foo': 'Testing', 'Content-Type': 'text/plain; charset=utf-8' } - @app.route('/two_args') - def from_two_args_tuple(): + @app.route('/text_headers') + def from_text_headers(): return 'Hello', { 'X-Foo': 'Test', 'Content-Type': 'text/plain; charset=utf-8' } - @app.route('/args_status') - def from_status_tuple(): + @app.route('/text_status') + def from_text_status(): return 'Hi, status!', 400 - @app.route('/args_header') - def from_response_instance_status_tuple(): - return flask.Response('Hello world', 404), { + @app.route('/response_headers') + def from_response_headers(): + return flask.Response('Hello world', 404, {'X-Foo': 'Baz'}), { "X-Foo": "Bar", "X-Bar": "Foo" } + @app.route('/response_status') + def from_response_status(): + return app.response_class('Hello world', 400), 500 + + @app.route('/wsgi') + def from_wsgi(): + return NotFound() + c = app.test_client() - assert c.get('/unicode').data == u'Hällo Wörld'.encode('utf-8') - assert c.get('/string').data == u'Hällo Wörld'.encode('utf-8') - rv = c.get('/args') + + assert c.get('/text').data == u'Hällo Wörld'.encode('utf-8') + assert c.get('/bytes').data == u'Hällo Wörld'.encode('utf-8') + + rv = c.get('/full_tuple') assert rv.data == b'Meh' assert rv.headers['X-Foo'] == 'Testing' assert rv.status_code == 400 assert rv.mimetype == 'text/plain' - rv2 = c.get('/two_args') - assert rv2.data == b'Hello' - assert rv2.headers['X-Foo'] == 'Test' - assert rv2.status_code == 200 - assert rv2.mimetype == 'text/plain' - rv3 = c.get('/args_status') - assert rv3.data == b'Hi, status!' - assert rv3.status_code == 400 - assert rv3.mimetype == 'text/html' - rv4 = c.get('/args_header') - assert rv4.data == b'Hello world' - assert rv4.headers['X-Foo'] == 'Bar' - assert rv4.headers['X-Bar'] == 'Foo' - assert rv4.status_code == 404 + + rv = c.get('/text_headers') + assert rv.data == b'Hello' + assert rv.headers['X-Foo'] == 'Test' + assert rv.status_code == 200 + assert rv.mimetype == 'text/plain' + + rv = c.get('/text_status') + assert rv.data == b'Hi, status!' + assert rv.status_code == 400 + assert rv.mimetype == 'text/html' + + rv = c.get('/response_headers') + assert rv.data == b'Hello world' + assert rv.headers.getlist('X-Foo') == ['Baz', 'Bar'] + assert rv.headers['X-Bar'] == 'Foo' + assert rv.status_code == 404 + + rv = c.get('/response_status') + assert rv.data == b'Hello world' + assert rv.status_code == 500 + + rv = c.get('/wsgi') + assert b'Not Found' in rv.data + assert rv.status_code == 404 + + +def test_response_type_errors(): + app = flask.Flask(__name__) + app.testing = True + + @app.route('/none') + def from_none(): + pass + + @app.route('/small_tuple') + def from_small_tuple(): + return 'Hello', + + @app.route('/large_tuple') + def from_large_tuple(): + return 'Hello', 234, {'X-Foo': 'Bar'}, '???' + + @app.route('/bad_type') + def from_bad_type(): + return True + + @app.route('/bad_wsgi') + def from_bad_wsgi(): + return lambda: None + + c = app.test_client() + + with pytest.raises(TypeError) as e: + c.get('/none') + assert 'returned None' in str(e) + + with pytest.raises(TypeError) as e: + c.get('/small_tuple') + assert 'tuple must have the form' in str(e) + + pytest.raises(TypeError, c.get, '/large_tuple') + + with pytest.raises(TypeError) as e: + c.get('/bad_type') + assert 'it was a bool' in str(e) + + pytest.raises(TypeError, c.get, '/bad_wsgi') def test_make_response(): @@ -1272,22 +1337,6 @@ def test_static_route_with_host_matching(): flask.Flask(__name__, host_matching=True, static_folder=None) -def test_none_response(): - app = flask.Flask(__name__) - app.testing = True - - @app.route('/') - def test(): - return None - try: - app.test_client().get('/') - except ValueError as e: - assert str(e) == 'View function did not return a response' - pass - else: - assert "Expected ValueError" - - def test_request_locals(): assert repr(flask.g) == '' assert not flask.g From 501f0431259a30569a5e62bcce68d102fc3ef993 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 25 Apr 2017 12:03:08 -0700 Subject: [PATCH 057/131] clean up preprocess_request docs [ci skip] --- flask/app.py | 70 ++++++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/flask/app.py b/flask/app.py index 84f55d2c..703cfef2 100644 --- a/flask/app.py +++ b/flask/app.py @@ -415,17 +415,16 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.9 self.url_build_error_handlers = [] - #: A dictionary with lists of functions that should be called at the - #: beginning of the request. The key of the dictionary is the name of - #: the blueprint this function is active for, ``None`` for all requests. - #: This can for example be used to open database connections or - #: getting hold of the currently logged in user. To register a - #: function here, use the :meth:`before_request` decorator. + #: A dictionary with lists of functions that will be called at the + #: beginning of each request. The key of the dictionary is the name of + #: the blueprint this function is active for, or ``None`` for all + #: requests. To register a function, use the :meth:`before_request` + #: decorator. self.before_request_funcs = {} - #: A lists of functions that should be called at the beginning of the - #: first request to this instance. To register a function here, use - #: the :meth:`before_first_request` decorator. + #: A list of functions that will be called at the beginning of the + #: first request to this instance. To register a function, use the + #: :meth:`before_first_request` decorator. #: #: .. versionadded:: 0.8 self.before_first_request_funcs = [] @@ -457,12 +456,11 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.9 self.teardown_appcontext_funcs = [] - #: A dictionary with lists of functions that can be used as URL - #: value processor functions. Whenever a URL is built these functions - #: are called to modify the dictionary of values in place. The key - #: ``None`` here is used for application wide - #: callbacks, otherwise the key is the name of the blueprint. - #: Each of these functions has the chance to modify the dictionary + #: A dictionary with lists of functions that are called before the + #: :attr:`before_request_funcs` functions. The key of the dictionary is + #: the name of the blueprint this function is active for, or ``None`` + #: for all requests. To register a function, use + #: :meth:`url_value_preprocessor`. #: #: .. versionadded:: 0.7 self.url_value_preprocessors = {} @@ -1314,11 +1312,13 @@ class Flask(_PackageBoundObject): @setupmethod def before_request(self, f): """Registers a function to run before each request. + + For example, this can be used to open a database connection, or to load + the logged in user from the session. - The function will be called without any arguments. - If the function returns a non-None value, it's handled as - if it was the return value from the view and further - request handling is stopped. + The function will be called without any arguments. If it returns a + non-None value, the value is handled as if it was the return value from + the view, and further request handling is stopped. """ self.before_request_funcs.setdefault(None, []).append(f) return f @@ -1437,9 +1437,17 @@ class Flask(_PackageBoundObject): @setupmethod def url_value_preprocessor(self, f): - """Registers a function as URL value preprocessor for all view - functions of the application. It's called before the view functions - are called and can modify the url values provided. + """Register a URL value preprocessor function for all view + functions in the application. These functions will be called before the + :meth:`before_request` functions. + + The function can modify the values captured from the matched url before + they are passed to the view. For example, this can be used to pop a + common language code value and place it in ``g`` rather than pass it to + every view. + + The function is passed the endpoint name and values dict. The return + value is ignored. """ self.url_value_preprocessors.setdefault(None, []).append(f) return f @@ -1877,16 +1885,14 @@ class Flask(_PackageBoundObject): raise error def preprocess_request(self): - """Called before the request dispatching. - - Triggers two set of hook functions that should be invoked prior to request dispatching: - :attr:`url_value_preprocessors` and :attr:`before_request_funcs` - (the latter are functions decorated with :meth:`before_request` decorator). - In both cases, the method triggers only the functions that are either global - or registered to the current blueprint. - - If any function in :attr:`before_request_funcs` returns a value, it's handled as if it was - the return value from the view function, and further request handling is stopped. + """Called before the request is dispatched. Calls + :attr:`url_value_preprocessors` registered with the app and the + current blueprint (if any). Then calls :attr:`before_request_funcs` + registered with the app and the blueprint. + + If any :meth:`before_request` handler returns a non-None value, the + value is handled as if it was the return value from the view, and + further request handling is stopped. """ bp = _request_ctx_stack.top.request.blueprint From 7ad79583b9558cc7806d56c534f9527e500734e9 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 25 Apr 2017 14:15:38 -0700 Subject: [PATCH 058/131] add sort by match order sort by endpoint by default combine sort flags sort methods ignore HEAD and OPTIONS methods by default rearrange columns use format to build row format string rework tests add changelog --- CHANGES | 3 ++ flask/cli.py | 74 +++++++++++++++---------- tests/test_apps/cliapp/routesapp.py | 18 ------- tests/test_cli.py | 84 ++++++++++++++++++----------- 4 files changed, 104 insertions(+), 75 deletions(-) delete mode 100644 tests/test_apps/cliapp/routesapp.py diff --git a/CHANGES b/CHANGES index 11ac6430..ddab541f 100644 --- a/CHANGES +++ b/CHANGES @@ -32,6 +32,8 @@ Major release, unreleased - ``Flask.make_response`` raises ``TypeError`` instead of ``ValueError`` for bad response types. The error messages have been improved to describe why the type is invalid. (`#2256`_) +- Add ``routes`` CLI command to output routes registered on the application. + (`#2259`_) .. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1898: https://github.com/pallets/flask/pull/1898 @@ -40,6 +42,7 @@ Major release, unreleased .. _#2223: https://github.com/pallets/flask/pull/2223 .. _#2254: https://github.com/pallets/flask/pull/2254 .. _#2256: https://github.com/pallets/flask/pull/2256 +.. _#2259: https://github.com/pallets/flask/pull/2259 Version 0.12.1 -------------- diff --git a/flask/cli.py b/flask/cli.py index 80aa1cd5..3d361be8 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -12,14 +12,17 @@ import os import sys import traceback -from threading import Lock, Thread from functools import update_wrapper +from operator import attrgetter +from threading import Lock, Thread import click +from . import __version__ from ._compat import iteritems, reraise +from .globals import current_app from .helpers import get_debug_flag -from . import __version__ + class NoAppException(click.UsageError): """Raised if an application cannot be found or loaded.""" @@ -485,34 +488,51 @@ def shell_command(): code.interact(banner=banner, local=ctx) -@click.command('routes', short_help='Show routes for the app.') -@click.option('-r', 'order_by', flag_value='rule', default=True, help='Order by route') -@click.option('-e', 'order_by', flag_value='endpoint', help='Order by endpoint') -@click.option('-m', 'order_by', flag_value='methods', help='Order by methods') +@click.command('routes', short_help='Show the routes for the app.') +@click.option( + '--sort', '-s', + type=click.Choice(('endpoint', 'methods', 'rule', 'match')), + default='endpoint', + help=( + 'Method to sort routes by. "match" is the order that Flask will match ' + 'routes when dispatching a request.' + ) +) +@click.option( + '--all-methods', + is_flag=True, + help="Show HEAD and OPTIONS methods." +) @with_appcontext -def routes_command(order_by): - """Show all routes with endpoints and methods.""" - from flask.globals import _app_ctx_stack - app = _app_ctx_stack.top.app - - order_key = lambda rule: getattr(rule, order_by) - sorted_rules = sorted(app.url_map.iter_rules(), key=order_key) - - max_rule = max(len(rule.rule) for rule in sorted_rules) - max_rule = max(max_rule, len('Route')) - max_ep = max(len(rule.endpoint) for rule in sorted_rules) - max_ep = max(max_ep, len('Endpoint')) - max_meth = max(len(', '.join(rule.methods)) for rule in sorted_rules) - max_meth = max(max_meth, len('Methods')) +def routes_command(sort, all_methods): + """Show all registered routes with endpoints and methods.""" + + rules = list(current_app.url_map.iter_rules()) + ignored_methods = set(() if all_methods else ('HEAD', 'OPTIONS')) + + if sort in ('endpoint', 'rule'): + rules = sorted(rules, key=attrgetter(sort)) + elif sort == 'methods': + rules = sorted(rules, key=lambda rule: sorted(rule.methods)) + + rule_methods = [ + ', '.join(sorted(rule.methods - ignored_methods)) for rule in rules + ] + + headers = ('Endpoint', 'Methods', 'Rule') + widths = ( + max(len(rule.endpoint) for rule in rules), + max(len(methods) for methods in rule_methods), + max(len(rule.rule) for rule in rules), + ) + widths = [max(len(h), w) for h, w in zip(headers, widths)] + row = '{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}'.format(*widths) - columnformat = '{:<%s} {:<%s} {:<%s}' % (max_rule, max_ep, max_meth) - click.echo(columnformat.format('Route', 'Endpoint', 'Methods')) - under_count = max_rule + max_ep + max_meth + 4 - click.echo('-' * under_count) + click.echo(row.format(*headers).strip()) + click.echo(row.format(*('-' * width for width in widths))) - for rule in sorted_rules: - methods = ', '.join(rule.methods) - click.echo(columnformat.format(rule.rule, rule.endpoint, methods)) + for rule, methods in zip(rules, rule_methods): + click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip()) cli = FlaskGroup(help="""\ diff --git a/tests/test_apps/cliapp/routesapp.py b/tests/test_apps/cliapp/routesapp.py deleted file mode 100644 index 84060546..00000000 --- a/tests/test_apps/cliapp/routesapp.py +++ /dev/null @@ -1,18 +0,0 @@ -from __future__ import absolute_import, print_function - -from flask import Flask - - -noroute_app = Flask('noroute app') -simpleroute_app = Flask('simpleroute app') -only_POST_route_app = Flask('GET route app') - - -@simpleroute_app.route('/simpleroute') -def simple(): - pass - - -@only_POST_route_app.route('/only-post', methods=['POST']) -def only_post(): - pass diff --git a/tests/test_cli.py b/tests/test_cli.py index 56ebce90..ab875cef 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -14,6 +14,7 @@ from __future__ import absolute_import, print_function import os import sys +from functools import partial import click import pytest @@ -195,7 +196,7 @@ def test_flaskgroup(runner): assert result.output == 'flaskgroup\n' -def test_print_exceptions(): +def test_print_exceptions(runner): """Print the stacktrace if the CLI.""" def create_app(info): raise Exception("oh no") @@ -205,7 +206,6 @@ def test_print_exceptions(): def cli(**params): pass - runner = CliRunner() result = runner.invoke(cli, ['--help']) assert result.exit_code == 0 assert 'Exception: oh no' in result.output @@ -213,34 +213,58 @@ def test_print_exceptions(): class TestRoutes: - def test_no_route(self, runner, monkeypatch): - monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:noroute_app') - result = runner.invoke(cli, ['routes'], catch_exceptions=False) - assert result.exit_code == 0 - assert result.output == """\ -Route Endpoint Methods ------------------------------------------------------ -/static/ static HEAD, OPTIONS, GET -""" + @pytest.fixture + def invoke(self, runner): + def create_app(info): + app = Flask(__name__) + app.testing = True - def test_simple_route(self, runner, monkeypatch): - monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:simpleroute_app') - result = runner.invoke(cli, ['routes'], catch_exceptions=False) - assert result.exit_code == 0 - assert result.output == """\ -Route Endpoint Methods ------------------------------------------------------ -/simpleroute simple HEAD, OPTIONS, GET -/static/ static HEAD, OPTIONS, GET -""" + @app.route('/get_post//', methods=['GET', 'POST']) + def yyy_get_post(x, y): + pass + + @app.route('/zzz_post', methods=['POST']) + def aaa_post(): + pass - def test_only_POST_route(self, runner, monkeypatch): - monkeypatch.setitem(os.environ, 'FLASK_APP', 'cliapp.routesapp:only_POST_route_app') - result = runner.invoke(cli, ['routes'], catch_exceptions=False) + return app + + cli = FlaskGroup(create_app=create_app) + return partial(runner.invoke, cli) + + def expect_order(self, order, output): + # skip the header and match the start of each row + for expect, line in zip(order, output.splitlines()[2:]): + # do this instead of startswith for nicer pytest output + assert line[:len(expect)] == expect + + def test_simple(self, invoke): + result = invoke(['routes']) assert result.exit_code == 0 - assert result.output == """\ -Route Endpoint Methods ------------------------------------------------------- -/only-post only_post POST, OPTIONS -/static/ static HEAD, OPTIONS, GET -""" + self.expect_order( + ['aaa_post', 'static', 'yyy_get_post'], + result.output + ) + + def test_sort(self, invoke): + default_output = invoke(['routes']).output + endpoint_output = invoke(['routes', '-s', 'endpoint']).output + assert default_output == endpoint_output + self.expect_order( + ['static', 'yyy_get_post', 'aaa_post'], + invoke(['routes', '-s', 'methods']).output + ) + self.expect_order( + ['yyy_get_post', 'static', 'aaa_post'], + invoke(['routes', '-s', 'rule']).output + ) + self.expect_order( + ['aaa_post', 'yyy_get_post', 'static'], + invoke(['routes', '-s', 'match']).output + ) + + def test_all_methods(self, invoke): + output = invoke(['routes']).output + assert 'GET, HEAD, OPTIONS, POST' not in output + output = invoke(['routes', '--all-methods']).output + assert 'GET, HEAD, OPTIONS, POST' in output From 6032c94aeb090c82ac99b9f886341f61c73b65d0 Mon Sep 17 00:00:00 2001 From: Benjamin Liebald Date: Wed, 2 Nov 2016 13:36:54 -0700 Subject: [PATCH 059/131] Mention existence of register_error_handler in errorpages.rst See https://github.com/pallets/flask/issues/1837 for context. --- docs/patterns/errorpages.rst | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index fccd4a6f..e7ec6b65 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -54,9 +54,11 @@ can be a different error: a handler for internal server errors will be passed other exception instances as well if they are uncaught. An error handler is registered with the :meth:`~flask.Flask.errorhandler` -decorator and the error code of the exception. Keep in mind that Flask -will *not* set the error code for you, so make sure to also provide the -HTTP status code when returning a response. +decorator and the error code of the exception (alternatively, you can use the +:meth:`~flask.Flask.register_error_handler` function, e.g., when you're +registering error handlers as part of your Application Factory). Keep in mind +that Flask will *not* set the error code for you, so make sure to also provide +the HTTP status code when returning a response.delete_cookie. Please note that if you add an error handler for "500 Internal Server Error", Flask will not trigger it if it's running in Debug mode. @@ -69,6 +71,18 @@ Here an example implementation for a "404 Page Not Found" exception:: def page_not_found(e): return render_template('404.html'), 404 +And, using an application factory pattern (see :ref:`app-factories`):: + + from flask import Flask, render_template + + def page_not_found(e): + return render_template('404.html'), 404 + + def create_app(config_filename): + app = Flask(__name__) + # ... + app.register_error_handler(404, page_not_found) + An example template might be this: .. sourcecode:: html+jinja From 011a4b1899fcb6da15a9d60c3a5c36a73ff92c46 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sun, 30 Apr 2017 08:20:13 -0700 Subject: [PATCH 060/131] clean up error handler docs --- docs/patterns/errorpages.rst | 52 +++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/docs/patterns/errorpages.rst b/docs/patterns/errorpages.rst index e7ec6b65..1df9c061 100644 --- a/docs/patterns/errorpages.rst +++ b/docs/patterns/errorpages.rst @@ -47,51 +47,53 @@ even if the application behaves correctly: Error Handlers -------------- -An error handler is a function, just like a view function, but it is -called when an error happens and is passed that error. The error is most -likely a :exc:`~werkzeug.exceptions.HTTPException`, but in one case it -can be a different error: a handler for internal server errors will be -passed other exception instances as well if they are uncaught. +An error handler is a function that returns a response when a type of error is +raised, similar to how a view is a function that returns a response when a +request URL is matched. It is passed the instance of the error being handled, +which is most likely a :exc:`~werkzeug.exceptions.HTTPException`. An error +handler for "500 Internal Server Error" will be passed uncaught exceptions in +addition to explicit 500 errors. An error handler is registered with the :meth:`~flask.Flask.errorhandler` -decorator and the error code of the exception (alternatively, you can use the -:meth:`~flask.Flask.register_error_handler` function, e.g., when you're -registering error handlers as part of your Application Factory). Keep in mind -that Flask will *not* set the error code for you, so make sure to also provide -the HTTP status code when returning a response.delete_cookie. +decorator or the :meth:`~flask.Flask.register_error_handler` method. A handler +can be registered for a status code, like 404, or for an exception class. -Please note that if you add an error handler for "500 Internal Server -Error", Flask will not trigger it if it's running in Debug mode. +The status code of the response will not be set to the handler's code. Make +sure to provide the appropriate HTTP status code when returning a response from +a handler. -Here an example implementation for a "404 Page Not Found" exception:: +A handler for "500 Internal Server Error" will not be used when running in +debug mode. Instead, the interactive debugger will be shown. + +Here is an example implementation for a "404 Page Not Found" exception:: from flask import render_template @app.errorhandler(404) def page_not_found(e): + # note that we set the 404 status explicitly return render_template('404.html'), 404 -And, using an application factory pattern (see :ref:`app-factories`):: +When using the :ref:`application factory pattern `:: from flask import Flask, render_template - + def page_not_found(e): return render_template('404.html'), 404 - + def create_app(config_filename): app = Flask(__name__) - # ... app.register_error_handler(404, page_not_found) + return app An example template might be this: .. sourcecode:: html+jinja - {% extends "layout.html" %} - {% block title %}Page Not Found{% endblock %} - {% block body %} -

Page Not Found

-

What you were looking for is just not there. -

go somewhere nice - {% endblock %} - + {% extends "layout.html" %} + {% block title %}Page Not Found{% endblock %} + {% block body %} +

Page Not Found

+

What you were looking for is just not there. +

go somewhere nice + {% endblock %} From 75b85656ddad86cbf0c3adb1128b758242793929 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 4 May 2017 18:24:04 -0700 Subject: [PATCH 061/131] optionally enable sphinxcontrib.log_cabinet collapses old changelog directives closes #1704 closes #1867 --- docs/conf.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/conf.py b/docs/conf.py index f53d72fa..47168c65 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -38,6 +38,14 @@ extensions = [ 'flaskdocext' ] +try: + __import__('sphinxcontrib.log_cabinet') +except ImportError: + print('sphinxcontrib-log-cabinet is not installed.') + print('Changelog directives will not be re-organized.') +else: + extensions.append('sphinxcontrib.log_cabinet') + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] From 160999e88241137c73aaa49e352dcbf2829fad5a Mon Sep 17 00:00:00 2001 From: wangbing Date: Wed, 10 May 2017 00:34:36 +0800 Subject: [PATCH 062/131] Removed unused import --- flask/json.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flask/json.py b/flask/json.py index bf8a8843..a029e73a 100644 --- a/flask/json.py +++ b/flask/json.py @@ -13,7 +13,6 @@ import uuid from datetime import date from .globals import current_app, request from ._compat import text_type, PY2 -from .ctx import has_request_context from werkzeug.http import http_date from jinja2 import Markup From dfb03c5673e97ee95289b844c13dbf506ea99e71 Mon Sep 17 00:00:00 2001 From: Xephyr826 Date: Wed, 10 May 2017 22:38:22 -0700 Subject: [PATCH 063/131] Improve Routing section Edited the entire section for clarity and concision. I rewrote sentences to make them shorter and to reduce ambiguity. Added a code sample to show the path converter type Removed the HTTP method overview. Although it was well written, the overview wasn't necessary in the quickstart. Readers can easily find an overview elsewhere. --- docs/quickstart.rst | 152 ++++++++++---------------------------------- 1 file changed, 34 insertions(+), 118 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 09365496..76f2af42 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -159,14 +159,9 @@ Have another debugger in mind? See :ref:`working-with-debuggers`. Routing ------- -Modern web applications have beautiful URLs. This helps people remember -the URLs, which is especially handy for applications that are used from -mobile devices with slower network connections. If the user can directly -go to the desired page without having to hit the index page it is more -likely they will like the page and come back next time. +Modern web applications use meaningful URLs to help users. Users are more likely to like a page and come back if the page uses a meaningful URL they can remember and use to directly visit a page. -As you have seen above, the :meth:`~flask.Flask.route` decorator is used to -bind a function to a URL. Here are some basic examples:: +As you saw in the minimal application, you can use the :meth:`~flask.Flask.route` decorator to bind a function to a meaningful URL. For example:: @app.route('/') def index(): @@ -176,16 +171,12 @@ bind a function to a URL. Here are some basic examples:: def hello(): return 'Hello, World' -But there is more to it! You can make certain parts of the URL dynamic and -attach multiple rules to a function. +You can do more! You can make parts of the URL dynamic and attach multiple rules to a function. Variable Rules `````````````` -To add variable parts to a URL you can mark these special sections as -````. Such a part is then passed as a keyword argument to your -function. Optionally a converter can be used by specifying a rule with -````. Here are some nice examples:: +You can add variable sections to a URL by marking sections with ````. Your function then receives the ```` as a keyword argument. Optionally, you can use a converter to specify a rule with ````. For example:: @app.route('/user/') def show_user_profile(username): @@ -197,22 +188,25 @@ function. Optionally a converter can be used by specifying a rule with # show the post with the given id, the id is an integer return 'Post %d' % post_id -The following converters exist: + @app.route('/path/') + def show_subpath(subpath): + # show the subpath after /path/ + return 'Subpath %s' % subpath + +Converter types: =========== =============================================== -`string` accepts any text without a slash (the default) +`string` (default) accepts any text without a slash `int` accepts integers -`float` like ``int`` but for floating point values -`path` like the default but also accepts slashes +`float` accepts floating point values +`path` like `string` but also accepts slashes `any` matches one of the items provided `uuid` accepts UUID strings =========== =============================================== .. admonition:: Unique URLs / Redirection Behavior - Flask's URL rules are based on Werkzeug's routing module. The idea - behind that module is to ensure beautiful and unique URLs based on - precedents laid down by Apache and earlier HTTP servers. + Flask's URL rules are based on Werkzeug's routing module. The routing module creates meaningful and unique URLs based on precedents laid down by Apache and earlier HTTP servers. Take these two rules:: @@ -224,15 +218,9 @@ The following converters exist: def about(): return 'The about page' - Though they look rather similar, they differ in their use of the trailing - slash in the URL *definition*. In the first case, the canonical URL for the - ``projects`` endpoint has a trailing slash. In that sense, it is similar to - a folder on a filesystem. Accessing it without a trailing slash will cause - Flask to redirect to the canonical URL with the trailing slash. + Though they look similar, they differ in their use of the trailing slash in the URL *definition*. In the first case, the canonical URL for the ``projects`` endpoint uses a trailing slash. It's similar to a folder in a file system, and if you access the URL without a trailing slash, Flask redirects you to the canonical URL with the trailing slash. - In the second case, however, the URL is defined without a trailing slash, - rather like the pathname of a file on UNIX-like systems. Accessing the URL - with a trailing slash will produce a 404 "Not Found" error. + In the second case, however, the URL definition lacks a trailing slash like the pathname of a file on UNIX-like systems. Accessing the URL with a trailing slash produces a 404 “Not Found” error. This behavior allows relative URLs to continue working even if the trailing slash is omitted, consistent with how Apache and other servers work. Also, @@ -245,12 +233,19 @@ The following converters exist: URL Building ```````````` -If it can match URLs, can Flask also generate them? Of course it can. To -build a URL to a specific function you can use the :func:`~flask.url_for` -function. It accepts the name of the function as first argument and a number -of keyword arguments, each corresponding to the variable part of the URL rule. -Unknown variable parts are appended to the URL as query parameters. Here are -some examples:: +Flask can match URLs, but can it also generate them? Yes! To build a URL to a specific function, you can use the :func:`~flask.url_for` function. It accepts the name of the function as its first argument and any number of keyword arguments, each corresponding to a variable part of the URL rule. Unknown variable parts are appended to the URL as query parameters. + +Why would you want to build URLs using the URL reversing function +:func:`~flask.url_for` instead of hard-coding them into your templates? Three good reasons: + +1. Reversing is often more descriptive than hard-coding the URLs. More importantly, you can change your URLs in one go instead of needing to remember to manually change hard-coded URLs. +2. URL building handles escaping of special characters and Unicode data transparently. +3. If your application is placed outside the URL root, for example, in ``/myapplication`` instead of ``/``, :func:`~flask.url_for` properly handles that for you. + + +For example, here we use the :meth:`~flask.Flask.test_request_context` method to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context` tells Flask to behave as though it's handling a request even while we use a Python shell. Also see, :ref:`context-locals`. + +Example:: >>> from flask import Flask, url_for >>> app = Flask(__name__) @@ -274,98 +269,19 @@ some examples:: /login?next=/ /user/John%20Doe -(This also uses the :meth:`~flask.Flask.test_request_context` method, explained -below. It tells Flask to behave as though it is handling a request, even -though we are interacting with it through a Python shell. Have a look at the -explanation below. :ref:`context-locals`). - -Why would you want to build URLs using the URL reversing function -:func:`~flask.url_for` instead of hard-coding them into your templates? -There are three good reasons for this: - -1. Reversing is often more descriptive than hard-coding the URLs. More - importantly, it allows you to change URLs in one go, without having to - remember to change URLs all over the place. -2. URL building will handle escaping of special characters and Unicode - data transparently for you, so you don't have to deal with them. -3. If your application is placed outside the URL root - say, in - ``/myapplication`` instead of ``/`` - :func:`~flask.url_for` will handle - that properly for you. - - HTTP Methods ```````````` -HTTP (the protocol web applications are speaking) knows different methods for -accessing URLs. By default, a route only answers to ``GET`` requests, but that -can be changed by providing the ``methods`` argument to the -:meth:`~flask.Flask.route` decorator. Here are some examples:: - - from flask import request +Web application communicate using HTTP and use different methods for accessing URLs. You should familiarize yourself with the HTTP methods as you work with Flask. By default, a route only answers to ``GET`` requests. You can use the ``methods`` argument of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods. For example:: @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': - return do_the_login() + do_the_login() else: - return show_the_login_form() - -If ``GET`` is present, ``HEAD`` will be added automatically for you. You -don't have to deal with that. It will also make sure that ``HEAD`` requests -are handled as the `HTTP RFC`_ (the document describing the HTTP -protocol) demands, so you can completely ignore that part of the HTTP -specification. Likewise, as of Flask 0.6, ``OPTIONS`` is implemented for you -automatically as well. - -You have no idea what an HTTP method is? Worry not, here is a quick -introduction to HTTP methods and why they matter: - -The HTTP method (also often called "the verb") tells the server what the -client wants to *do* with the requested page. The following methods are -very common: - -``GET`` - The browser tells the server to just *get* the information stored on - that page and send it. This is probably the most common method. - -``HEAD`` - The browser tells the server to get the information, but it is only - interested in the *headers*, not the content of the page. An - application is supposed to handle that as if a ``GET`` request was - received but to not deliver the actual content. In Flask you don't - have to deal with that at all, the underlying Werkzeug library handles - that for you. - -``POST`` - The browser tells the server that it wants to *post* some new - information to that URL and that the server must ensure the data is - stored and only stored once. This is how HTML forms usually - transmit data to the server. - -``PUT`` - Similar to ``POST`` but the server might trigger the store procedure - multiple times by overwriting the old values more than once. Now you - might be asking why this is useful, but there are some good reasons - to do it this way. Consider that the connection is lost during - transmission: in this situation a system between the browser and the - server might receive the request safely a second time without breaking - things. With ``POST`` that would not be possible because it must only - be triggered once. - -``DELETE`` - Remove the information at the given location. - -``OPTIONS`` - Provides a quick way for a client to figure out which methods are - supported by this URL. Starting with Flask 0.6, this is implemented - for you automatically. - -Now the interesting part is that in HTML4 and XHTML1, the only methods a -form can submit to the server are ``GET`` and ``POST``. But with JavaScript -and future HTML standards you can use the other methods as well. Furthermore -HTTP has become quite popular lately and browsers are no longer the only -clients that are using HTTP. For instance, many revision control systems -use it. + show_the_login_form() + +If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method and handles ``HEAD`` requests according to the the `HTTP RFC`_. Likewise, as of Flask 0.6, ``OPTIONS`` is automatically implemented for you. .. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt From a80eab70ba96d7d43c2ca5dc742597320aebe104 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 11 May 2017 12:35:03 -0700 Subject: [PATCH 064/131] Update CONTRIBUTING.rst basic pytest can't handle examples, pass tests dir --- CONTRIBUTING.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 66766512..852e44be 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -54,7 +54,7 @@ Install Flask as an editable package using the current source:: Then you can run the testsuite with:: - pytest + pytest tests/ With only pytest installed, a large part of the testsuite will get skipped though. Whether this is relevant depends on which part of Flask you're working From 1fb43e3be47ab24cc32396e28a6ad3734e9109e9 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 11 May 2017 13:54:37 -0700 Subject: [PATCH 065/131] rewrite installation docs discuss python version discuss all dependencies prefer python 3 in instructions [ci skip] --- docs/installation.rst | 231 ++++++++++++++++++++---------------------- 1 file changed, 108 insertions(+), 123 deletions(-) diff --git a/docs/installation.rst b/docs/installation.rst index 38094ded..cd869b9a 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -3,188 +3,173 @@ Installation ============ -Flask depends on some external libraries, like `Werkzeug -`_ and `Jinja2 `_. -Werkzeug is a toolkit for WSGI, the standard Python interface between web -applications and a variety of servers for both development and deployment. -Jinja2 renders templates. +Python Version +-------------- -So how do you get all that on your computer quickly? There are many ways you -could do that, but the most kick-ass method is virtualenv, so let's have a look -at that first. +We recommend using the latest version of Python 3. Flask supports Python 3.3 +and newer, Python 2.6 and newer, and PyPy. -You will need Python 2.6 or newer to get started, so be sure to have an -up-to-date Python 2.x installation. For using Flask with Python 3 have a -look at :ref:`python3-support`. +Dependencies +------------ -.. _virtualenv: +These distributions will be installed automatically when installing Flask. -virtualenv ----------- +* `Werkzeug`_ implements WSGI, the standard Python interface between + applications and servers. +* `Jinja`_ is a template language that renders the pages your application + serves. +* `MarkupSafe`_ comes with Jinja. It escapes untrusted input when rendering + templates to avoid injection attacks. +* `ItsDangerous`_ securely signs data to ensure its integrity. This is used + to protect Flask's session cookie. +* `Click`_ is a framework for writing command line applications. It provides + the ``flask`` command and allows adding custom management commands. -Virtualenv is probably what you want to use during development, and if you have -shell access to your production machines, you'll probably want to use it there, -too. +.. _Werkzeug: http://werkzeug.pocoo.org/ +.. _Jinja: http://jinja.pocoo.org/ +.. _MarkupSafe: https://pypi.python.org/pypi/MarkupSafe +.. _ItsDangerous: https://pythonhosted.org/itsdangerous/ +.. _Click: http://click.pocoo.org/ -What problem does virtualenv solve? If you like Python as much as I do, -chances are you want to use it for other projects besides Flask-based web -applications. But the more projects you have, the more likely it is that you -will be working with different versions of Python itself, or at least different -versions of Python libraries. Let's face it: quite often libraries break -backwards compatibility, and it's unlikely that any serious application will -have zero dependencies. So what do you do if two or more of your projects have -conflicting dependencies? +Optional dependencies +~~~~~~~~~~~~~~~~~~~~~ -Virtualenv to the rescue! Virtualenv enables multiple side-by-side -installations of Python, one for each project. It doesn't actually install -separate copies of Python, but it does provide a clever way to keep different -project environments isolated. Let's see how virtualenv works. +These distributions will not be installed automatically. Flask will detect and +use them if you install them. +* `Blinker`_ provides support for :ref:`signals`. +* `SimpleJSON`_ is a fast JSON implementation that is compatible with + Python's ``json`` module. It is preferred for JSON operations if it is + installed. -.. admonition:: A note on python3 and virtualenv +.. _Blinker: https://pythonhosted.org/blinker/ +.. _SimpleJSON: https://simplejson.readthedocs.io/ - If you are planning on using python3 with the virtualenv, you don't need to - install ``virtualenv``. Python3 has built-in support for virtual environments. +Virtual environments +-------------------- -If you are on Mac OS X or Linux, chances are that the following -command will work for you:: +Use a virtual environment to manage the dependencies for your project, both in +development and in production. - $ sudo pip install virtualenv +What problem does a virtual environment solve? The more Python projects you +have, the more likely it is that you need to work with different versions of +Python libraries, or even Python itself. Newer versions of libraries for one +project can break compatibility in another project. -It will probably install virtualenv on your system. Maybe it's even -in your package manager. If you use Ubuntu, try:: +Virtual environments are independent groups of Python libraries, one for each +project. Packages installed for one project will not affect other projects or +the operating system's packages. - $ sudo apt-get install python-virtualenv +Python 3 comes bundled with the :mod:`venv` module to create virtual +environments. If you're using a modern version of Python, you can continue on +to the next section. -If you are on Windows and don't have the ``easy_install`` command, you must -install it first. Check the :ref:`windows-easy-install` section for more -information about how to do that. Once you have it installed, run the same -commands as above, but without the ``sudo`` prefix. +If you're using Python 2, see :ref:`install-install-virtualenv` first. -Creating a virtual environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. _install-create-env: -Once you have virtualenv installed, just fire up a shell and create -your own environment. I usually create a project folder and a :file:`virtenv` -folder within:: +Create an environment +~~~~~~~~~~~~~~~~~~~~~ - $ mkdir myproject - $ cd myproject +Create a project folder and a :file:`venv` folder within: -There is a little change in how you create a virtualenv depending on which python-version you are currently using. +.. code-block:: sh -**Python2** + mkdir myproject + cd myproject + python3 -m venv venv -:: +On Windows: - $ virtualenv virtenv - New python executable in virtenv/bin/python - Installing setuptools, pip............done. +.. code-block:: bat -**Python 3.6 and above** + py -3 -m venv venv -:: +If you needed to install virtualenv because you are on an older version of +Python, use the following command instead: - $ python3 -m venv virtenv +.. code-block:: sh -Activating a virtual environment -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + virtualenv venv -Now, whenever you want to work on a project, you only have to activate the -corresponding environment. On OS X and Linux, do the following:: +On Windows: - $ . virtenv/bin/activate +.. code-block:: bat -If you are a Windows user, the following command is for you:: + \Python27\Scripts\virtualenv.exe venv - $ virtenv\Scripts\activate +Activate the environment +~~~~~~~~~~~~~~~~~~~~~~~~ -Either way, you should now be using your virtualenv (notice how the prompt of -your shell has changed to show the active environment). +Before you work on your project, activate the corresponding environment: -And if you want to go back to the real world, use the following command:: +.. code-block:: sh - $ deactivate + . venv/bin/activate -After doing this, the prompt of your shell should be as familiar as before. +On Windows: -Now, let's move on. Enter the following command to get Flask activated in your -virtualenv:: +.. code-block:: bat - $ pip install Flask + venv\Scripts\activate -A few seconds later and you are good to go. +Your shell prompt will change to show the name of the activated environment. +Install Flask +------------- -System-Wide Installation ------------------------- +Within the activated environment, use the following command to install Flask: -This is possible as well, though I do not recommend it. Just run -``pip`` with root privileges:: +.. code-block:: sh - $ sudo pip install Flask + pip install Flask -(On Windows systems, run it in a command-prompt window with administrator -privileges, and leave out ``sudo``.) +Living on the edge +~~~~~~~~~~~~~~~~~~ +If you want to work with the latest Flask code before it's released, install or +update the code from the master branch: -Living on the Edge ------------------- +.. code-block:: sh -If you want to work with the latest version of Flask, there are two ways: you -can either let ``pip`` pull in the development version, or you can tell -it to operate on a git checkout. Either way, virtualenv is recommended. + pip install -U https://github.com/pallets/flask/archive/master.tar.gz -Get the git checkout in a new virtualenv and run in development mode:: +.. _install-install-virtualenv: - $ git clone https://github.com/pallets/flask.git - Initialized empty Git repository in ~/dev/flask/.git/ - $ cd flask - $ virtualenv virtenv - New python executable in virtenv/bin/python - Installing setuptools, pip............done. - $ . virtenv/bin/activate - $ python setup.py develop - ... - Finished processing dependencies for Flask +Install virtualenv +------------------ -This will pull in the dependencies and activate the git head as the current -version inside the virtualenv. Then all you have to do is run ``git pull -origin`` to update to the latest version. +If you are using Python 2, the venv module is not available. Instead, +install `virtualenv`_. -.. _windows-easy-install: +On Linux, virtualenv is provided by your package manager: -`pip` and `setuptools` on Windows ---------------------------------- +.. code-block:: sh -Sometimes getting the standard "Python packaging tools" like ``pip``, ``setuptools`` -and ``virtualenv`` can be a little trickier, but nothing very hard. The crucial -package you will need is pip - this will let you install -anything else (like virtualenv). Fortunately there is a "bootstrap script" -you can run to install. + # Debian, Ubuntu + sudo apt-get install python-virtualenv -If you don't currently have ``pip``, then `get-pip.py` will install it for you. + # CentOS, Fedora + sudo yum install python-virtualenv -`get-pip.py`_ + # Arch + sudo pacman -S python-virtualenv -It should be double-clickable once you download it. If you already have ``pip``, -you can upgrade them by running:: +If you are on Mac OS X or Windows, download `get-pip.py`_, then: - > pip install --upgrade pip setuptools +.. code-block:: sh -Most often, once you pull up a command prompt you want to be able to type ``pip`` -and ``python`` which will run those things, but this might not automatically happen -on Windows, because it doesn't know where those executables are (give either a try!). + sudo python2 Downloads/get-pip.py + sudo python2 -m pip install virtualenv -To fix this, you should be able to navigate to your Python install directory -(e.g :file:`C:\Python27`), then go to :file:`Tools`, then :file:`Scripts`, then find the -:file:`win_add2path.py` file and run that. Open a **new** Command Prompt and -check that you can now just type ``python`` to bring up the interpreter. +On Windows, as an administrator: -Finally, to install `virtualenv`_, you can simply run:: +.. code-block:: bat - > pip install virtualenv + \Python27\python.exe Downloads\get-pip.py + \Python27\python.exe -m pip install virtualenv -Then you can be off on your way following the installation instructions above. +Now you can continue to :ref:`install-create-env`. +.. _virtualenv: https://virtualenv.pypa.io/ .. _get-pip.py: https://bootstrap.pypa.io/get-pip.py From 2592f927a00f39ca089f67144a1e7f200ad213d1 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 11 May 2017 22:31:19 -0700 Subject: [PATCH 066/131] wrap lines tighten up wording remove any converter from quickstart use correct rst code syntax --- docs/quickstart.rst | 148 ++++++++++++++++++++++++++------------------ 1 file changed, 88 insertions(+), 60 deletions(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 76f2af42..7bdb67e4 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -159,9 +159,11 @@ Have another debugger in mind? See :ref:`working-with-debuggers`. Routing ------- -Modern web applications use meaningful URLs to help users. Users are more likely to like a page and come back if the page uses a meaningful URL they can remember and use to directly visit a page. +Modern web applications use meaningful URLs to help users. Users are more +likely to like a page and come back if the page uses a meaningful URL they can +remember and use to directly visit a page. -As you saw in the minimal application, you can use the :meth:`~flask.Flask.route` decorator to bind a function to a meaningful URL. For example:: +Use the :meth:`~flask.Flask.route` decorator to bind a function to a URL. :: @app.route('/') def index(): @@ -171,12 +173,16 @@ As you saw in the minimal application, you can use the :meth:`~flask.Flask.route def hello(): return 'Hello, World' -You can do more! You can make parts of the URL dynamic and attach multiple rules to a function. +You can do more! You can make parts of the URL dynamic and attach multiple +rules to a function. Variable Rules `````````````` -You can add variable sections to a URL by marking sections with ````. Your function then receives the ```` as a keyword argument. Optionally, you can use a converter to specify a rule with ````. For example:: +You can add variable sections to a URL by marking sections with +````. Your function then receives the ```` +as a keyword argument. Optionally, you can use a converter to specify the type +of the argument like ````. :: @app.route('/user/') def show_user_profile(username): @@ -195,75 +201,91 @@ You can add variable sections to a URL by marking sections with ``>> from flask import Flask, url_for - >>> app = Flask(__name__) - >>> @app.route('/') - ... def index(): pass - ... - >>> @app.route('/login') - ... def login(): pass - ... - >>> @app.route('/user/') - ... def profile(username): pass - ... - >>> with app.test_request_context(): - ... print(url_for('index')) - ... print(url_for('login')) - ... print(url_for('login', next='/')) - ... print(url_for('profile', username='John Doe')) - ... +:func:`~flask.url_for` instead of hard-coding them into your templates? + +1. Reversing is often more descriptive than hard-coding the URLs. +2. You can change your URLs in one go instead of needing to remember to + manually change hard-coded URLs. +3. URL building handles escaping of special characters and Unicode data + transparently. +4. If your application is placed outside the URL root, for example, in + ``/myapplication`` instead of ``/``, :func:`~flask.url_for` properly + handles that for you. + +For example, here we use the :meth:`~flask.Flask.test_request_context` method +to try out :func:`~flask.url_for`. :meth:`~flask.Flask.test_request_context` +tells Flask to behave as though it's handling a request even while we use a +Python shell. See :ref:`context-locals`. :: + + from flask import Flask, url_for + + app = Flask(__name__) + + @app.route('/') + def index(): + return 'index' + + @app.route('/login') + def login(): + return 'login' + + @app.route('/user/') + def profile(username): + return '{}'s profile'.format(username) + + with app.test_request_context(): + print(url_for('index')) + print(url_for('login')) + print(url_for('login', next='/')) + print(url_for('profile', username='John Doe')) + / /login /login?next=/ @@ -272,7 +294,11 @@ Example:: HTTP Methods ```````````` -Web application communicate using HTTP and use different methods for accessing URLs. You should familiarize yourself with the HTTP methods as you work with Flask. By default, a route only answers to ``GET`` requests. You can use the ``methods`` argument of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods. For example:: +Web applications use different HTTP methods when accessing URLs. You should +familiarize yourself with the HTTP methods as you work with Flask. By default, +a route only answers to ``GET`` requests. You can use the ``methods`` argument +of the :meth:`~flask.Flask.route` decorator to handle different HTTP methods. +:: @app.route('/login', methods=['GET', 'POST']) def login(): @@ -281,7 +307,9 @@ Web application communicate using HTTP and use different methods for accessing U else: show_the_login_form() -If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method and handles ``HEAD`` requests according to the the `HTTP RFC`_. Likewise, as of Flask 0.6, ``OPTIONS`` is automatically implemented for you. +If ``GET`` is present, Flask automatically adds support for the ``HEAD`` method +and handles ``HEAD`` requests according to the the `HTTP RFC`_. Likewise, +``OPTIONS`` is automatically implemented for you. .. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt From cc59f2b2046fdc5ef280b95aaa22c26c3e0fba46 Mon Sep 17 00:00:00 2001 From: David Lord Date: Thu, 11 May 2017 22:48:21 -0700 Subject: [PATCH 067/131] clean up deferred callback doc remove doc about writing after_this_request [ci skip] --- docs/patterns/deferredcallbacks.rst | 80 ++++++++++------------------- 1 file changed, 26 insertions(+), 54 deletions(-) diff --git a/docs/patterns/deferredcallbacks.rst b/docs/patterns/deferredcallbacks.rst index a79dc913..bc20cdd6 100644 --- a/docs/patterns/deferredcallbacks.rst +++ b/docs/patterns/deferredcallbacks.rst @@ -3,71 +3,43 @@ Deferred Request Callbacks ========================== -One of the design principles of Flask is that response objects are created -and passed down a chain of potential callbacks that can modify them or -replace them. When the request handling starts, there is no response -object yet. It is created as necessary either by a view function or by -some other component in the system. - -But what happens if you want to modify the response at a point where the -response does not exist yet? A common example for that would be a -before-request function that wants to set a cookie on the response object. - -One way is to avoid the situation. Very often that is possible. For -instance you can try to move that logic into an after-request callback -instead. Sometimes however moving that code there is just not a very -pleasant experience or makes code look very awkward. - -As an alternative possibility you can attach a bunch of callback functions -to the :data:`~flask.g` object and call them at the end of the request. -This way you can defer code execution from anywhere in the application. - - -The Decorator -------------- - -The following decorator is the key. It registers a function on a list on -the :data:`~flask.g` object:: - - from flask import g - - def after_this_request(f): - if not hasattr(g, 'after_request_callbacks'): - g.after_request_callbacks = [] - g.after_request_callbacks.append(f) - return f - - -Calling the Deferred --------------------- - -Now you can use the `after_this_request` decorator to mark a function to -be called at the end of the request. But we still need to call them. For -this the following function needs to be registered as -:meth:`~flask.Flask.after_request` callback:: - - @app.after_request - def call_after_request_callbacks(response): - for callback in getattr(g, 'after_request_callbacks', ()): - callback(response) - return response - - -A Practical Example -------------------- +One of the design principles of Flask is that response objects are created and +passed down a chain of potential callbacks that can modify them or replace +them. When the request handling starts, there is no response object yet. It is +created as necessary either by a view function or by some other component in +the system. + +What happens if you want to modify the response at a point where the response +does not exist yet? A common example for that would be a +:meth:`~flask.Flask.before_request` callback that wants to set a cookie on the +response object. + +One way is to avoid the situation. Very often that is possible. For instance +you can try to move that logic into a :meth:`~flask.Flask.after_request` +callback instead. However, sometimes moving code there makes it more +more complicated or awkward to reason about. + +As an alternative, you can use :func:`~flask.after_this_request` to register +callbacks that will execute after only the current request. This way you can +defer code execution from anywhere in the application, based on the current +request. At any time during a request, we can register a function to be called at the -end of the request. For example you can remember the current language of the -user in a cookie in the before-request function:: +end of the request. For example you can remember the current language of the +user in a cookie in a :meth:`~flask.Flask.before_request` callback:: from flask import request, after_this_request @app.before_request def detect_user_language(): language = request.cookies.get('user_lang') + if language is None: language = guess_language_from_request() + + # when the response exists, set a cookie with the language @after_this_request def remember_language(response): response.set_cookie('user_lang', language) + g.language = language From c3d49e29ea42d2f468bc780c15683ca197b50d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Oliveira?= Date: Sun, 4 Dec 2016 18:29:11 +0000 Subject: [PATCH 068/131] show warning if session cookie domain is ip closes #2007 --- flask/helpers.py | 20 ++++++++++++++++++++ flask/sessions.py | 6 +++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/flask/helpers.py b/flask/helpers.py index 828f5840..4794ef97 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -976,3 +976,23 @@ def total_seconds(td): :rtype: int """ return td.days * 60 * 60 * 24 + td.seconds + +def is_ip(ip): + """Returns the if the string received is an IP or not. + + :param string: the string to check if it an IP or not + :param var_name: the name of the string that is being checked + + :returns: True if string is an IP, False if not + :rtype: boolean + """ + import socket + + for family in (socket.AF_INET, socket.AF_INET6): + try: + socket.inet_pton(family, ip) + except socket.error: + pass + else: + return True + return False diff --git a/flask/sessions.py b/flask/sessions.py index 525ff246..f1729674 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -11,13 +11,14 @@ import uuid import hashlib +from warnings import warn from base64 import b64encode, b64decode from datetime import datetime from werkzeug.http import http_date, parse_date from werkzeug.datastructures import CallbackDict from . import Markup, json from ._compat import iteritems, text_type -from .helpers import total_seconds +from .helpers import total_seconds, is_ip from itsdangerous import URLSafeTimedSerializer, BadSignature @@ -336,6 +337,9 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): domain = self.get_cookie_domain(app) + if domain is not None: + if is_ip(domain): + warnings.warn("IP introduced in SESSION_COOKIE_DOMAIN", RuntimeWarning) path = self.get_cookie_path(app) # Delete case. If there is no session we bail early. From f75ad9fca2e17c4777a3d7efc65f3ccab261e22c Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 13 May 2017 21:31:46 -0700 Subject: [PATCH 069/131] refactor session cookie domain logic cache result of session cookie domain add warnings for session cookie domain issues add changelog --- CHANGES | 7 ++++ flask/helpers.py | 18 +++++----- flask/sessions.py | 83 ++++++++++++++++++++++++++++++--------------- tests/test_basic.py | 36 ++++++++++++++++++++ 4 files changed, 109 insertions(+), 35 deletions(-) diff --git a/CHANGES b/CHANGES index ddab541f..a6eceb87 100644 --- a/CHANGES +++ b/CHANGES @@ -34,6 +34,12 @@ Major release, unreleased type is invalid. (`#2256`_) - Add ``routes`` CLI command to output routes registered on the application. (`#2259`_) +- Show warning when session cookie domain is a bare hostname or an IP + address, as these may not behave properly in some browsers, such as Chrome. + (`#2282`_) +- Allow IP address as exact session cookie domain. (`#2282`_) +- ``SESSION_COOKIE_DOMAIN`` is set if it is detected through ``SERVER_NAME``. + (`#2282`_) .. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1898: https://github.com/pallets/flask/pull/1898 @@ -43,6 +49,7 @@ Major release, unreleased .. _#2254: https://github.com/pallets/flask/pull/2254 .. _#2256: https://github.com/pallets/flask/pull/2256 .. _#2259: https://github.com/pallets/flask/pull/2259 +.. _#2282: https://github.com/pallets/flask/pull/2282 Version 0.12.1 -------------- diff --git a/flask/helpers.py b/flask/helpers.py index 4794ef97..51c3b064 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -10,6 +10,7 @@ """ import os +import socket import sys import pkgutil import posixpath @@ -977,22 +978,23 @@ def total_seconds(td): """ return td.days * 60 * 60 * 24 + td.seconds -def is_ip(ip): - """Returns the if the string received is an IP or not. - :param string: the string to check if it an IP or not - :param var_name: the name of the string that is being checked +def is_ip(value): + """Determine if the given string is an IP address. - :returns: True if string is an IP, False if not - :rtype: boolean + :param value: value to check + :type value: str + + :return: True if string is an IP address + :rtype: bool """ - import socket for family in (socket.AF_INET, socket.AF_INET6): try: - socket.inet_pton(family, ip) + socket.inet_pton(family, value) except socket.error: pass else: return True + return False diff --git a/flask/sessions.py b/flask/sessions.py index f1729674..9fef6a9d 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -11,7 +11,7 @@ import uuid import hashlib -from warnings import warn +import warnings from base64 import b64encode, b64decode from datetime import datetime from werkzeug.http import http_date, parse_date @@ -201,30 +201,62 @@ class SessionInterface(object): return isinstance(obj, self.null_session_class) 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. + """Returns the domain that should be set for the session cookie. + + Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise + falls back to detecting the domain based on ``SERVER_NAME``. + + Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is + updated to avoid re-running the logic. """ - if app.config['SESSION_COOKIE_DOMAIN'] is not None: - return app.config['SESSION_COOKIE_DOMAIN'] - if app.config['SERVER_NAME'] is not None: - # chop off the port which is usually not supported by browsers - rv = '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0] - - # Google chrome does not like cookies set to .localhost, so - # we just go with no domain then. Flask documents anyways that - # cross domain cookies need a fully qualified domain name - if rv == '.localhost': - rv = None - - # If we infer the cookie domain from the server name we need - # to check if we are in a subpath. In that case we can't - # set a cross domain cookie. - if rv is not None: - path = self.get_cookie_path(app) - if path != '/': - rv = rv.lstrip('.') - - return rv + + rv = app.config['SESSION_COOKIE_DOMAIN'] + + # set explicitly, or cached from SERVER_NAME detection + # if False, return None + if rv is not None: + return rv if rv else None + + rv = app.config['SERVER_NAME'] + + # server name not set, cache False to return none next time + if not rv: + app.config['SESSION_COOKIE_DOMAIN'] = False + return None + + # chop off the port which is usually not supported by browsers + # remove any leading '.' since we'll add that later + rv = rv.rsplit(':', 1)[0].lstrip('.') + + if '.' not in rv: + # Chrome doesn't allow names without a '.' + # this should only come up with localhost + # hack around this by not setting the name, and show a warning + warnings.warn( + '"{rv}" is not a valid cookie domain, it must contain a ".".' + ' Add an entry to your hosts file, for example' + ' "{rv}.localdomain", and use that instead.'.format(rv=rv) + ) + app.config['SESSION_COOKIE_DOMAIN'] = False + return None + + ip = is_ip(rv) + + if ip: + warnings.warn( + 'The session cookie domain is an IP address. This may not work' + ' as intended in some browsers. Add an entry to your hosts' + ' file, for example "localhost.localdomain", and use that' + ' instead.' + ) + + # if this is not an ip and app is mounted at the root, allow subdomain + # matching by adding a '.' prefix + if self.get_cookie_path(app) == '/' and not ip: + rv = '.' + rv + + app.config['SESSION_COOKIE_DOMAIN'] = rv + return rv def get_cookie_path(self, app): """Returns the path for which the cookie should be valid. The @@ -337,9 +369,6 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): domain = self.get_cookie_domain(app) - if domain is not None: - if is_ip(domain): - warnings.warn("IP introduced in SESSION_COOKIE_DOMAIN", RuntimeWarning) path = self.get_cookie_path(app) # Delete case. If there is no session we bail early. diff --git a/tests/test_basic.py b/tests/test_basic.py index 163b83cf..80091bd6 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -351,6 +351,42 @@ def test_session_using_session_settings(): assert 'httponly' not in cookie +def test_session_localhost_warning(recwarn): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='testing', + SERVER_NAME='localhost:5000', + ) + + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'testing' + + rv = app.test_client().get('/', 'http://localhost:5000/') + assert 'domain' not in rv.headers['set-cookie'].lower() + w = recwarn.pop(UserWarning) + assert '"localhost" is not a valid cookie domain' in str(w.message) + + +def test_session_ip_warning(recwarn): + app = flask.Flask(__name__) + app.config.update( + SECRET_KEY='testing', + SERVER_NAME='127.0.0.1:5000', + ) + + @app.route('/') + def index(): + flask.session['testing'] = 42 + return 'testing' + + rv = app.test_client().get('/', 'http://127.0.0.1:5000/') + assert 'domain=127.0.0.1' in rv.headers['set-cookie'].lower() + w = recwarn.pop(UserWarning) + assert 'cookie domain is an IP' in str(w.message) + + def test_missing_session(): app = flask.Flask(__name__) From d5a88bf0d36a2648cfc224884a144df7870894ab Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 15 May 2017 12:40:09 -0700 Subject: [PATCH 070/131] explain when to use a task queue remove deprecated abstract attr from celery add explanation of example task [ci skip] --- docs/patterns/celery.rst | 78 ++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst index 548da29b..c3098a9e 100644 --- a/docs/patterns/celery.rst +++ b/docs/patterns/celery.rst @@ -1,24 +1,27 @@ -Celery Based Background Tasks -============================= +Celery Background Tasks +======================= -Celery is a task queue for Python with batteries included. It used to -have a Flask integration but it became unnecessary after some -restructuring of the internals of Celery with Version 3. This guide fills -in the blanks in how to properly use Celery with Flask but assumes that -you generally already read the `First Steps with Celery -`_ -guide in the official Celery documentation. +If your application has a long running task, such as processing some uploaded +data or sending email, you don't want to wait for it to finish during a +request. Instead, use a task queue to send the necessary data to another +process that will run the task in the background while the request returns +immediately. -Installing Celery ------------------ +Celery is a powerful task queue that can be used for simple background tasks +as well as complex multi-stage programs and schedules. This guide will show you +how to configure Celery using Flask, but assumes you've already read the +`First Steps with Celery `_ +guide in the Celery documentation. -Celery is on the Python Package Index (PyPI), so it can be installed with -standard Python tools like :command:`pip` or :command:`easy_install`:: +Install +------- + +Celery is a separate Python package. Install it from PyPI using pip:: $ pip install celery -Configuring Celery ------------------- +Configure +--------- The first thing you need is a Celery instance, this is called the celery application. It serves the same purpose as the :class:`~flask.Flask` @@ -36,15 +39,18 @@ This is all that is necessary to properly integrate Celery with Flask:: from celery import Celery def make_celery(app): - celery = Celery(app.import_name, backend=app.config['CELERY_RESULT_BACKEND'], - broker=app.config['CELERY_BROKER_URL']) + celery = Celery( + app.import_name, + backend=app.config['CELERY_RESULT_BACKEND'], + broker=app.config['CELERY_BROKER_URL'] + ) celery.conf.update(app.config) - TaskBase = celery.Task - class ContextTask(TaskBase): - abstract = True + + class ContextTask(celery.Task): def __call__(self, *args, **kwargs): with app.app_context(): return self.run(*args, **kwargs) + celery.Task = ContextTask return celery @@ -53,11 +59,12 @@ from the application config, updates the rest of the Celery config from the Flask config and then creates a subclass of the task that wraps the task execution in an application context. -Minimal Example +An example task --------------- -With what we have above this is the minimal example of using Celery with -Flask:: +Let's write a task that adds two numbers together and returns the result. We +configure Celery's broker and backend to use Redis, create a ``celery`` +application using the factor from above, and then use it to define the task. :: from flask import Flask @@ -68,26 +75,27 @@ Flask:: ) celery = make_celery(flask_app) - @celery.task() def add_together(a, b): return a + b -This task can now be called in the background: +This task can now be called in the background:: ->>> result = add_together.delay(23, 42) ->>> result.wait() -65 + result = add_together.delay(23, 42) + result.wait() # 65 -Running the Celery Worker -------------------------- +Run a worker +------------ -Now if you jumped in and already executed the above code you will be -disappointed to learn that your ``.wait()`` will never actually return. -That's because you also need to run celery. You can do that by running -celery as a worker:: +If you jumped in and already executed the above code you will be +disappointed to learn that ``.wait()`` will never actually return. +That's because you also need to run a Celery worker to receive and execute the +task. :: $ celery -A your_application.celery worker The ``your_application`` string has to point to your application's package -or module that creates the `celery` object. +or module that creates the ``celery`` object. + +Now that the worker is running, ``wait`` will return the result once the task +is finished. From 2a6579430649d6514384ca592730c6995e140c43 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 15 May 2017 16:58:01 -0700 Subject: [PATCH 071/131] safe_join on Windows uses posixpath fixes #2033 closes #2059 --- flask/helpers.py | 22 ++++++++++++++-------- tests/test_helpers.py | 25 ++++++++++++------------- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/flask/helpers.py b/flask/helpers.py index 51c3b064..f37be677 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -638,18 +638,24 @@ def safe_join(directory, *pathnames): :raises: :class:`~werkzeug.exceptions.NotFound` if one or more passed paths fall out of its boundaries. """ + + parts = [directory] + for filename in pathnames: if filename != '': filename = posixpath.normpath(filename) - for sep in _os_alt_seps: - if sep in filename: - raise NotFound() - if os.path.isabs(filename) or \ - filename == '..' or \ - filename.startswith('../'): + + if ( + any(sep in filename for sep in _os_alt_seps) + or os.path.isabs(filename) + or filename == '..' + or filename.startswith('../') + ): raise NotFound() - directory = os.path.join(directory, filename) - return directory + + parts.append(filename) + + return posixpath.join(*parts) def send_from_directory(directory, filename, **options): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 325713c0..d5706521 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -903,21 +903,20 @@ class TestStreaming(object): class TestSafeJoin(object): - def test_safe_join(self): # Valid combinations of *args and expected joined paths. passing = ( - (('a/b/c', ), 'a/b/c'), - (('/', 'a/', 'b/', 'c/', ), '/a/b/c'), - (('a', 'b', 'c', ), 'a/b/c'), - (('/a', 'b/c', ), '/a/b/c'), - (('a/b', 'X/../c'), 'a/b/c', ), - (('/a/b', 'c/X/..'), '/a/b/c', ), + (('a/b/c',), 'a/b/c'), + (('/', 'a/', 'b/', 'c/'), '/a/b/c'), + (('a', 'b', 'c'), 'a/b/c'), + (('/a', 'b/c'), '/a/b/c'), + (('a/b', 'X/../c'), 'a/b/c'), + (('/a/b', 'c/X/..'), '/a/b/c'), # If last path is '' add a slash - (('/a/b/c', '', ), '/a/b/c/', ), + (('/a/b/c', ''), '/a/b/c/'), # Preserve dot slash - (('/a/b/c', './', ), '/a/b/c/.', ), - (('a/b/c', 'X/..'), 'a/b/c/.', ), + (('/a/b/c', './'), '/a/b/c/.'), + (('a/b/c', 'X/..'), 'a/b/c/.'), # Base directory is always considered safe (('../', 'a/b/c'), '../a/b/c'), (('/..', ), '/..'), @@ -931,12 +930,12 @@ class TestSafeJoin(object): failing = ( # path.isabs and ``..'' checks ('/a', 'b', '/c'), - ('/a', '../b/c', ), + ('/a', '../b/c'), ('/a', '..', 'b/c'), # Boundaries violations after path normalization - ('/a', 'b/../b/../../c', ), + ('/a', 'b/../b/../../c'), ('/a', 'b', 'c/../..'), - ('/a', 'b/../../c', ), + ('/a', 'b/../../c'), ) for args in failing: From f7c35bf0d51d1ae02709e39fe29110e12f64fb87 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 15 May 2017 16:58:01 -0700 Subject: [PATCH 072/131] safe_join on Windows uses posixpath fixes #2033 closes #2059 --- CHANGES | 7 +++++++ flask/helpers.py | 22 ++++++++++++++-------- tests/test_helpers.py | 25 ++++++++++++------------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/CHANGES b/CHANGES index 613b8189..a67c8bf9 100644 --- a/CHANGES +++ b/CHANGES @@ -15,6 +15,13 @@ Major release, unreleased method returns compressed response by default, and pretty response in debug mode. +Version 0.12.2 +-------------- + +Bugfix release + +- Fix a bug in `safe_join` on Windows. + Version 0.12.1 -------------- diff --git a/flask/helpers.py b/flask/helpers.py index c6c2cddc..4bb1d1c9 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -619,18 +619,24 @@ def safe_join(directory, *pathnames): :raises: :class:`~werkzeug.exceptions.NotFound` if one or more passed paths fall out of its boundaries. """ + + parts = [directory] + for filename in pathnames: if filename != '': filename = posixpath.normpath(filename) - for sep in _os_alt_seps: - if sep in filename: - raise NotFound() - if os.path.isabs(filename) or \ - filename == '..' or \ - filename.startswith('../'): + + if ( + any(sep in filename for sep in _os_alt_seps) + or os.path.isabs(filename) + or filename == '..' + or filename.startswith('../') + ): raise NotFound() - directory = os.path.join(directory, filename) - return directory + + parts.append(filename) + + return posixpath.join(*parts) def send_from_directory(directory, filename, **options): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index b1241ce9..9320ef71 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -846,21 +846,20 @@ class TestStreaming(object): class TestSafeJoin(object): - def test_safe_join(self): # Valid combinations of *args and expected joined paths. passing = ( - (('a/b/c', ), 'a/b/c'), - (('/', 'a/', 'b/', 'c/', ), '/a/b/c'), - (('a', 'b', 'c', ), 'a/b/c'), - (('/a', 'b/c', ), '/a/b/c'), - (('a/b', 'X/../c'), 'a/b/c', ), - (('/a/b', 'c/X/..'), '/a/b/c', ), + (('a/b/c',), 'a/b/c'), + (('/', 'a/', 'b/', 'c/'), '/a/b/c'), + (('a', 'b', 'c'), 'a/b/c'), + (('/a', 'b/c'), '/a/b/c'), + (('a/b', 'X/../c'), 'a/b/c'), + (('/a/b', 'c/X/..'), '/a/b/c'), # If last path is '' add a slash - (('/a/b/c', '', ), '/a/b/c/', ), + (('/a/b/c', ''), '/a/b/c/'), # Preserve dot slash - (('/a/b/c', './', ), '/a/b/c/.', ), - (('a/b/c', 'X/..'), 'a/b/c/.', ), + (('/a/b/c', './'), '/a/b/c/.'), + (('a/b/c', 'X/..'), 'a/b/c/.'), # Base directory is always considered safe (('../', 'a/b/c'), '../a/b/c'), (('/..', ), '/..'), @@ -874,12 +873,12 @@ class TestSafeJoin(object): failing = ( # path.isabs and ``..'' checks ('/a', 'b', '/c'), - ('/a', '../b/c', ), + ('/a', '../b/c'), ('/a', '..', 'b/c'), # Boundaries violations after path normalization - ('/a', 'b/../b/../../c', ), + ('/a', 'b/../b/../../c'), ('/a', 'b', 'c/../..'), - ('/a', 'b/../../c', ), + ('/a', 'b/../../c'), ) for args in failing: From bb83ae9843fd1e170a97139d489399c4823ba779 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 16 May 2017 08:39:28 +0200 Subject: [PATCH 073/131] Release 0.12.2 --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index a67c8bf9..3456276a 100644 --- a/CHANGES +++ b/CHANGES @@ -18,7 +18,7 @@ Major release, unreleased Version 0.12.2 -------------- -Bugfix release +Released on May 16 2017 - Fix a bug in `safe_join` on Windows. From 571334df8e26333f34873a3dcb84441946e6c64c Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 16 May 2017 08:39:30 +0200 Subject: [PATCH 074/131] Bump version number to 0.12.2 --- flask/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/__init__.py b/flask/__init__.py index 59d711b8..a9a873fa 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.12.2-dev' +__version__ = '0.12.2' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. From f347d3c59e3304474ca85b17e24261f127b27282 Mon Sep 17 00:00:00 2001 From: Markus Unterwaditzer Date: Tue, 16 May 2017 08:40:08 +0200 Subject: [PATCH 075/131] Bump to dev version --- flask/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flask/__init__.py b/flask/__init__.py index a9a873fa..28a87cb6 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.12.2' +__version__ = '0.12.3-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. From ae133aa1734775f03560bde3d98c7f7d1c1569f7 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 20 May 2017 12:11:37 -0700 Subject: [PATCH 076/131] reorder session cookie checks to deleted, accessed, modified --- flask/sessions.py | 75 +++++++++++++++++++++++------------------------ 1 file changed, 37 insertions(+), 38 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index 2eb887fc..182ace85 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -125,7 +125,8 @@ class SecureCookieSession(CallbackDict, SessionMixin): def on_update(self): self.modified = True self.accessed = True - CallbackDict.__init__(self, initial, on_update) + + super(SecureCookieSession, self).__init__(initial, on_update) self.modified = False self.accessed = False @@ -306,22 +307,20 @@ class SessionInterface(object): return datetime.utcnow() + app.permanent_session_lifetime def should_set_cookie(self, app, session): - """Indicates whether a cookie should be set now or not. This is - used by session backends to figure out if they should emit a - set-cookie header or not. The default behavior is controlled by - the ``SESSION_REFRESH_EACH_REQUEST`` config variable. If - it's set to ``False`` then a cookie is only set if the session is - modified, if set to ``True`` it's always set if the session is - permanent. - - This check is usually skipped if sessions get deleted. + """Used by session backends to determine if a ``Set-Cookie`` header + should be set for this session cookie for this response. If the session + has been modified, the cookie is set. If the session is permanent and + the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is + always set. + + This check is usually skipped if the session was deleted. .. versionadded:: 0.11 """ - if session.modified: - return True - save_each = app.config['SESSION_REFRESH_EACH_REQUEST'] - return save_each and session.permanent + + return session.modified or ( + session.permanent and app.config['SESSION_REFRESH_EACH_REQUEST'] + ) def open_session(self, app, request): """This method has to be implemented and must either return ``None`` @@ -387,35 +386,35 @@ class SecureCookieSessionInterface(SessionInterface): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) - if session.accessed: + # If the session is modified to be empty, remove the cookie. + # If the session is empty, return without setting the cookie. + if not session: + if session.modified: + response.delete_cookie( + app.session_cookie_name, + domain=domain, + path=path + ) + + return + # Add a "Vary: Cookie" header if the session was accessed at all. + if session.accessed: response.headers.add('Vary', 'Cookie') - else: - - # Delete case. If there is no session we bail early. - # If the session was modified to be empty we remove the - # whole cookie. - if not session: - if session.modified: - response.delete_cookie(app.session_cookie_name, - domain=domain, path=path) - return - - # Modification case. There are upsides and downsides to - # emitting a set-cookie header each request. The behavior - # is controlled by the :meth:`should_set_cookie` method - # which performs a quick check to figure out if the cookie - # should be set or not. This is controlled by the - # SESSION_REFRESH_EACH_REQUEST config flag as well as - # the permanent flag on the session itself. - if not self.should_set_cookie(app, session): - return + if not self.should_set_cookie(app, session): + return httponly = self.get_cookie_httponly(app) secure = self.get_cookie_secure(app) expires = self.get_expiration_time(app, session) val = self.get_signing_serializer(app).dumps(dict(session)) - response.set_cookie(app.session_cookie_name, val, - expires=expires, httponly=httponly, - domain=domain, path=path, secure=secure) + response.set_cookie( + app.session_cookie_name, + val, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure + ) From 5d9dd0b379a63d5a90265f2469f86fbd81b05853 Mon Sep 17 00:00:00 2001 From: David Lord Date: Sat, 20 May 2017 13:00:17 -0700 Subject: [PATCH 077/131] set session accessed for setdefault --- flask/sessions.py | 5 +++++ tests/test_basic.py | 40 ++++++++++++++++++++++------------------ 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/flask/sessions.py b/flask/sessions.py index 182ace85..2dbd8b32 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -138,6 +138,11 @@ class SecureCookieSession(CallbackDict, SessionMixin): self.accessed = True return super(SecureCookieSession, self).get(key, default) + def setdefault(self, key, default=None): + self.accessed = True + return super(SecureCookieSession, self).setdefault(key, default) + + class NullSession(SecureCookieSession): """Class used to generate nicer error messages if sessions are not available. Will still allow read-only access to the empty session diff --git a/tests/test_basic.py b/tests/test_basic.py index dd9fd17e..54f4c8e6 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -533,20 +533,22 @@ def test_session_vary_cookie(): app = flask.Flask(__name__) app.secret_key = 'testkey' - @app.route('/set-session') + @app.route('/set') def set_session(): flask.session['test'] = 'test' return '' - @app.route('/get-session') - def get_session(): - s = flask.session.get('test') - return '' + @app.route('/get') + def get(): + return flask.session.get('test') - @app.route('/get-session-with-dictionary') - def get_session_with_dictionary(): - s = flask.session['test'] - return '' + @app.route('/getitem') + def getitem(): + return flask.session['test'] + + @app.route('/setdefault') + def setdefault(): + return flask.session.setdefault('test', 'default') @app.route('/no-vary-header') def no_vary_header(): @@ -554,17 +556,19 @@ def test_session_vary_cookie(): c = app.test_client() - rv = c.get('/set-session') - assert rv.headers['Vary'] == 'Cookie' - - rv = c.get('/get-session') - assert rv.headers['Vary'] == 'Cookie' + def expect(path, header=True): + rv = c.get(path) - rv = c.get('/get-session-with-dictionary') - assert rv.headers['Vary'] == 'Cookie' + if header: + assert rv.headers['Vary'] == 'Cookie' + else: + assert 'Vary' not in rv.headers - rv = c.get('/no-vary-header') - assert 'Vary' not in rv.headers + expect('/set') + expect('/get') + expect('/getitem') + expect('/setdefault') + expect('/no-vary-header', False) def test_flashes(): From c590e820aadb9134a7fb19fe32c8ddae37199ec3 Mon Sep 17 00:00:00 2001 From: Tully Rankin Date: Mon, 22 May 2017 11:25:02 -0700 Subject: [PATCH 078/131] Updated documentation. Replaced term development mode with debug mode. #2261 --- docs/reqcontext.rst | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 51cd66f6..29af2677 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -119,9 +119,9 @@ understand what is actually happening. The new behavior is quite simple: 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 -however the exception is not further processed and bubbles up to the WSGI +Now what happens on errors? If you are not in debug mode if an exception is not +caught, the 500 internal server handler is called. In debug mode +however the exception is not further processed and bubbles up to the WSGI server. That way things like the interactive debugger can provide helpful debug information. @@ -214,10 +214,11 @@ provide you with important information. Starting with Flask 0.7 you have finer control over that behavior by setting the ``PRESERVE_CONTEXT_ON_EXCEPTION`` configuration variable. By default it's linked to the setting of ``DEBUG``. If the application is in -debug mode the context is preserved, in production mode it's not. +debug mode the context is preserved. If debug mode is set to off, the context +is not preserved. -Do not force activate ``PRESERVE_CONTEXT_ON_EXCEPTION`` in production mode -as it will cause your application to leak memory on exceptions. However +Do not force activate ``PRESERVE_CONTEXT_ON_EXCEPTION`` if debug mode is set to off +as it will cause your application to leak memory on exceptions. However, it can be useful during development to get the same error preserving -behavior as in development mode when attempting to debug an error that +behavior as debug mode when attempting to debug an error that only occurs under production settings. From 041c68f48baa2ee3a8437c5f0e8c6fbd6039f28f Mon Sep 17 00:00:00 2001 From: Tully Rankin Date: Mon, 22 May 2017 11:28:58 -0700 Subject: [PATCH 079/131] Updated request context documentation. --- docs/reqcontext.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/reqcontext.rst b/docs/reqcontext.rst index 29af2677..c3d37297 100644 --- a/docs/reqcontext.rst +++ b/docs/reqcontext.rst @@ -119,7 +119,7 @@ understand what is actually happening. The new behavior is quite simple: 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? If you are not in debug mode if an exception is not +Now what happens on errors? If you are not in debug mode and an exception is not caught, the 500 internal server handler is called. In debug mode however the exception is not further processed and bubbles up to the WSGI server. That way things like the interactive debugger can provide helpful From 9fddecd4d92763f38a42a3842abab70b0a6f4678 Mon Sep 17 00:00:00 2001 From: Randy Liou Date: Mon, 22 May 2017 11:55:23 -0700 Subject: [PATCH 080/131] Add coverage for Blueprint.app_errorhandler This test case registers an application-wise error handler from a Blueprint. Verifies the error handler by aborting the flask app from the application itself as well as from another registered Blueprint. --- tests/test_blueprints.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index de293e7f..e48fddc0 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -91,6 +91,33 @@ def test_blueprint_specific_user_error_handling(): assert c.get('/decorator').data == b'boom' assert c.get('/function').data == b'bam' +def test_blueprint_app_error_handling(): + errors = flask.Blueprint('errors', __name__) + + @errors.app_errorhandler(403) + def forbidden_handler(e): + return 'you shall not pass', 403 + + app = flask.Flask(__name__) + + @app.route('/forbidden') + def app_forbidden(): + flask.abort(403) + + forbidden_bp = flask.Blueprint('forbidden_bp', __name__) + + @forbidden_bp.route('/nope') + def bp_forbidden(): + flask.abort(403) + + app.register_blueprint(errors) + app.register_blueprint(forbidden_bp) + + c = app.test_client() + + assert c.get('/forbidden').data == b'you shall not pass' + assert c.get('/nope').data == b'you shall not pass' + def test_blueprint_url_definitions(): bp = flask.Blueprint('test', __name__) From 409dd15c10cbf79a52af554fd26a6e9c07ccd452 Mon Sep 17 00:00:00 2001 From: Tully Rankin Date: Mon, 22 May 2017 12:14:52 -0700 Subject: [PATCH 081/131] Added link to using Werkzeug debugger in quickstart documentation. --- docs/quickstart.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 7bdb67e4..6a18053e 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -153,6 +153,11 @@ Screenshot of the debugger in action: :class: screenshot :alt: screenshot of debugger in action +More information on using the debugger can be found in the `Werkzeug +documentation`_. + +.. _Werkzeug documentation: http://werkzeug.pocoo.org/docs/0.11/debug/#using-the-debugger + Have another debugger in mind? See :ref:`working-with-debuggers`. From 50b73f967bae6b452220155de830a2c0c24bfd2e Mon Sep 17 00:00:00 2001 From: Tully Rankin Date: Mon, 22 May 2017 12:19:52 -0700 Subject: [PATCH 082/131] Removed the version number out of the documenation link to Werkzeug. --- docs/quickstart.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 6a18053e..aea776f3 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -156,7 +156,7 @@ Screenshot of the debugger in action: More information on using the debugger can be found in the `Werkzeug documentation`_. -.. _Werkzeug documentation: http://werkzeug.pocoo.org/docs/0.11/debug/#using-the-debugger +.. _Werkzeug documentation: http://werkzeug.pocoo.org/docs/debug/#using-the-debugger Have another debugger in mind? See :ref:`working-with-debuggers`. From ced719ea18a56f6c4075e08dbe05f0e77eac1866 Mon Sep 17 00:00:00 2001 From: Hendrik Makait Date: Mon, 22 May 2017 12:30:18 -0700 Subject: [PATCH 083/131] Auto-detect create_app and make_app factory functions --- flask/cli.py | 15 +++++++++++++++ tests/test_cli.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/flask/cli.py b/flask/cli.py index 3d361be8..cdb7f094 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -46,6 +46,21 @@ def find_best_app(module): if len(matches) == 1: return matches[0] + + # Search for app factory callables. + for attr_name in 'create_app', 'make_app': + app_factory = getattr(module, attr_name, None) + if app_factory is not None and callable(app_factory): + try: + app = app_factory() + if app is not None and isinstance(app, Flask): + return app + except TypeError: + raise NoAppException('Auto-detected "%s()" in module "%s", ' + 'but could not call it without ' + 'specifying arguments.' + % (attr_name, module.__name__)) + raise NoAppException('Failed to find application in module "%s". Are ' 'you sure it contains a Flask application? Maybe ' 'you wrapped it in a WSGI middleware or you are ' diff --git a/tests/test_cli.py b/tests/test_cli.py index ab875cef..bbb6fe58 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -51,6 +51,34 @@ def test_find_best_app(test_apps): myapp = Flask('appname') assert find_best_app(Module) == Module.myapp + class Module: + @staticmethod + def create_app(): + return Flask('appname') + assert isinstance(find_best_app(Module), Flask) + assert find_best_app(Module).name == 'appname' + + class Module: + @staticmethod + def make_app(): + return Flask('appname') + assert isinstance(find_best_app(Module), Flask) + assert find_best_app(Module).name == 'appname' + + class Module: + myapp = Flask('appname1') + @staticmethod + def create_app(): + return Flask('appname2') + assert find_best_app(Module) == Module.myapp + + class Module: + myapp = Flask('appname1') + @staticmethod + def create_app(foo): + return Flask('appname2') + assert find_best_app(Module) == Module.myapp + class Module: pass pytest.raises(NoAppException, find_best_app, Module) @@ -60,6 +88,12 @@ def test_find_best_app(test_apps): myapp2 = Flask('appname2') pytest.raises(NoAppException, find_best_app, Module) + class Module: + @staticmethod + def create_app(foo): + return Flask('appname2') + pytest.raises(NoAppException, find_best_app, Module) + def test_prepare_exec_for_file(test_apps): """Expect the correct path to be set and the correct module name to be returned. From 136dbf7de014dda29f3af89d1ba4fd4f4620e25c Mon Sep 17 00:00:00 2001 From: Randy Liou Date: Mon, 22 May 2017 13:06:47 -0700 Subject: [PATCH 084/131] Add coverage for Blueprints.(app_)context_processor Test both context_processor and app_context_processor functions. Two context parameters are added into the context: one added to the blueprint locally; another added to the app globally. The test asserts the behaviors in both blueprint scope and the app scope. The coverage for flask.blueprints is increased by 3%. --- tests/test_blueprints.py | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index de293e7f..cee84d4e 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -591,3 +591,45 @@ def test_add_template_test_with_name_and_template(): return flask.render_template('template_test.html', value=False) rv = app.test_client().get('/') assert b'Success!' in rv.data + +def test_context_processing(): + app = flask.Flask(__name__) + answer_bp = flask.Blueprint('answer_bp', __name__) + + template_string = lambda: flask.render_template_string( + '{% if notanswer %}{{ notanswer }} is not the answer. {% endif %}' + '{% if answer %}{{ answer }} is the answer.{% endif %}' + ) + + # App global context processor + @answer_bp.app_context_processor + def not_answer_context_processor(): + return {'notanswer': 43} + + # Blueprint local context processor + @answer_bp.context_processor + def answer_context_processor(): + return {'answer': 42} + + # Setup endpoints for testing + @answer_bp.route('/bp') + def bp_page(): + return template_string() + + @app.route('/') + def app_page(): + return template_string() + + # Register the blueprint + app.register_blueprint(answer_bp) + + c = app.test_client() + + app_page_bytes = c.get('/').data + answer_page_bytes = c.get('/bp').data + + assert b'43' in app_page_bytes + assert b'42' not in app_page_bytes + + assert b'42' in answer_page_bytes + assert b'43' in answer_page_bytes From b4eb6534d52e86af3979b06734e5779f6bb9a4f6 Mon Sep 17 00:00:00 2001 From: Hendrik Makait Date: Mon, 22 May 2017 14:26:00 -0700 Subject: [PATCH 085/131] Remove unnecessary checks and reformat NoAppException messages --- flask/cli.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/flask/cli.py b/flask/cli.py index cdb7f094..109ba3fe 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -37,7 +37,7 @@ def find_best_app(module): # Search for the most common names first. for attr_name in 'app', 'application': app = getattr(module, attr_name, None) - if app is not None and isinstance(app, Flask): + if isinstance(app, Flask): return app # Otherwise find the only object that is a Flask instance. @@ -50,21 +50,23 @@ def find_best_app(module): # Search for app factory callables. for attr_name in 'create_app', 'make_app': app_factory = getattr(module, attr_name, None) - if app_factory is not None and callable(app_factory): + if callable(app_factory): try: app = app_factory() - if app is not None and isinstance(app, Flask): + if isinstance(app, Flask): return app except TypeError: - raise NoAppException('Auto-detected "%s()" in module "%s", ' - 'but could not call it without ' - 'specifying arguments.' - % (attr_name, module.__name__)) - - raise NoAppException('Failed to find application in module "%s". Are ' - 'you sure it contains a Flask application? Maybe ' - 'you wrapped it in a WSGI middleware or you are ' - 'using a factory function.' % module.__name__) + raise NoAppException( + 'Auto-detected "{callable}()" in module "{module}", but ' + 'could not call it without specifying arguments.' + .format(callable=attr_name, + module=module.__name__)) + + raise NoAppException( + 'Failed to find application in module "{module}". Are you sure ' + 'it contains a Flask application? Maybe you wrapped it in a WSGI ' + 'middleware or you are using a factory function.' + .format(module=module.__name__)) def prepare_exec_for_file(filename): From 7ce01ab9b4aac9e559cd6cb2e309381cfc3cc71b Mon Sep 17 00:00:00 2001 From: Randy Liou Date: Mon, 22 May 2017 14:14:24 -0700 Subject: [PATCH 086/131] Add coverage for Blueprint.add_app_template_global This tests the Blueprint.add_app_template_global mothod, which internally calls the Blueprint.app_template_global method. The methods are used to registering a function to the jinja template environment. This PR increases the test coverage for module flask.blueprint by 4%. --- tests/test_blueprints.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 12b58417..5f0d87da 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -660,3 +660,22 @@ def test_context_processing(): assert b'42' in answer_page_bytes assert b'43' in answer_page_bytes + +def test_template_global(): + app = flask.Flask(__name__) + bp = flask.Blueprint('bp', __name__) + @bp.app_template_global() + def get_answer(): + return 42 + # Make sure the function is not in the jinja_env already + assert 'get_answer' not in app.jinja_env.globals.keys() + app.register_blueprint(bp) + + # Tests + assert 'get_answer' in app.jinja_env.globals.keys() + assert app.jinja_env.globals['get_answer'] is get_answer + assert app.jinja_env.globals['get_answer']() == 42 + + with app.app_context(): + rv = flask.render_template_string('{{ get_answer() }}') + assert rv == '42' From d7d21f55596b141ae6ca3cc1a52e9b5be9d474eb Mon Sep 17 00:00:00 2001 From: Tully Rankin Date: Mon, 22 May 2017 14:48:24 -0700 Subject: [PATCH 087/131] Moved WSGI-Standalone above mod_wsgi in deployment documentation. --- docs/deploying/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index 6950e47a..0bc61fc2 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -32,8 +32,8 @@ Self-hosted options .. toctree:: :maxdepth: 2 - mod_wsgi wsgi-standalone + mod_wsgi uwsgi fastcgi cgi From 2b96052240ff1113bfb0c376aaf7aa376785184d Mon Sep 17 00:00:00 2001 From: Tully Rankin Date: Mon, 22 May 2017 15:13:31 -0700 Subject: [PATCH 088/131] Moved mod_wsgi under uwsgi in TOC deployment docs. --- docs/deploying/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index 0bc61fc2..da8ac14e 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -33,7 +33,7 @@ Self-hosted options :maxdepth: 2 wsgi-standalone - mod_wsgi uwsgi + mod_wsgi fastcgi cgi From 7ecdbcfa2b2d1c83c8c6561b49303ed5b1042350 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 22 May 2017 15:48:08 -0700 Subject: [PATCH 089/131] show error if multiple Flask instances are detected add changelog --- CHANGES | 3 +++ flask/cli.py | 28 +++++++++++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/CHANGES b/CHANGES index 76a0d7a2..7effdfac 100644 --- a/CHANGES +++ b/CHANGES @@ -40,6 +40,8 @@ Major release, unreleased - Allow IP address as exact session cookie domain. (`#2282`_) - ``SESSION_COOKIE_DOMAIN`` is set if it is detected through ``SERVER_NAME``. (`#2282`_) +- Auto-detect 0-argument app factory called ``create_app`` or ``make_app`` from + ``FLASK_APP``. (`#2297`_) .. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1898: https://github.com/pallets/flask/pull/1898 @@ -50,6 +52,7 @@ Major release, unreleased .. _#2256: https://github.com/pallets/flask/pull/2256 .. _#2259: https://github.com/pallets/flask/pull/2259 .. _#2282: https://github.com/pallets/flask/pull/2282 +.. _#2297: https://github.com/pallets/flask/pull/2297 Version 0.12.2 -------------- diff --git a/flask/cli.py b/flask/cli.py index 109ba3fe..a10cf5e5 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -41,32 +41,42 @@ def find_best_app(module): return app # Otherwise find the only object that is a Flask instance. - matches = [v for k, v in iteritems(module.__dict__) - if isinstance(v, Flask)] + matches = [ + v for k, v in iteritems(module.__dict__) if isinstance(v, Flask) + ] if len(matches) == 1: return matches[0] + elif len(matches) > 1: + raise NoAppException( + 'Auto-detected multiple Flask applications in module "{module}".' + ' Use "FLASK_APP={module}:name" to specify the correct' + ' one.'.format(module=module.__name__) + ) # Search for app factory callables. for attr_name in 'create_app', 'make_app': app_factory = getattr(module, attr_name, None) + if callable(app_factory): try: app = app_factory() + if isinstance(app, Flask): return app except TypeError: raise NoAppException( 'Auto-detected "{callable}()" in module "{module}", but ' - 'could not call it without specifying arguments.' - .format(callable=attr_name, - module=module.__name__)) + 'could not call it without specifying arguments.'.format( + callable=attr_name, module=module.__name__ + ) + ) raise NoAppException( - 'Failed to find application in module "{module}". Are you sure ' - 'it contains a Flask application? Maybe you wrapped it in a WSGI ' - 'middleware or you are using a factory function.' - .format(module=module.__name__)) + 'Failed to find application in module "{module}". Are you sure ' + 'it contains a Flask application? Maybe you wrapped it in a WSGI ' + 'middleware.'.format(module=module.__name__) + ) def prepare_exec_for_file(filename): From 01ddf54b87850c50ee9020c6027647e0c5fca283 Mon Sep 17 00:00:00 2001 From: David Lord Date: Mon, 22 May 2017 16:12:23 -0700 Subject: [PATCH 090/131] adjust for loop style --- CHANGES | 4 ++-- flask/cli.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGES b/CHANGES index 7effdfac..d243d66d 100644 --- a/CHANGES +++ b/CHANGES @@ -40,8 +40,8 @@ Major release, unreleased - Allow IP address as exact session cookie domain. (`#2282`_) - ``SESSION_COOKIE_DOMAIN`` is set if it is detected through ``SERVER_NAME``. (`#2282`_) -- Auto-detect 0-argument app factory called ``create_app`` or ``make_app`` from - ``FLASK_APP``. (`#2297`_) +- Auto-detect zero-argument app factory called ``create_app`` or ``make_app`` + from ``FLASK_APP``. (`#2297`_) .. _#1489: https://github.com/pallets/flask/pull/1489 .. _#1898: https://github.com/pallets/flask/pull/1898 diff --git a/flask/cli.py b/flask/cli.py index a10cf5e5..6aa66f4f 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -35,7 +35,7 @@ def find_best_app(module): from . import Flask # Search for the most common names first. - for attr_name in 'app', 'application': + for attr_name in ('app', 'application'): app = getattr(module, attr_name, None) if isinstance(app, Flask): return app @@ -55,7 +55,7 @@ def find_best_app(module): ) # Search for app factory callables. - for attr_name in 'create_app', 'make_app': + for attr_name in ('create_app', 'make_app'): app_factory = getattr(module, attr_name, None) if callable(app_factory): From 6f49089a624a13353a07db87aff5fe6f01699728 Mon Sep 17 00:00:00 2001 From: MikeTheReader Date: Mon, 22 May 2017 16:15:48 -0700 Subject: [PATCH 091/131] Added tests for make_response and get_debug_flag to improve coverage of helpers.py --- tests/test_helpers.py | 50 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index d5706521..19c364f5 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -22,6 +22,7 @@ from werkzeug.exceptions import BadRequest, NotFound from werkzeug.http import parse_cache_control_header, parse_options_header from werkzeug.http import http_date from flask._compat import StringIO, text_type +from flask.helpers import get_debug_flag, make_response def has_encoding(name): @@ -941,3 +942,52 @@ class TestSafeJoin(object): for args in failing: with pytest.raises(NotFound): print(flask.safe_join(*args)) + +class TestHelpers(object): + + def test_get_debug_flag(self): + original_debug_value = os.environ.get('FLASK_DEBUG') or '' + os.environ['FLASK_DEBUG'] = '' + assert get_debug_flag() == None + assert get_debug_flag(default=True) == True + + os.environ['FLASK_DEBUG'] = '0' + assert get_debug_flag() == False + assert get_debug_flag(default=True) == False + + os.environ['FLASK_DEBUG'] = 'False' + assert get_debug_flag() == False + assert get_debug_flag(default=True) == False + + os.environ['FLASK_DEBUG'] = 'No' + assert get_debug_flag() == False + assert get_debug_flag(default=True) == False + + os.environ['FLASK_DEBUG'] = 'True' + assert get_debug_flag() == True + assert get_debug_flag(default=True) == True + + os.environ['FLASK_DEBUG'] = original_debug_value + + def test_make_response_no_args(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + return flask.helpers.make_response() + c = app.test_client() + rv = c.get() + assert rv + + def test_make_response_with_args(self): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + response = flask.helpers.make_response(flask.render_template_string('Hello World')) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + c = app.test_client() + rv = c.get() + assert rv + assert rv.headers['X-Parachutes'] == 'parachutes are cool' From a690ae27a392cfa1474e062f7655250c416cd536 Mon Sep 17 00:00:00 2001 From: Randy Liou Date: Mon, 22 May 2017 15:49:04 -0700 Subject: [PATCH 092/131] Add coverage for Blueprint request process methods Add test to cover following methodss to the Blueprint object: before_request, after_request, before_app_request, before_app_first_request, after_app_request. This PR increases the coverage of flask.blueprints by 6%. --- tests/test_blueprints.py | 62 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 5f0d87da..78da0d2c 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -679,3 +679,65 @@ def test_template_global(): with app.app_context(): rv = flask.render_template_string('{{ get_answer() }}') assert rv == '42' + +def test_request_processing(): + app = flask.Flask(__name__) + bp = flask.Blueprint('bp', __name__) + evts = [] + @bp.before_request + def before_bp(): + evts.append('before') + @bp.after_request + def after_bp(response): + response.data += b'|after' + evts.append('after') + return response + + # Setup routes for testing + @bp.route('/bp') + def bp_endpoint(): + return 'request' + + app.register_blueprint(bp) + + assert evts == [] + rv = app.test_client().get('/bp') + assert rv.data == b'request|after' + assert evts == ['before', 'after'] + +def test_app_request_processing(): + app = flask.Flask(__name__) + bp = flask.Blueprint('bp', __name__) + evts = [] + + @bp.before_app_first_request + def before_first_request(): + evts.append('first') + @bp.before_app_request + def before_app(): + evts.append('before') + @bp.after_app_request + def after_app(response): + response.data += b'|after' + evts.append('after') + return response + + app.register_blueprint(bp) + + # Setup routes for testing + @app.route('/') + def bp_endpoint(): + return 'request' + + # before first request + assert evts == [] + + # first request + resp = app.test_client().get('/').data + assert resp == b'request|after' + assert evts == ['first', 'before', 'after'] + + # second request + resp = app.test_client().get('/').data + assert resp == b'request|after' + assert evts == ['first', 'before', 'after', 'before', 'after'] From d8d712a0def83d146d155acb941c31781e7c3b3a Mon Sep 17 00:00:00 2001 From: Randy Liou Date: Mon, 22 May 2017 16:54:52 -0700 Subject: [PATCH 093/131] Add coverage for Blueprint teardown request method Test the following methods in the Blueprint object: teardown_request, and teardown_app_request. This PR increases the coverage of blueprint module by 3%. --- tests/test_blueprints.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 78da0d2c..5c5119c0 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -692,6 +692,9 @@ def test_request_processing(): response.data += b'|after' evts.append('after') return response + @bp.teardown_request + def teardown_bp(exc): + evts.append('teardown') # Setup routes for testing @bp.route('/bp') @@ -703,7 +706,7 @@ def test_request_processing(): assert evts == [] rv = app.test_client().get('/bp') assert rv.data == b'request|after' - assert evts == ['before', 'after'] + assert evts == ['before', 'after', 'teardown'] def test_app_request_processing(): app = flask.Flask(__name__) @@ -721,6 +724,9 @@ def test_app_request_processing(): response.data += b'|after' evts.append('after') return response + @bp.teardown_app_request + def teardown_app(exc): + evts.append('teardown') app.register_blueprint(bp) @@ -735,9 +741,9 @@ def test_app_request_processing(): # first request resp = app.test_client().get('/').data assert resp == b'request|after' - assert evts == ['first', 'before', 'after'] + assert evts == ['first', 'before', 'after', 'teardown'] # second request resp = app.test_client().get('/').data assert resp == b'request|after' - assert evts == ['first', 'before', 'after', 'before', 'after'] + assert evts == ['first'] + ['before', 'after', 'teardown'] * 2 From 65fc888172fbff89a8354e8926a69b4515766389 Mon Sep 17 00:00:00 2001 From: Neil Grey Date: Mon, 22 May 2017 17:36:55 -0700 Subject: [PATCH 094/131] For Issue #2286: Replaces references to unittest in the documentation with pytest --- docs/signals.rst | 4 +- docs/testing.rst | 200 ++++++++++++++++++++++++++--------------------- flask/app.py | 2 +- 3 files changed, 112 insertions(+), 94 deletions(-) diff --git a/docs/signals.rst b/docs/signals.rst index 2426e920..40041491 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -27,7 +27,7 @@ executed in undefined order and do not modify any data. The big advantage of signals over handlers is that you can safely subscribe to them for just a split second. These temporary -subscriptions are helpful for unittesting for example. Say you want to +subscriptions are helpful for unit testing for example. Say you want to know what templates were rendered as part of a request: signals allow you to do exactly that. @@ -45,7 +45,7 @@ signal. When you subscribe to a signal, be sure to also provide a sender unless you really want to listen for signals from all applications. This is especially true if you are developing an extension. -For example, here is a helper context manager that can be used in a unittest +For example, here is a helper context manager that can be used in a unit test to determine which templates were rendered and what variables were passed to the template:: diff --git a/docs/testing.rst b/docs/testing.rst index bd1c8467..eb0737da 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -5,23 +5,30 @@ Testing Flask Applications **Something that is untested is broken.** -The origin of this quote is unknown and while it is not entirely correct, it is also -not far from the truth. Untested applications make it hard to +The origin of this quote is unknown and while it is not entirely correct, it +is also not far from the truth. Untested applications make it hard to improve existing code and developers of untested applications tend to become pretty paranoid. If an application has automated tests, you can safely make changes and instantly know if anything breaks. Flask provides a way to test your application by exposing the Werkzeug test :class:`~werkzeug.test.Client` and handling the context locals for you. -You can then use that with your favourite testing solution. In this documentation -we will use the :mod:`unittest` package that comes pre-installed with Python. +You can then use that with your favourite testing solution. + +In this documentation we will use the `pytest`_ package as the base +framework for our tests. You can install it with ``pip``, like so:: + + pip install pytest + +.. _pytest: + https://pytest.org The Application --------------- First, we need an application to test; we will use the application from the :ref:`tutorial`. If you don't have that application yet, get the -sources from `the examples`_. +source code from `the examples`_. .. _the examples: https://github.com/pallets/flask/tree/master/examples/flaskr/ @@ -29,92 +36,89 @@ sources from `the examples`_. The Testing Skeleton -------------------- -In order to test the application, we add a second module -(:file:`flaskr_tests.py`) and create a unittest skeleton there:: +We begin by adding a tests directory under the application root. Then +create a Python file to store our tests (:file:`test_flaskr.py`). When we +format the filename like ``test_*.py``, it will be auto-discoverable by +pytest. + +Next, we create a `pytest fixture`_ called +:func:`client` that configures +the application for testing and initializes a new database.:: import os - from flaskr import flaskr - import unittest import tempfile + import pytest + from flaskr import flaskr - class FlaskrTestCase(unittest.TestCase): - - def setUp(self): - self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() - flaskr.app.testing = True - self.app = flaskr.app.test_client() - with flaskr.app.app_context(): - flaskr.init_db() + @pytest.fixture + def client(request): + db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() + flaskr.app.config['TESTING'] = True + client = flaskr.app.test_client() + with flaskr.app.app_context(): + flaskr.init_db() - def tearDown(self): - os.close(self.db_fd) + def teardown(): + os.close(db_fd) os.unlink(flaskr.app.config['DATABASE']) + request.addfinalizer(teardown) - if __name__ == '__main__': - unittest.main() + return client -The code in the :meth:`~unittest.TestCase.setUp` method creates a new test -client and initializes a new database. This function is called before -each individual test function is run. To delete the database after the -test, we close the file and remove it from the filesystem in the -:meth:`~unittest.TestCase.tearDown` method. Additionally during setup the -``TESTING`` config flag is activated. What it does is disable the error -catching during request handling so that you get better error reports when -performing test requests against the application. +This client fixture will be called by each individual test. It gives us a +simple interface to the application, where we can trigger test requests to the +application. The client will also keep track of cookies for us. -This test client will give us a simple interface to the application. We can -trigger test requests to the application, and the client will also keep track -of cookies for us. +During setup, the ``TESTING`` config flag is activated. What +this does is disable error catching during request handling, so that +you get better error reports when performing test requests against the +application. -Because SQLite3 is filesystem-based we can easily use the tempfile module +Because SQLite3 is filesystem-based, we can easily use the :mod:`tempfile` module to create a temporary database and initialize it. The :func:`~tempfile.mkstemp` function does two things for us: it returns a low-level file handle and a random file name, the latter we use as database name. We just have to keep the `db_fd` around so that we can use the :func:`os.close` function to close the file. +To delete the database after the test, we close the file and remove it +from the filesystem in the +:func:`teardown` function. + If we now run the test suite, we should see the following output:: - $ python flaskr_tests.py + $ pytest - ---------------------------------------------------------------------- - Ran 0 tests in 0.000s + ================ test session starts ================ + rootdir: ./flask/examples/flaskr, inifile: setup.cfg + collected 0 items - OK + =========== no tests ran in 0.07 seconds ============ -Even though it did not run any actual tests, we already know that our flaskr +Even though it did not run any actual tests, we already know that our ``flaskr`` application is syntactically valid, otherwise the import would have died with an exception. +.. _pytest fixture: + https://docs.pytest.org/en/latest/fixture.html + The First Test -------------- Now it's time to start testing the functionality of the application. Let's check that the application shows "No entries here so far" if we -access the root of the application (``/``). To do this, we add a new -test method to our class, like this:: +access the root of the application (``/``). To do this, we add a new +test function to :file:`test_flaskr.py`, like this:: - class FlaskrTestCase(unittest.TestCase): - - def setUp(self): - self.db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() - flaskr.app.testing = True - self.app = flaskr.app.test_client() - with flaskr.app.app_context(): - flaskr.init_db() - - def tearDown(self): - os.close(self.db_fd) - os.unlink(flaskr.app.config['DATABASE']) - - def test_empty_db(self): - rv = self.app.get('/') - assert b'No entries here so far' in rv.data + def test_empty_db(client): + """Start with a blank database.""" + rv = client.get('/') + assert b'No entries here so far' in rv.data Notice that our test functions begin with the word `test`; this allows -:mod:`unittest` to automatically identify the method as a test to run. +`pytest`_ to automatically identify the function as a test to run. -By using `self.app.get` we can send an HTTP ``GET`` request to the application with +By using `client.get` we can send an HTTP ``GET`` request to the application with the given path. The return value will be a :class:`~flask.Flask.response_class` object. We can now use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect the return value (as string) from the application. In this case, we ensure that @@ -122,12 +126,15 @@ the return value (as string) from the application. In this case, we ensure that Run it again and you should see one passing test:: - $ python flaskr_tests.py - . - ---------------------------------------------------------------------- - Ran 1 test in 0.034s + $ pytest -v + + ================ test session starts ================ + rootdir: ./flask/examples/flaskr, inifile: setup.cfg + collected 1 items - OK + tests/test_flaskr.py::test_empty_db PASSED + + ============= 1 passed in 0.10 seconds ============== Logging In and Out ------------------ @@ -138,39 +145,45 @@ of the application. To do this, we fire some requests to the login and logout pages with the required form data (username and password). And because the login and logout pages redirect, we tell the client to `follow_redirects`. -Add the following two methods to your `FlaskrTestCase` class:: +Add the following two functions to your :file:`test_flaskr.py` file:: - def login(self, username, password): - return self.app.post('/login', data=dict( - username=username, - password=password - ), follow_redirects=True) + def login(client, username, password): + return client.post('/login', data=dict( + username=username, + password=password + ), follow_redirects=True) - def logout(self): - return self.app.get('/logout', follow_redirects=True) + def logout(client): + return client.get('/logout', follow_redirects=True) Now we can easily test that logging in and out works and that it fails with -invalid credentials. Add this new test to the class:: - - def test_login_logout(self): - rv = self.login('admin', 'default') - assert b'You were logged in' in rv.data - rv = self.logout() - assert b'You were logged out' in rv.data - rv = self.login('adminx', 'default') - assert b'Invalid username' in rv.data - rv = self.login('admin', 'defaultx') - assert b'Invalid password' in rv.data +invalid credentials. Add this new test function:: + + def test_login_logout(client): + """Make sure login and logout works""" + rv = login(client, flaskr.app.config['USERNAME'], + flaskr.app.config['PASSWORD']) + assert b'You were logged in' in rv.data + rv = logout(client) + assert b'You were logged out' in rv.data + rv = login(client, flaskr.app.config['USERNAME'] + 'x', + flaskr.app.config['PASSWORD']) + assert b'Invalid username' in rv.data + rv = login(client, flaskr.app.config['USERNAME'], + flaskr.app.config['PASSWORD'] + 'x') + assert b'Invalid password' in rv.data Test Adding Messages -------------------- -We should also test that adding messages works. Add a new test method +We should also test that adding messages works. Add a new test function like this:: - def test_messages(self): - self.login('admin', 'default') - rv = self.app.post('/add', data=dict( + def test_messages(client): + """Test that messages work""" + login(client, flaskr.app.config['USERNAME'], + flaskr.app.config['PASSWORD']) + rv = client.post('/add', data=dict( title='', text='HTML allowed here' ), follow_redirects=True) @@ -183,12 +196,17 @@ which is the intended behavior. Running that should now give us three passing tests:: - $ python flaskr_tests.py - ... - ---------------------------------------------------------------------- - Ran 3 tests in 0.332s + $ pytest -v + + ================ test session starts ================ + rootdir: ./flask/examples/flaskr, inifile: setup.cfg + collected 3 items + + tests/test_flaskr.py::test_empty_db PASSED + tests/test_flaskr.py::test_login_logout PASSED + tests/test_flaskr.py::test_messages PASSED - OK + ============= 3 passed in 0.23 seconds ============== For more complex tests with headers and status codes, check out the `MiniTwit Example`_ from the sources which contains a larger test diff --git a/flask/app.py b/flask/app.py index 703cfef2..d851bf83 100644 --- a/flask/app.py +++ b/flask/app.py @@ -222,7 +222,7 @@ class Flask(_PackageBoundObject): #: The testing flag. Set this to ``True`` to enable the test mode of #: Flask extensions (and in the future probably also Flask itself). - #: For example this might activate unittest helpers that have an + #: For example this might activate test helpers that have an #: additional runtime cost which should not be enabled by default. #: #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the From 4e9d51b39b674b5c1c00081c1b457ff03ba6e76c Mon Sep 17 00:00:00 2001 From: Neil Grey Date: Mon, 22 May 2017 17:42:30 -0700 Subject: [PATCH 095/131] For Issue #2286: Fixing indenting of test_login_logout --- docs/testing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testing.rst b/docs/testing.rst index eb0737da..7a16e336 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -159,7 +159,7 @@ Add the following two functions to your :file:`test_flaskr.py` file:: Now we can easily test that logging in and out works and that it fails with invalid credentials. Add this new test function:: - def test_login_logout(client): + def test_login_logout(client): """Make sure login and logout works""" rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD']) From 65b61aa7c202bb26df53e2cb271269fddd95aeee Mon Sep 17 00:00:00 2001 From: Tully Rankin Date: Mon, 22 May 2017 18:08:40 -0700 Subject: [PATCH 096/131] Added uWSGI and example usage to stand-alone WSGI containers documentation (#2302) --- docs/deploying/wsgi-standalone.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/deploying/wsgi-standalone.rst b/docs/deploying/wsgi-standalone.rst index ad43c144..bf680976 100644 --- a/docs/deploying/wsgi-standalone.rst +++ b/docs/deploying/wsgi-standalone.rst @@ -27,6 +27,22 @@ For example, to run a Flask application with 4 worker processes (``-w .. _eventlet: http://eventlet.net/ .. _greenlet: https://greenlet.readthedocs.io/en/latest/ +uWSGI +-------- + +`uWSGI`_ is a fast application server written in C. It is very configurable +which makes it more complicated to setup than gunicorn. + +Running `uWSGI HTTP Router`_:: + + uwsgi --http 127.0.0.1:5000 --module myproject:app + +For a more optimized setup, see `configuring uWSGI and NGINX`_. + +.. _uWSGI: http://uwsgi-docs.readthedocs.io/en/latest/ +.. _uWSGI HTTP Router: http://uwsgi-docs.readthedocs.io/en/latest/HTTP.html#the-uwsgi-http-https-router +.. _configuring uWSGI and NGINX: uwsgi.html#starting-your-app-with-uwsgi + Gevent ------- From 378a11f99275a6b80d365a5d51d469844bf9ca17 Mon Sep 17 00:00:00 2001 From: Neil Grey Date: Mon, 22 May 2017 18:22:08 -0700 Subject: [PATCH 097/131] For Issue #2286: Updating test_flaskr to use yield inside fixture --- docs/testing.rst | 15 +++++---------- examples/flaskr/tests/test_flaskr.py | 10 +++------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 7a16e336..67b8aaea 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -57,13 +57,9 @@ the application for testing and initializes a new database.:: client = flaskr.app.test_client() with flaskr.app.app_context(): flaskr.init_db() - - def teardown(): - os.close(db_fd) - os.unlink(flaskr.app.config['DATABASE']) - request.addfinalizer(teardown) - - return client + yield client + os.close(db_fd) + os.unlink(flaskr.app.config['DATABASE']) This client fixture will be called by each individual test. It gives us a simple interface to the application, where we can trigger test requests to the @@ -82,8 +78,7 @@ database name. We just have to keep the `db_fd` around so that we can use the :func:`os.close` function to close the file. To delete the database after the test, we close the file and remove it -from the filesystem in the -:func:`teardown` function. +from the filesystem. If we now run the test suite, we should see the following output:: @@ -118,7 +113,7 @@ test function to :file:`test_flaskr.py`, like this:: Notice that our test functions begin with the word `test`; this allows `pytest`_ to automatically identify the function as a test to run. -By using `client.get` we can send an HTTP ``GET`` request to the application with +By using ``client.get`` we can send an HTTP ``GET`` request to the application with the given path. The return value will be a :class:`~flask.Flask.response_class` object. We can now use the :attr:`~werkzeug.wrappers.BaseResponse.data` attribute to inspect the return value (as string) from the application. In this case, we ensure that diff --git a/examples/flaskr/tests/test_flaskr.py b/examples/flaskr/tests/test_flaskr.py index 663e92e0..df32cd4b 100644 --- a/examples/flaskr/tests/test_flaskr.py +++ b/examples/flaskr/tests/test_flaskr.py @@ -22,13 +22,9 @@ def client(request): client = flaskr.app.test_client() with flaskr.app.app_context(): flaskr.init_db() - - def teardown(): - os.close(db_fd) - os.unlink(flaskr.app.config['DATABASE']) - request.addfinalizer(teardown) - - return client + yield client + os.close(db_fd) + os.unlink(flaskr.app.config['DATABASE']) def login(client, username, password): From fd4a363657f3b4f13611e029c52fcb8c806f7229 Mon Sep 17 00:00:00 2001 From: MikeTheReader Date: Mon, 22 May 2017 20:49:37 -0700 Subject: [PATCH 098/131] Modifications based on review --- tests/test_helpers.py | 44 +++++++++++++++---------------------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 19c364f5..a5863da9 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -945,49 +945,35 @@ class TestSafeJoin(object): class TestHelpers(object): - def test_get_debug_flag(self): - original_debug_value = os.environ.get('FLASK_DEBUG') or '' - os.environ['FLASK_DEBUG'] = '' + def test_get_debug_flag(self, monkeypatch): + monkeypatch.setenv('FLASK_DEBUG', '') assert get_debug_flag() == None assert get_debug_flag(default=True) == True - os.environ['FLASK_DEBUG'] = '0' + monkeypatch.setenv('FLASK_DEBUG', '0') assert get_debug_flag() == False assert get_debug_flag(default=True) == False - os.environ['FLASK_DEBUG'] = 'False' + monkeypatch.setenv('FLASK_DEBUG', 'False') assert get_debug_flag() == False assert get_debug_flag(default=True) == False - os.environ['FLASK_DEBUG'] = 'No' + monkeypatch.setenv('FLASK_DEBUG', 'No') assert get_debug_flag() == False assert get_debug_flag(default=True) == False - os.environ['FLASK_DEBUG'] = 'True' + monkeypatch.setenv('FLASK_DEBUG', 'True') assert get_debug_flag() == True assert get_debug_flag(default=True) == True - os.environ['FLASK_DEBUG'] = original_debug_value - - def test_make_response_no_args(self): + def test_make_response(self): app = flask.Flask(__name__) - app.testing = True - @app.route('/') - def index(): - return flask.helpers.make_response() - c = app.test_client() - rv = c.get() - assert rv + with app.test_request_context(): + rv = flask.helpers.make_response() + assert rv.status_code == 200 + assert rv.mimetype == 'text/html' - def test_make_response_with_args(self): - app = flask.Flask(__name__) - app.testing = True - @app.route('/') - def index(): - response = flask.helpers.make_response(flask.render_template_string('Hello World')) - response.headers['X-Parachutes'] = 'parachutes are cool' - return response - c = app.test_client() - rv = c.get() - assert rv - assert rv.headers['X-Parachutes'] == 'parachutes are cool' + rv = flask.helpers.make_response('Hello') + assert rv.status_code == 200 + assert rv.data == b'Hello' + assert rv.mimetype == 'text/html' From 11d2eec3acb7d0cf291b572e3b6b493df5fadc6b Mon Sep 17 00:00:00 2001 From: Andrey Kislyuk Date: Mon, 22 May 2017 23:46:22 -0700 Subject: [PATCH 099/131] Fix refactoring error in static_folder docstring (#2310) --- flask/app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index 703cfef2..13a56c68 100644 --- a/flask/app.py +++ b/flask/app.py @@ -133,8 +133,6 @@ class Flask(_PackageBoundObject): :param static_folder: the folder with static files that should be served at `static_url_path`. Defaults to the ``'static'`` folder in the root path of the application. - folder in the root path of the application. Defaults - to None. :param host_matching: sets the app's ``url_map.host_matching`` to the given given value. Defaults to False. :param static_host: the host to use when adding the static route. Defaults From cd412b20dc64f3bfa980c9993e5a4dce39a22b94 Mon Sep 17 00:00:00 2001 From: MikeTheReader Date: Tue, 23 May 2017 07:51:57 -0700 Subject: [PATCH 100/131] Parameterize test_get_debug_flag --- tests/test_helpers.py | 34 ++++++++++++++-------------------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index a5863da9..1f623559 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -945,26 +945,20 @@ class TestSafeJoin(object): class TestHelpers(object): - def test_get_debug_flag(self, monkeypatch): - monkeypatch.setenv('FLASK_DEBUG', '') - assert get_debug_flag() == None - assert get_debug_flag(default=True) == True - - monkeypatch.setenv('FLASK_DEBUG', '0') - assert get_debug_flag() == False - assert get_debug_flag(default=True) == False - - monkeypatch.setenv('FLASK_DEBUG', 'False') - assert get_debug_flag() == False - assert get_debug_flag(default=True) == False - - monkeypatch.setenv('FLASK_DEBUG', 'No') - assert get_debug_flag() == False - assert get_debug_flag(default=True) == False - - monkeypatch.setenv('FLASK_DEBUG', 'True') - assert get_debug_flag() == True - assert get_debug_flag(default=True) == True + @pytest.mark.parametrize("debug, expected_flag, expected_default_flag", [ + ('', None, True), + ('0', False, False), + ('False', False, False), + ('No', False, False), + ('True', True, True) + ]) + def test_get_debug_flag(self, monkeypatch, debug, expected_flag, expected_default_flag): + monkeypatch.setenv('FLASK_DEBUG', debug) + if expected_flag is None: + assert get_debug_flag() is None + else: + assert get_debug_flag() == expected_flag + assert get_debug_flag(default=True) == expected_default_flag def test_make_response(self): app = flask.Flask(__name__) From 7c882a457b734cd0a38d41c7e930e375a6ce6e4c Mon Sep 17 00:00:00 2001 From: MikeTheReader Date: Tue, 23 May 2017 07:59:53 -0700 Subject: [PATCH 101/131] Replace double quotes with single quotes --- tests/test_helpers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 1f623559..5af95f18 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -945,7 +945,7 @@ class TestSafeJoin(object): class TestHelpers(object): - @pytest.mark.parametrize("debug, expected_flag, expected_default_flag", [ + @pytest.mark.parametrize('debug, expected_flag, expected_default_flag', [ ('', None, True), ('0', False, False), ('False', False, False), From 2339f1f24e29adfd263cb6ebcb98c09ecb4396d9 Mon Sep 17 00:00:00 2001 From: Christopher Sorenson Date: Mon, 22 May 2017 18:39:07 -0500 Subject: [PATCH 102/131] making some updates to the CONTRIBUTING.rst --- CONTRIBUTING.rst | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 852e44be..80b6af35 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -24,12 +24,31 @@ Reporting issues Submitting patches ================== + +- `Make sure you have a github account `_ +- `Download and install latest version of git on your + machine `_ +- `Set up your username in git + `_ +- `Set up your email in git + `_ +- Fork flask to your github account (Click the Fork button) +- `Copy your github fork locally + `_ +- Create a branch to identify the issue you would like to work + on (eg 2287-dry-test-suite) +- Using your favorite editor, make your changes, + `committing as you go `_ - Include tests if your patch is supposed to solve a bug, and explain clearly under which circumstances the bug happens. Make sure the test fails without your patch. - - Try to follow `PEP8 `_, but you may ignore the line-length-limit if following it would make the code uglier. +- `Run tests. + `_ + When tests pass push changes to github and `create pull request + `_ +- Celebrate 🎉 Running the testsuite From 5963cb5a5172bce3a1ff7b0f8c64230f043c9cfd Mon Sep 17 00:00:00 2001 From: bovarysme Date: Tue, 23 May 2017 18:21:29 +0200 Subject: [PATCH 103/131] Use the yield syntax in pytest's fixtures --- examples/minitwit/tests/test_minitwit.py | 12 +++++------- tests/conftest.py | 19 ++++++++++--------- tests/test_ext.py | 22 ++++++++++------------ 3 files changed, 25 insertions(+), 28 deletions(-) diff --git a/examples/minitwit/tests/test_minitwit.py b/examples/minitwit/tests/test_minitwit.py index 50ca26d9..c8992e57 100644 --- a/examples/minitwit/tests/test_minitwit.py +++ b/examples/minitwit/tests/test_minitwit.py @@ -15,18 +15,16 @@ from minitwit import minitwit @pytest.fixture -def client(request): +def client(): db_fd, minitwit.app.config['DATABASE'] = tempfile.mkstemp() client = minitwit.app.test_client() with minitwit.app.app_context(): minitwit.init_db() - def teardown(): - """Get rid of the database again after each test.""" - os.close(db_fd) - os.unlink(minitwit.app.config['DATABASE']) - request.addfinalizer(teardown) - return client + yield client + + os.close(db_fd) + os.unlink(minitwit.app.config['DATABASE']) def register(client, username, password, password2=None, email=None): diff --git a/tests/conftest.py b/tests/conftest.py index 8c9541de..eb130db6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,16 +22,17 @@ def test_apps(monkeypatch): os.path.dirname(__file__), 'test_apps')) ) + @pytest.fixture(autouse=True) -def leak_detector(request): - def ensure_clean_request_context(): - # make sure we're not leaking a request context since we are - # testing flask internally in debug mode in a few cases - leaks = [] - while flask._request_ctx_stack.top is not None: - leaks.append(flask._request_ctx_stack.pop()) - assert leaks == [] - request.addfinalizer(ensure_clean_request_context) +def leak_detector(): + yield + + # make sure we're not leaking a request context since we are + # testing flask internally in debug mode in a few cases + leaks = [] + while flask._request_ctx_stack.top is not None: + leaks.append(flask._request_ctx_stack.pop()) + assert leaks == [] @pytest.fixture(params=(True, False)) diff --git a/tests/test_ext.py b/tests/test_ext.py index ebb5f02d..48214905 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -21,19 +21,18 @@ from flask._compat import PY2 @pytest.fixture(autouse=True) -def disable_extwarnings(request, recwarn): +def disable_extwarnings(recwarn): from flask.exthook import ExtDeprecationWarning - def inner(): - assert set(w.category for w in recwarn.list) \ - <= set([ExtDeprecationWarning]) - recwarn.clear() + yield - request.addfinalizer(inner) + assert set(w.category for w in recwarn.list) \ + <= set([ExtDeprecationWarning]) + recwarn.clear() @pytest.fixture(autouse=True) -def importhook_setup(monkeypatch, request): +def importhook_setup(monkeypatch): # we clear this out for various reasons. The most important one is # that a real flaskext could be in there which would disable our # fake package. Secondly we want to make sure that the flaskext @@ -58,12 +57,11 @@ def importhook_setup(monkeypatch, request): import_hooks += 1 assert import_hooks == 1 - def teardown(): - from flask import ext - for key in ext.__dict__: - assert '.' not in key + yield - request.addfinalizer(teardown) + from flask import ext + for key in ext.__dict__: + assert '.' not in key @pytest.fixture From 668061a5fc928a5055815acf818b02baf1aef37b Mon Sep 17 00:00:00 2001 From: Florian Sachs Date: Sun, 8 Jan 2017 20:42:41 +0100 Subject: [PATCH 104/131] Register errorhandlers for Exceptions Allow a default errorhandler by registering an errorhandler for HTTPException tests included --- flask/app.py | 11 ++++++++++- tests/test_user_error_handler.py | 25 ++++++++++++++++++++++++- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index 13a56c68..1251d2fb 100644 --- a/flask/app.py +++ b/flask/app.py @@ -1484,7 +1484,16 @@ class Flask(_PackageBoundObject): return handler # fall back to app handlers - return find_handler(self.error_handler_spec[None].get(code)) + handler = find_handler(self.error_handler_spec[None].get(code)) + if handler is not None: + return handler + + try: + handler = find_handler(self.error_handler_spec[None][None]) + except KeyError: + handler = None + + return handler def handle_http_exception(self, e): """Handles an HTTP exception. By default this will invoke the diff --git a/tests/test_user_error_handler.py b/tests/test_user_error_handler.py index 11677433..b6d4ca34 100644 --- a/tests/test_user_error_handler.py +++ b/tests/test_user_error_handler.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from werkzeug.exceptions import Forbidden, InternalServerError +from werkzeug.exceptions import Forbidden, InternalServerError, HTTPException, NotFound import flask @@ -32,6 +32,29 @@ def test_error_handler_no_match(): assert c.get('/keyerror').data == b'KeyError' +def test_default_error_handler(): + app = flask.Flask(__name__) + + @app.errorhandler(HTTPException) + def catchall_errorhandler(e): + assert isinstance(e, HTTPException) + assert isinstance(e, NotFound) + return 'default' + + @app.errorhandler(Forbidden) + def catchall_errorhandler(e): + assert isinstance(e, Forbidden) + return 'forbidden' + + @app.route('/forbidden') + def forbidden(): + raise Forbidden() + + c = app.test_client() + assert c.get('/undefined').data == b'default' + assert c.get('/forbidden').data == b'forbidden' + + def test_error_handler_subclass(): app = flask.Flask(__name__) From 637eae548935ad1434459c1afac286c8686abeed Mon Sep 17 00:00:00 2001 From: cerickson Date: Mon, 22 May 2017 15:57:31 -0700 Subject: [PATCH 105/131] Added support for generic HTTPException handlers on app and blueprints Error handlers are now returned in order of blueprint:code, app:code, blueprint:HTTPException, app:HTTPException, None Corresponding tests also added. Ref issue #941, pr #1383, #2082, #2144 --- flask/app.py | 33 ++++++------- tests/test_user_error_handler.py | 80 ++++++++++++++++++++++---------- 2 files changed, 69 insertions(+), 44 deletions(-) diff --git a/flask/app.py b/flask/app.py index 1251d2fb..f4c73978 100644 --- a/flask/app.py +++ b/flask/app.py @@ -133,6 +133,8 @@ class Flask(_PackageBoundObject): :param static_folder: the folder with static files that should be served at `static_url_path`. Defaults to the ``'static'`` folder in the root path of the application. + folder in the root path of the application. Defaults + to None. :param host_matching: sets the app's ``url_map.host_matching`` to the given given value. Defaults to False. :param static_host: the host to use when adding the static route. Defaults @@ -1460,15 +1462,17 @@ class Flask(_PackageBoundObject): return f def _find_error_handler(self, e): - """Finds a registered error handler for the request’s blueprint. - Otherwise falls back to the app, returns None if not a suitable - handler is found. + """Find a registered error handler for a request in this order: + blueprint handler for a specific code, app handler for a specific code, + blueprint generic HTTPException handler, app generic HTTPException handler, + and returns None if a suitable handler is not found. """ exc_class, code = self._get_exc_class_and_code(type(e)) def find_handler(handler_map): if not handler_map: return + for cls in exc_class.__mro__: handler = handler_map.get(cls) if handler is not None: @@ -1476,24 +1480,13 @@ class Flask(_PackageBoundObject): handler_map[exc_class] = handler return handler - # try blueprint handlers - handler = find_handler(self.error_handler_spec - .get(request.blueprint, {}) - .get(code)) - if handler is not None: - return handler - - # fall back to app handlers - handler = find_handler(self.error_handler_spec[None].get(code)) - if handler is not None: - return handler - - try: - handler = find_handler(self.error_handler_spec[None][None]) - except KeyError: - handler = None + # check for any in blueprint or app + for name, c in ((request.blueprint, code), (None, code), + (request.blueprint, None), (None, None)): + handler = find_handler(self.error_handler_spec.get(name, {}).get(c)) - return handler + if handler: + return handler def handle_http_exception(self, e): """Handles an HTTP exception. By default this will invoke the diff --git a/tests/test_user_error_handler.py b/tests/test_user_error_handler.py index b6d4ca34..24ffe73f 100644 --- a/tests/test_user_error_handler.py +++ b/tests/test_user_error_handler.py @@ -1,5 +1,10 @@ # -*- coding: utf-8 -*- -from werkzeug.exceptions import Forbidden, InternalServerError, HTTPException, NotFound +from werkzeug.exceptions import ( + Forbidden, + InternalServerError, + HTTPException, + NotFound + ) import flask @@ -32,29 +37,6 @@ def test_error_handler_no_match(): assert c.get('/keyerror').data == b'KeyError' -def test_default_error_handler(): - app = flask.Flask(__name__) - - @app.errorhandler(HTTPException) - def catchall_errorhandler(e): - assert isinstance(e, HTTPException) - assert isinstance(e, NotFound) - return 'default' - - @app.errorhandler(Forbidden) - def catchall_errorhandler(e): - assert isinstance(e, Forbidden) - return 'forbidden' - - @app.route('/forbidden') - def forbidden(): - raise Forbidden() - - c = app.test_client() - assert c.get('/undefined').data == b'default' - assert c.get('/forbidden').data == b'forbidden' - - def test_error_handler_subclass(): app = flask.Flask(__name__) @@ -161,3 +143,53 @@ def test_error_handler_blueprint(): assert c.get('/error').data == b'app-error' assert c.get('/bp/error').data == b'bp-error' + + +def test_default_error_handler(): + bp = flask.Blueprint('bp', __name__) + + @bp.errorhandler(HTTPException) + def bp_exception_handler(e): + assert isinstance(e, HTTPException) + assert isinstance(e, NotFound) + return 'bp-default' + + @bp.errorhandler(Forbidden) + def bp_exception_handler(e): + assert isinstance(e, Forbidden) + return 'bp-forbidden' + + @bp.route('/undefined') + def bp_registered_test(): + raise NotFound() + + @bp.route('/forbidden') + def bp_forbidden_test(): + raise Forbidden() + + app = flask.Flask(__name__) + + @app.errorhandler(HTTPException) + def catchall_errorhandler(e): + assert isinstance(e, HTTPException) + assert isinstance(e, NotFound) + return 'default' + + @app.errorhandler(Forbidden) + def catchall_errorhandler(e): + assert isinstance(e, Forbidden) + return 'forbidden' + + @app.route('/forbidden') + def forbidden(): + raise Forbidden() + + app.register_blueprint(bp, url_prefix='/bp') + + c = app.test_client() + assert c.get('/bp/undefined').data == b'bp-default' + assert c.get('/bp/forbidden').data == b'bp-forbidden' + assert c.get('/undefined').data == b'default' + assert c.get('/forbidden').data == b'forbidden' + + From 4f815015b803a146ddc24a2dfadd9ed9b22d7de7 Mon Sep 17 00:00:00 2001 From: cerickson Date: Mon, 22 May 2017 15:57:31 -0700 Subject: [PATCH 106/131] Added support for generic HTTPException handlers on app and blueprints Error handlers are now returned in order of blueprint:code, app:code, blueprint:HTTPException, app:HTTPException, None Corresponding tests also added. Ref issue #941, pr #1383, #2082, #2144 --- flask/app.py | 33 ++++++------- tests/test_user_error_handler.py | 80 ++++++++++++++++++++++---------- 2 files changed, 69 insertions(+), 44 deletions(-) diff --git a/flask/app.py b/flask/app.py index 1251d2fb..f4c73978 100644 --- a/flask/app.py +++ b/flask/app.py @@ -133,6 +133,8 @@ class Flask(_PackageBoundObject): :param static_folder: the folder with static files that should be served at `static_url_path`. Defaults to the ``'static'`` folder in the root path of the application. + folder in the root path of the application. Defaults + to None. :param host_matching: sets the app's ``url_map.host_matching`` to the given given value. Defaults to False. :param static_host: the host to use when adding the static route. Defaults @@ -1460,15 +1462,17 @@ class Flask(_PackageBoundObject): return f def _find_error_handler(self, e): - """Finds a registered error handler for the request’s blueprint. - Otherwise falls back to the app, returns None if not a suitable - handler is found. + """Find a registered error handler for a request in this order: + blueprint handler for a specific code, app handler for a specific code, + blueprint generic HTTPException handler, app generic HTTPException handler, + and returns None if a suitable handler is not found. """ exc_class, code = self._get_exc_class_and_code(type(e)) def find_handler(handler_map): if not handler_map: return + for cls in exc_class.__mro__: handler = handler_map.get(cls) if handler is not None: @@ -1476,24 +1480,13 @@ class Flask(_PackageBoundObject): handler_map[exc_class] = handler return handler - # try blueprint handlers - handler = find_handler(self.error_handler_spec - .get(request.blueprint, {}) - .get(code)) - if handler is not None: - return handler - - # fall back to app handlers - handler = find_handler(self.error_handler_spec[None].get(code)) - if handler is not None: - return handler - - try: - handler = find_handler(self.error_handler_spec[None][None]) - except KeyError: - handler = None + # check for any in blueprint or app + for name, c in ((request.blueprint, code), (None, code), + (request.blueprint, None), (None, None)): + handler = find_handler(self.error_handler_spec.get(name, {}).get(c)) - return handler + if handler: + return handler def handle_http_exception(self, e): """Handles an HTTP exception. By default this will invoke the diff --git a/tests/test_user_error_handler.py b/tests/test_user_error_handler.py index b6d4ca34..24ffe73f 100644 --- a/tests/test_user_error_handler.py +++ b/tests/test_user_error_handler.py @@ -1,5 +1,10 @@ # -*- coding: utf-8 -*- -from werkzeug.exceptions import Forbidden, InternalServerError, HTTPException, NotFound +from werkzeug.exceptions import ( + Forbidden, + InternalServerError, + HTTPException, + NotFound + ) import flask @@ -32,29 +37,6 @@ def test_error_handler_no_match(): assert c.get('/keyerror').data == b'KeyError' -def test_default_error_handler(): - app = flask.Flask(__name__) - - @app.errorhandler(HTTPException) - def catchall_errorhandler(e): - assert isinstance(e, HTTPException) - assert isinstance(e, NotFound) - return 'default' - - @app.errorhandler(Forbidden) - def catchall_errorhandler(e): - assert isinstance(e, Forbidden) - return 'forbidden' - - @app.route('/forbidden') - def forbidden(): - raise Forbidden() - - c = app.test_client() - assert c.get('/undefined').data == b'default' - assert c.get('/forbidden').data == b'forbidden' - - def test_error_handler_subclass(): app = flask.Flask(__name__) @@ -161,3 +143,53 @@ def test_error_handler_blueprint(): assert c.get('/error').data == b'app-error' assert c.get('/bp/error').data == b'bp-error' + + +def test_default_error_handler(): + bp = flask.Blueprint('bp', __name__) + + @bp.errorhandler(HTTPException) + def bp_exception_handler(e): + assert isinstance(e, HTTPException) + assert isinstance(e, NotFound) + return 'bp-default' + + @bp.errorhandler(Forbidden) + def bp_exception_handler(e): + assert isinstance(e, Forbidden) + return 'bp-forbidden' + + @bp.route('/undefined') + def bp_registered_test(): + raise NotFound() + + @bp.route('/forbidden') + def bp_forbidden_test(): + raise Forbidden() + + app = flask.Flask(__name__) + + @app.errorhandler(HTTPException) + def catchall_errorhandler(e): + assert isinstance(e, HTTPException) + assert isinstance(e, NotFound) + return 'default' + + @app.errorhandler(Forbidden) + def catchall_errorhandler(e): + assert isinstance(e, Forbidden) + return 'forbidden' + + @app.route('/forbidden') + def forbidden(): + raise Forbidden() + + app.register_blueprint(bp, url_prefix='/bp') + + c = app.test_client() + assert c.get('/bp/undefined').data == b'bp-default' + assert c.get('/bp/forbidden').data == b'bp-forbidden' + assert c.get('/undefined').data == b'default' + assert c.get('/forbidden').data == b'forbidden' + + From 361dba7e3af06be8f81c7bdd5067c59e08df2df9 Mon Sep 17 00:00:00 2001 From: cerickson Date: Tue, 23 May 2017 10:49:01 -0700 Subject: [PATCH 107/131] removed dupe text from merge --- flask/app.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/flask/app.py b/flask/app.py index f4c73978..6e45774e 100644 --- a/flask/app.py +++ b/flask/app.py @@ -133,8 +133,6 @@ class Flask(_PackageBoundObject): :param static_folder: the folder with static files that should be served at `static_url_path`. Defaults to the ``'static'`` folder in the root path of the application. - folder in the root path of the application. Defaults - to None. :param host_matching: sets the app's ``url_map.host_matching`` to the given given value. Defaults to False. :param static_host: the host to use when adding the static route. Defaults From 532ca2e08961b6ffd925c28581c45795986755ee Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 23 May 2017 11:18:48 -0700 Subject: [PATCH 108/131] reorganize git instructions --- CONTRIBUTING.rst | 58 ++++++++++++++++++++++++++++-------------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 80b6af35..22828608 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -24,32 +24,42 @@ Reporting issues Submitting patches ================== - -- `Make sure you have a github account `_ -- `Download and install latest version of git on your - machine `_ -- `Set up your username in git - `_ -- `Set up your email in git - `_ -- Fork flask to your github account (Click the Fork button) -- `Copy your github fork locally - `_ -- Create a branch to identify the issue you would like to work - on (eg 2287-dry-test-suite) -- Using your favorite editor, make your changes, - `committing as you go `_ -- Include tests if your patch is supposed to solve a bug, and explain - clearly under which circumstances the bug happens. Make sure the test fails - without your patch. -- Try to follow `PEP8 `_, but you - may ignore the line-length-limit if following it would make the code uglier. -- `Run tests. - `_ - When tests pass push changes to github and `create pull request - `_ +First time setup +---------------- + +- Download and install the `latest version of git`_. +- Configure git with your `username`_ and `email`_. +- Make sure you have a `GitHub account`_. +- Fork Flask to your GitHub account by clicking the `Fork`_ button. +- `Clone`_ your GitHub fork locally. +- Add the main repository as a remote to update later. + ``git remote add pallets https://github.com/pallets/flask`` + +.. _GitHub account: https://github.com/join +.. _latest version of git: https://git-scm.com/downloads +.. _username: https://help.github.com/articles/setting-your-username-in-git/ +.. _email: https://help.github.com/articles/setting-your-email-in-git/ +.. _Fork: https://github.com/pallets/flask/pull/2305#fork-destination-box +.. _Clone: https://help.github.com/articles/fork-a-repo/#step-2-create-a-local-clone-of-your-fork + +Start coding +------------ + +- Create a branch to identify the issue you would like to work on (e.g. + ``2287-dry-test-suite``) +- Using your favorite editor, make your changes, `committing as you go`_. +- Try to follow `PEP8`_, but you may ignore the line length limit if following + it would make the code uglier. +- Include tests that cover any code changes you make. Make sure the test fails + without your patch. `Run the tests. `_. +- Push your commits to GitHub and `create a pull request`_. - Celebrate 🎉 +.. _committing as you go: http://dont-be-afraid-to-commit.readthedocs.io/en/latest/git/commandlinegit.html#commit-your-changes +.. _PEP8: https://pep8.org/ +.. _create a pull request: https://help.github.com/articles/creating-a-pull-request/ + +.. _contributing-testsuite: Running the testsuite --------------------- From 954d9ca0b8519ee651daba64ca9292aaa74fff0d Mon Sep 17 00:00:00 2001 From: Levi Roth Date: Tue, 23 May 2017 14:30:39 -0400 Subject: [PATCH 109/131] Added documentation for PowerShell environment variables --- docs/quickstart.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/quickstart.rst b/docs/quickstart.rst index aea776f3..d56fa8e2 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -50,7 +50,14 @@ to tell your terminal the application to work with by exporting the $ flask run * Running on http://127.0.0.1:5000/ -If you are on Windows you need to use ``set`` instead of ``export``. +If you are on Windows, the environment variable syntax depends on command line +interpreter. On Command Prompt:: + + C:\path\to\app>set FLASK_APP=hello.py + +And on PowerShell:: + + PS C:\path\to\app> $env:FLASK_APP = "hello.py" Alternatively you can use :command:`python -m flask`:: From 8858135043a42df9f0728eb548d3272b476100b6 Mon Sep 17 00:00:00 2001 From: David Lord Date: Tue, 23 May 2017 11:50:14 -0700 Subject: [PATCH 110/131] Update testing.rst --- docs/testing.rst | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index 67b8aaea..c00d06e0 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -47,17 +47,23 @@ the application for testing and initializes a new database.:: import os import tempfile + import pytest + from flaskr import flaskr + @pytest.fixture def client(request): db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() flaskr.app.config['TESTING'] = True client = flaskr.app.test_client() + with flaskr.app.app_context(): flaskr.init_db() + yield client + os.close(db_fd) os.unlink(flaskr.app.config['DATABASE']) @@ -77,8 +83,8 @@ low-level file handle and a random file name, the latter we use as database name. We just have to keep the `db_fd` around so that we can use the :func:`os.close` function to close the file. -To delete the database after the test, we close the file and remove it -from the filesystem. +To delete the database after the test, the fixture closes the file and removes +it from the filesystem. If we now run the test suite, we should see the following output:: @@ -107,6 +113,7 @@ test function to :file:`test_flaskr.py`, like this:: def test_empty_db(client): """Start with a blank database.""" + rv = client.get('/') assert b'No entries here so far' in rv.data @@ -148,6 +155,7 @@ Add the following two functions to your :file:`test_flaskr.py` file:: password=password ), follow_redirects=True) + def logout(client): return client.get('/logout', follow_redirects=True) @@ -155,17 +163,18 @@ Now we can easily test that logging in and out works and that it fails with invalid credentials. Add this new test function:: def test_login_logout(client): - """Make sure login and logout works""" - rv = login(client, flaskr.app.config['USERNAME'], - flaskr.app.config['PASSWORD']) + """Make sure login and logout works.""" + + rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD']) assert b'You were logged in' in rv.data + rv = logout(client) assert b'You were logged out' in rv.data - rv = login(client, flaskr.app.config['USERNAME'] + 'x', - flaskr.app.config['PASSWORD']) + + rv = login(client, flaskr.app.config['USERNAME'] + 'x', flaskr.app.config['PASSWORD']) assert b'Invalid username' in rv.data - rv = login(client, flaskr.app.config['USERNAME'], - flaskr.app.config['PASSWORD'] + 'x') + + rv = login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD'] + 'x') assert b'Invalid password' in rv.data Test Adding Messages @@ -175,9 +184,9 @@ We should also test that adding messages works. Add a new test function like this:: def test_messages(client): - """Test that messages work""" - login(client, flaskr.app.config['USERNAME'], - flaskr.app.config['PASSWORD']) + """Test that messages work.""" + + login(client, flaskr.app.config['USERNAME'], flaskr.app.config['PASSWORD']) rv = client.post('/add', data=dict( title='', text='HTML allowed here' @@ -207,11 +216,9 @@ For more complex tests with headers and status codes, check out the `MiniTwit Example`_ from the sources which contains a larger test suite. - .. _MiniTwit Example: https://github.com/pallets/flask/tree/master/examples/minitwit/ - Other Testing Tricks -------------------- From 75f537fb87ea3d60465a82c5c221104bcceef1f2 Mon Sep 17 00:00:00 2001 From: kaveh Date: Tue, 23 May 2017 11:51:13 -0700 Subject: [PATCH 111/131] Adds provide_automatic_options to Class-based Views --- flask/views.py | 4 ++++ tests/test_views.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/flask/views.py b/flask/views.py index 848ccb0b..b3027970 100644 --- a/flask/views.py +++ b/flask/views.py @@ -51,6 +51,9 @@ class View(object): #: A list of methods this view can handle. methods = None + #: Setting this disables or force-enables the automatic options handling. + provide_automatic_options = None + #: The canonical way to decorate class-based views is to decorate the #: return value of as_view(). However since this moves parts of the #: logic from the class declaration to the place where it's hooked @@ -99,6 +102,7 @@ class View(object): view.__doc__ = cls.__doc__ view.__module__ = cls.__module__ view.methods = cls.methods + view.provide_automatic_options = cls.provide_automatic_options return view diff --git a/tests/test_views.py b/tests/test_views.py index 65981dbd..896880c0 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -109,6 +109,43 @@ def test_view_decorators(): assert rv.headers['X-Parachute'] == 'awesome' assert rv.data == b'Awesome' +def test_view_provide_automatic_options_attr(): + app = flask.Flask(__name__) + + class Index1(flask.views.View): + provide_automatic_options = False + def dispatch_request(self): + return 'Hello World!' + + app.add_url_rule('/', view_func=Index1.as_view('index')) + c = app.test_client() + rv = c.open('/', method='OPTIONS') + assert rv.status_code == 405 + + app = flask.Flask(__name__) + + class Index2(flask.views.View): + methods = ['OPTIONS'] + provide_automatic_options = True + def dispatch_request(self): + return 'Hello World!' + + app.add_url_rule('/', view_func=Index2.as_view('index')) + c = app.test_client() + rv = c.open('/', method='OPTIONS') + assert sorted(rv.allow) == ['OPTIONS'] + + app = flask.Flask(__name__) + + class Index3(flask.views.View): + def dispatch_request(self): + return 'Hello World!' + + app.add_url_rule('/', view_func=Index3.as_view('index')) + c = app.test_client() + rv = c.open('/', method='OPTIONS') + assert 'OPTIONS' in rv.allow + def test_implicit_head(): app = flask.Flask(__name__) From fe27d04cc196d65af17ac6cf67574fa61d5b4828 Mon Sep 17 00:00:00 2001 From: bovarysme Date: Tue, 23 May 2017 22:22:16 +0200 Subject: [PATCH 112/131] Fix a small oversight in the testing docs --- docs/testing.rst | 2 +- examples/flaskr/tests/test_flaskr.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/testing.rst b/docs/testing.rst index c00d06e0..fbd3fad5 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -54,7 +54,7 @@ the application for testing and initializes a new database.:: @pytest.fixture - def client(request): + def client(): db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() flaskr.app.config['TESTING'] = True client = flaskr.app.test_client() diff --git a/examples/flaskr/tests/test_flaskr.py b/examples/flaskr/tests/test_flaskr.py index df32cd4b..493067ee 100644 --- a/examples/flaskr/tests/test_flaskr.py +++ b/examples/flaskr/tests/test_flaskr.py @@ -16,7 +16,7 @@ from flaskr import flaskr @pytest.fixture -def client(request): +def client(): db_fd, flaskr.app.config['DATABASE'] = tempfile.mkstemp() flaskr.app.config['TESTING'] = True client = flaskr.app.test_client() From ae41df9a77ceab4469029b043e38333b8654e1da Mon Sep 17 00:00:00 2001 From: Hendrik Makait Date: Tue, 23 May 2017 13:46:45 -0700 Subject: [PATCH 113/131] Check if app factory takes script_info argument and call it with(out) script_info as an argument depending on that --- flask/_compat.py | 2 ++ flask/cli.py | 30 +++++++++++++++++------ tests/test_cli.py | 62 +++++++++++++++++++++++++++++++---------------- 3 files changed, 66 insertions(+), 28 deletions(-) diff --git a/flask/_compat.py b/flask/_compat.py index 071628fc..173b3689 100644 --- a/flask/_compat.py +++ b/flask/_compat.py @@ -25,6 +25,7 @@ if not PY2: itervalues = lambda d: iter(d.values()) iteritems = lambda d: iter(d.items()) + from inspect import getfullargspec as getargspec from io import StringIO def reraise(tp, value, tb=None): @@ -43,6 +44,7 @@ else: itervalues = lambda d: d.itervalues() iteritems = lambda d: d.iteritems() + from inspect import getargspec from cStringIO import StringIO exec('def reraise(tp, value, tb=None):\n raise tp, value, tb') diff --git a/flask/cli.py b/flask/cli.py index 6aa66f4f..ea294ae6 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -22,13 +22,14 @@ from . import __version__ from ._compat import iteritems, reraise from .globals import current_app from .helpers import get_debug_flag +from ._compat import getargspec class NoAppException(click.UsageError): """Raised if an application cannot be found or loaded.""" -def find_best_app(module): +def find_best_app(script_info, module): """Given a module instance this tries to find the best possible application in the module or raises an exception. """ @@ -60,8 +61,8 @@ def find_best_app(module): if callable(app_factory): try: - app = app_factory() - + app = check_factory_for_script_info_and_call(app_factory, + script_info) if isinstance(app, Flask): return app except TypeError: @@ -79,6 +80,21 @@ def find_best_app(module): ) +def check_factory_for_script_info_and_call(func, script_info): + """Given a function this checks if the function has an argument named + script_info or just a single argument and calls the function with + script_info if so. Otherwise calls the function without any arguments and + returns the result.""" + arguments = getargspec(func).args + if 'script_info' in arguments: + result = func(script_info=script_info) + elif len(arguments) == 1: + result = func(script_info) + else: + result = func() + return result + + def prepare_exec_for_file(filename): """Given a filename this will try to calculate the python path, add it to the search path and return the actual module name that is expected. @@ -108,7 +124,7 @@ def prepare_exec_for_file(filename): return '.'.join(module[::-1]) -def locate_app(app_id): +def locate_app(script_info, app_id): """Attempts to locate the application.""" __traceback_hide__ = True if ':' in app_id: @@ -134,7 +150,7 @@ def locate_app(app_id): mod = sys.modules[module] if app_obj is None: - app = find_best_app(mod) + app = find_best_app(script_info, mod) else: app = getattr(mod, app_obj, None) if app is None: @@ -259,7 +275,7 @@ class ScriptInfo(object): if self._loaded_app is not None: return self._loaded_app if self.create_app is not None: - rv = self.create_app(self) + rv = check_factory_for_script_info_and_call(self.create_app, self) else: if not self.app_import_path: raise NoAppException( @@ -267,7 +283,7 @@ class ScriptInfo(object): 'the FLASK_APP environment variable.\n\nFor more ' 'information see ' 'http://flask.pocoo.org/docs/latest/quickstart/') - rv = locate_app(self.app_import_path) + rv = locate_app(self, self.app_import_path) debug = get_debug_flag() if debug is not None: rv.debug = debug diff --git a/tests/test_cli.py b/tests/test_cli.py index bbb6fe58..1c843d61 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -39,60 +39,75 @@ def test_cli_name(test_apps): def test_find_best_app(test_apps): """Test if `find_best_app` behaves as expected with different combinations of input.""" + script_info = ScriptInfo() class Module: app = Flask('appname') - assert find_best_app(Module) == Module.app + assert find_best_app(script_info, Module) == Module.app class Module: application = Flask('appname') - assert find_best_app(Module) == Module.application + assert find_best_app(script_info, Module) == Module.application class Module: myapp = Flask('appname') - assert find_best_app(Module) == Module.myapp + assert find_best_app(script_info, Module) == Module.myapp class Module: @staticmethod def create_app(): return Flask('appname') - assert isinstance(find_best_app(Module), Flask) - assert find_best_app(Module).name == 'appname' + assert isinstance(find_best_app(script_info, Module), Flask) + assert find_best_app(script_info, Module).name == 'appname' + + class Module: + @staticmethod + def create_app(foo): + return Flask('appname') + assert isinstance(find_best_app(script_info, Module), Flask) + assert find_best_app(script_info, Module).name == 'appname' + + class Module: + @staticmethod + def create_app(foo=None, script_info=None): + return Flask('appname') + assert isinstance(find_best_app(script_info, Module), Flask) + assert find_best_app(script_info, Module).name == 'appname' class Module: @staticmethod def make_app(): return Flask('appname') - assert isinstance(find_best_app(Module), Flask) - assert find_best_app(Module).name == 'appname' + assert isinstance(find_best_app(script_info, Module), Flask) + assert find_best_app(script_info, Module).name == 'appname' class Module: myapp = Flask('appname1') @staticmethod def create_app(): return Flask('appname2') - assert find_best_app(Module) == Module.myapp + assert find_best_app(script_info, Module) == Module.myapp class Module: myapp = Flask('appname1') @staticmethod - def create_app(foo): + def create_app(): return Flask('appname2') - assert find_best_app(Module) == Module.myapp + assert find_best_app(script_info, Module) == Module.myapp class Module: pass - pytest.raises(NoAppException, find_best_app, Module) + pytest.raises(NoAppException, find_best_app, script_info, Module) class Module: myapp1 = Flask('appname1') myapp2 = Flask('appname2') - pytest.raises(NoAppException, find_best_app, Module) + pytest.raises(NoAppException, find_best_app, script_info, Module) class Module: @staticmethod - def create_app(foo): + def create_app(foo, bar): return Flask('appname2') - pytest.raises(NoAppException, find_best_app, Module) + pytest.raises(NoAppException, find_best_app, script_info, Module) def test_prepare_exec_for_file(test_apps): @@ -117,13 +132,18 @@ def test_prepare_exec_for_file(test_apps): def test_locate_app(test_apps): """Test of locate_app.""" - assert locate_app("cliapp.app").name == "testapp" - assert locate_app("cliapp.app:testapp").name == "testapp" - assert locate_app("cliapp.multiapp:app1").name == "app1" - pytest.raises(NoAppException, locate_app, "notanpp.py") - pytest.raises(NoAppException, locate_app, "cliapp/app") - pytest.raises(RuntimeError, locate_app, "cliapp.app:notanapp") - pytest.raises(NoAppException, locate_app, "cliapp.importerrorapp") + script_info = ScriptInfo() + assert locate_app(script_info, "cliapp.app").name == "testapp" + assert locate_app(script_info, "cliapp.app:testapp").name == "testapp" + assert locate_app(script_info, "cliapp.multiapp:app1").name == "app1" + pytest.raises(NoAppException, locate_app, + script_info, "notanpp.py") + pytest.raises(NoAppException, locate_app, + script_info, "cliapp/app") + pytest.raises(RuntimeError, locate_app, + script_info, "cliapp.app:notanapp") + pytest.raises(NoAppException, locate_app, + script_info, "cliapp.importerrorapp") def test_find_default_import_path(test_apps, monkeypatch, tmpdir): From 5b0b9717da958fd6325c675d92be4f6667796112 Mon Sep 17 00:00:00 2001 From: Christian Stade-Schuldt Date: Tue, 23 May 2017 15:18:39 -0700 Subject: [PATCH 114/131] DRYing up the test suite using pytest fixtures (#2306) * add fixtures to conftest.py * use fixtures in test_appctx.py * use fixtures in test_blueprints.py * use fixtures in test_depreciations.py * use fixtures in test_regressions.py * use fixtures in test_reqctx.py * use fixtures in test_templating.py * use fixtures in test_user_error_handler.py * use fixtures in test_views.py * use fixtures in test_basics.py * use fixtures in test_helpers.py * use fixtures in test_testing.py * update conftest.py * make docstrings PEP-257 compliant * cleanup * switch dictonary format * use pytest parameterization for test_json_as_unicode --- tests/conftest.py | 52 ++- tests/test_appctx.py | 96 ++--- tests/test_basic.py | 487 +++++++++++------------- tests/test_blueprints.py | 280 ++++++++------ tests/test_deprecations.py | 13 +- tests/test_helpers.py | 615 +++++++++++++++---------------- tests/test_regression.py | 7 +- tests/test_reqctx.py | 51 +-- tests/test_templating.py | 210 ++++++----- tests/test_testing.py | 96 ++--- tests/test_user_error_handler.py | 22 +- tests/test_views.py | 44 ++- 12 files changed, 999 insertions(+), 974 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index eb130db6..40b1e88f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,40 @@ import sys import pkgutil import pytest import textwrap +from flask import Flask as _Flask + + +class Flask(_Flask): + testing = True + secret_key = __name__ + + def make_response(self, rv): + if rv is None: + rv = '' + return super(Flask, self).make_response(rv) + + +@pytest.fixture +def app(): + app = Flask(__name__) + return app + + +@pytest.fixture +def app_ctx(app): + with app.app_context() as ctx: + yield ctx + + +@pytest.fixture +def req_ctx(app): + with app.test_request_context() as ctx: + yield ctx + + +@pytest.fixture +def client(app): + return app.test_client() @pytest.fixture @@ -63,12 +97,13 @@ def limit_loader(request, monkeypatch): def get_loader(*args, **kwargs): return LimitedLoader(old_get_loader(*args, **kwargs)) + monkeypatch.setattr(pkgutil, 'get_loader', get_loader) @pytest.fixture def modules_tmpdir(tmpdir, monkeypatch): - '''A tmpdir added to sys.path''' + """A tmpdir added to sys.path.""" rv = tmpdir.mkdir('modules_tmpdir') monkeypatch.syspath_prepend(str(rv)) return rv @@ -82,10 +117,10 @@ def modules_tmpdir_prefix(modules_tmpdir, monkeypatch): @pytest.fixture def site_packages(modules_tmpdir, monkeypatch): - '''Create a fake site-packages''' + """Create a fake site-packages.""" rv = modules_tmpdir \ - .mkdir('lib')\ - .mkdir('python{x[0]}.{x[1]}'.format(x=sys.version_info))\ + .mkdir('lib') \ + .mkdir('python{x[0]}.{x[1]}'.format(x=sys.version_info)) \ .mkdir('site-packages') monkeypatch.syspath_prepend(str(rv)) return rv @@ -93,8 +128,9 @@ def site_packages(modules_tmpdir, monkeypatch): @pytest.fixture def install_egg(modules_tmpdir, monkeypatch): - '''Generate egg from package name inside base and put the egg into - sys.path''' + """Generate egg from package name inside base and put the egg into + sys.path.""" + def inner(name, base=modules_tmpdir): if not isinstance(name, str): raise ValueError(name) @@ -118,6 +154,7 @@ def install_egg(modules_tmpdir, monkeypatch): egg_path, = modules_tmpdir.join('dist/').listdir() monkeypatch.syspath_prepend(str(egg_path)) return egg_path + return inner @@ -125,6 +162,7 @@ def install_egg(modules_tmpdir, monkeypatch): def purge_module(request): def inner(name): request.addfinalizer(lambda: sys.modules.pop(name, None)) + return inner @@ -132,4 +170,4 @@ def purge_module(request): def catch_deprecation_warnings(recwarn): yield gc.collect() - assert not recwarn.list + assert not recwarn.list, '\n'.join(str(w.message) for w in recwarn.list) diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 13b61eee..7ef7b479 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -14,8 +14,7 @@ import pytest import flask -def test_basic_url_generation(): - app = flask.Flask(__name__) +def test_basic_url_generation(app): app.config['SERVER_NAME'] = 'localhost' app.config['PREFERRED_URL_SCHEME'] = 'https' @@ -27,31 +26,33 @@ def test_basic_url_generation(): rv = flask.url_for('index') assert rv == 'https://localhost/' -def test_url_generation_requires_server_name(): - app = flask.Flask(__name__) + +def test_url_generation_requires_server_name(app): with app.app_context(): with pytest.raises(RuntimeError): flask.url_for('index') + def test_url_generation_without_context_fails(): with pytest.raises(RuntimeError): flask.url_for('index') -def test_request_context_means_app_context(): - app = flask.Flask(__name__) + +def test_request_context_means_app_context(app): with app.test_request_context(): assert flask.current_app._get_current_object() == app assert flask._app_ctx_stack.top is None -def test_app_context_provides_current_app(): - app = flask.Flask(__name__) + +def test_app_context_provides_current_app(app): with app.app_context(): assert flask.current_app._get_current_object() == app assert flask._app_ctx_stack.top is None -def test_app_tearing_down(): + +def test_app_tearing_down(app): cleanup_stuff = [] - app = flask.Flask(__name__) + @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) @@ -61,9 +62,10 @@ def test_app_tearing_down(): assert cleanup_stuff == [None] -def test_app_tearing_down_with_previous_exception(): + +def test_app_tearing_down_with_previous_exception(app): cleanup_stuff = [] - app = flask.Flask(__name__) + @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) @@ -78,9 +80,10 @@ def test_app_tearing_down_with_previous_exception(): assert cleanup_stuff == [None] -def test_app_tearing_down_with_handled_exception(): + +def test_app_tearing_down_with_handled_exception(app): cleanup_stuff = [] - app = flask.Flask(__name__) + @app.teardown_appcontext def cleanup(exception): cleanup_stuff.append(exception) @@ -93,46 +96,49 @@ def test_app_tearing_down_with_handled_exception(): assert cleanup_stuff == [None] -def test_app_ctx_globals_methods(): - app = flask.Flask(__name__) - with app.app_context(): - # get - assert flask.g.get('foo') is None - assert flask.g.get('foo', 'bar') == 'bar' - # __contains__ - assert 'foo' not in flask.g - flask.g.foo = 'bar' - assert 'foo' in flask.g - # setdefault - flask.g.setdefault('bar', 'the cake is a lie') - flask.g.setdefault('bar', 'hello world') - assert flask.g.bar == 'the cake is a lie' - # pop - assert flask.g.pop('bar') == 'the cake is a lie' - with pytest.raises(KeyError): - flask.g.pop('bar') - assert flask.g.pop('bar', 'more cake') == 'more cake' - # __iter__ - assert list(flask.g) == ['foo'] - -def test_custom_app_ctx_globals_class(): + +def test_app_ctx_globals_methods(app, app_ctx): + # get + assert flask.g.get('foo') is None + assert flask.g.get('foo', 'bar') == 'bar' + # __contains__ + assert 'foo' not in flask.g + flask.g.foo = 'bar' + assert 'foo' in flask.g + # setdefault + flask.g.setdefault('bar', 'the cake is a lie') + flask.g.setdefault('bar', 'hello world') + assert flask.g.bar == 'the cake is a lie' + # pop + assert flask.g.pop('bar') == 'the cake is a lie' + with pytest.raises(KeyError): + flask.g.pop('bar') + assert flask.g.pop('bar', 'more cake') == 'more cake' + # __iter__ + assert list(flask.g) == ['foo'] + + +def test_custom_app_ctx_globals_class(app): class CustomRequestGlobals(object): def __init__(self): self.spam = 'eggs' - app = flask.Flask(__name__) + app.app_ctx_globals_class = CustomRequestGlobals with app.app_context(): assert flask.render_template_string('{{ g.spam }}') == 'eggs' -def test_context_refcounts(): + +def test_context_refcounts(app, client): called = [] - app = flask.Flask(__name__) + @app.teardown_request def teardown_req(error=None): called.append('request') + @app.teardown_appcontext def teardown_app(error=None): called.append('app') + @app.route('/') def index(): with flask._app_ctx_stack.top: @@ -141,16 +147,16 @@ def test_context_refcounts(): env = flask._request_ctx_stack.top.request.environ assert env['werkzeug.request'] is not None return u'' - c = app.test_client() - res = c.get('/') + + res = client.get('/') assert res.status_code == 200 assert res.data == b'' assert called == ['request', 'app'] -def test_clean_pop(): +def test_clean_pop(app): + app.testing = False called = [] - app = flask.Flask(__name__) @app.teardown_request def teardown_req(error=None): @@ -166,5 +172,5 @@ def test_clean_pop(): except ZeroDivisionError: pass - assert called == ['test_appctx', 'TEARDOWN'] + assert called == ['conftest', 'TEARDOWN'] assert not flask.current_app diff --git a/tests/test_basic.py b/tests/test_basic.py index 54f4c8e6..9d72f6d1 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -25,20 +25,17 @@ from werkzeug.routing import BuildError import werkzeug.serving -def test_options_work(): - app = flask.Flask(__name__) - +def test_options_work(app, client): @app.route('/', methods=['GET', 'POST']) def index(): return 'Hello World' - rv = app.test_client().open('/', method='OPTIONS') + + rv = client.open('/', method='OPTIONS') assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] assert rv.data == b'' -def test_options_on_multiple_rules(): - app = flask.Flask(__name__) - +def test_options_on_multiple_rules(app, client): @app.route('/', methods=['GET', 'POST']) def index(): return 'Hello World' @@ -46,7 +43,8 @@ def test_options_on_multiple_rules(): @app.route('/', methods=['PUT']) def index_put(): return 'Aha!' - rv = app.test_client().open('/', method='OPTIONS') + + rv = client.open('/', method='OPTIONS') assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST', 'PUT'] @@ -55,6 +53,7 @@ def test_provide_automatic_options_attr(): def index(): return 'Hello World!' + index.provide_automatic_options = False app.route('/')(index) rv = app.test_client().open('/', method='OPTIONS') @@ -64,15 +63,14 @@ def test_provide_automatic_options_attr(): def index2(): return 'Hello World!' + index2.provide_automatic_options = True app.route('/', methods=['OPTIONS'])(index2) rv = app.test_client().open('/', method='OPTIONS') assert sorted(rv.allow) == ['OPTIONS'] -def test_provide_automatic_options_kwarg(): - app = flask.Flask(__name__) - +def test_provide_automatic_options_kwarg(app, client): def index(): return flask.request.method @@ -84,43 +82,39 @@ def test_provide_automatic_options_kwarg(): '/more', view_func=more, methods=['GET', 'POST'], provide_automatic_options=False ) + assert client.get('/').data == b'GET' - c = app.test_client() - assert c.get('/').data == b'GET' - - rv = c.post('/') + rv = client.post('/') assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD'] # Older versions of Werkzeug.test.Client don't have an options method - if hasattr(c, 'options'): - rv = c.options('/') + if hasattr(client, 'options'): + rv = client.options('/') else: - rv = c.open('/', method='OPTIONS') + rv = client.open('/', method='OPTIONS') assert rv.status_code == 405 - rv = c.head('/') + rv = client.head('/') assert rv.status_code == 200 assert not rv.data # head truncates - assert c.post('/more').data == b'POST' - assert c.get('/more').data == b'GET' + assert client.post('/more').data == b'POST' + assert client.get('/more').data == b'GET' - rv = c.delete('/more') + rv = client.delete('/more') assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD', 'POST'] - if hasattr(c, 'options'): - rv = c.options('/more') + if hasattr(client, 'options'): + rv = client.options('/more') else: - rv = c.open('/more', method='OPTIONS') + rv = client.open('/more', method='OPTIONS') assert rv.status_code == 405 -def test_request_dispatching(): - app = flask.Flask(__name__) - +def test_request_dispatching(app, client): @app.route('/') def index(): return flask.request.method @@ -129,32 +123,28 @@ def test_request_dispatching(): def more(): return flask.request.method - c = app.test_client() - assert c.get('/').data == b'GET' - rv = c.post('/') + assert client.get('/').data == b'GET' + rv = client.post('/') assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] - rv = c.head('/') + rv = client.head('/') assert rv.status_code == 200 assert not rv.data # head truncates - assert c.post('/more').data == b'POST' - assert c.get('/more').data == b'GET' - rv = c.delete('/more') + assert client.post('/more').data == b'POST' + assert client.get('/more').data == b'GET' + rv = client.delete('/more') assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] -def test_disallow_string_for_allowed_methods(): - app = flask.Flask(__name__) +def test_disallow_string_for_allowed_methods(app): with pytest.raises(TypeError): @app.route('/', methods='GET POST') def index(): return "Hey" -def test_url_mapping(): - app = flask.Flask(__name__) - +def test_url_mapping(app, client): random_uuid4 = "7eb41166-9ebf-4d26-b771-ea3f54f8b383" def index(): @@ -166,34 +156,31 @@ def test_url_mapping(): def options(): return random_uuid4 - app.add_url_rule('/', 'index', index) app.add_url_rule('/more', 'more', more, methods=['GET', 'POST']) # Issue 1288: Test that automatic options are not added when non-uppercase 'options' in methods app.add_url_rule('/options', 'options', options, methods=['options']) - c = app.test_client() - assert c.get('/').data == b'GET' - rv = c.post('/') + assert client.get('/').data == b'GET' + rv = client.post('/') assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS'] - rv = c.head('/') + rv = client.head('/') assert rv.status_code == 200 assert not rv.data # head truncates - assert c.post('/more').data == b'POST' - assert c.get('/more').data == b'GET' - rv = c.delete('/more') + assert client.post('/more').data == b'POST' + assert client.get('/more').data == b'GET' + rv = client.delete('/more') assert rv.status_code == 405 assert sorted(rv.allow) == ['GET', 'HEAD', 'OPTIONS', 'POST'] - rv = c.open('/options', method='OPTIONS') + rv = client.open('/options', method='OPTIONS') assert rv.status_code == 200 assert random_uuid4 in rv.data.decode("utf-8") -def test_werkzeug_routing(): +def test_werkzeug_routing(app, client): from werkzeug.routing import Submount, Rule - app = flask.Flask(__name__) app.url_map.add(Submount('/foo', [ Rule('/bar', endpoint='bar'), Rule('/', endpoint='index') @@ -204,17 +191,16 @@ def test_werkzeug_routing(): def index(): return 'index' + app.view_functions['bar'] = bar app.view_functions['index'] = index - c = app.test_client() - assert c.get('/foo/').data == b'index' - assert c.get('/foo/bar').data == b'bar' + assert client.get('/foo/').data == b'index' + assert client.get('/foo/bar').data == b'bar' -def test_endpoint_decorator(): +def test_endpoint_decorator(app, client): from werkzeug.routing import Submount, Rule - app = flask.Flask(__name__) app.url_map.add(Submount('/foo', [ Rule('/bar', endpoint='bar'), Rule('/', endpoint='index') @@ -228,13 +214,11 @@ def test_endpoint_decorator(): def index(): return 'index' - c = app.test_client() - assert c.get('/foo/').data == b'index' - assert c.get('/foo/bar').data == b'bar' + assert client.get('/foo/').data == b'index' + assert client.get('/foo/bar').data == b'bar' -def test_session(): - app = flask.Flask(__name__) +def test_session(app, client): app.secret_key = 'testkey' @app.route('/set', methods=['POST']) @@ -246,13 +230,11 @@ def test_session(): def get(): return flask.session['value'] - c = app.test_client() - assert c.post('/set', data={'value': '42'}).data == b'value set' - assert c.get('/get').data == b'42' + assert client.post('/set', data={'value': '42'}).data == b'value set' + assert client.get('/get').data == b'42' -def test_session_using_server_name(): - app = flask.Flask(__name__) +def test_session_using_server_name(app, client): app.config.update( SECRET_KEY='foo', SERVER_NAME='example.com' @@ -262,13 +244,13 @@ def test_session_using_server_name(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://example.com/') + + rv = client.get('/', 'http://example.com/') assert 'domain=.example.com' in rv.headers['set-cookie'].lower() assert 'httponly' in rv.headers['set-cookie'].lower() -def test_session_using_server_name_and_port(): - app = flask.Flask(__name__) +def test_session_using_server_name_and_port(app, client): app.config.update( SECRET_KEY='foo', SERVER_NAME='example.com:8080' @@ -278,13 +260,13 @@ def test_session_using_server_name_and_port(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://example.com:8080/') + + rv = client.get('/', 'http://example.com:8080/') assert 'domain=.example.com' in rv.headers['set-cookie'].lower() assert 'httponly' in rv.headers['set-cookie'].lower() -def test_session_using_server_name_port_and_path(): - app = flask.Flask(__name__) +def test_session_using_server_name_port_and_path(app, client): app.config.update( SECRET_KEY='foo', SERVER_NAME='example.com:8080', @@ -295,15 +277,15 @@ def test_session_using_server_name_port_and_path(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://example.com:8080/foo') + + rv = client.get('/', 'http://example.com:8080/foo') assert 'domain=example.com' in rv.headers['set-cookie'].lower() assert 'path=/foo' in rv.headers['set-cookie'].lower() assert 'httponly' in rv.headers['set-cookie'].lower() -def test_session_using_application_root(): +def test_session_using_application_root(app, client): class PrefixPathMiddleware(object): - def __init__(self, app, prefix): self.app = app self.prefix = prefix @@ -312,7 +294,6 @@ def test_session_using_application_root(): 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', @@ -323,12 +304,12 @@ def test_session_using_application_root(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://example.com:8080/') + + rv = client.get('/', 'http://example.com:8080/') assert 'path=/bar' in rv.headers['set-cookie'].lower() -def test_session_using_session_settings(): - app = flask.Flask(__name__) +def test_session_using_session_settings(app, client): app.config.update( SECRET_KEY='foo', SERVER_NAME='www.example.com:8080', @@ -343,7 +324,8 @@ def test_session_using_session_settings(): def index(): flask.session['testing'] = 42 return 'Hello World' - rv = app.test_client().get('/', 'http://www.example.com:8080/test/') + + rv = client.get('/', 'http://www.example.com:8080/test/') cookie = rv.headers['set-cookie'].lower() assert 'domain=.example.com' in cookie assert 'path=/' in cookie @@ -351,8 +333,7 @@ def test_session_using_session_settings(): assert 'httponly' not in cookie -def test_session_localhost_warning(recwarn): - app = flask.Flask(__name__) +def test_session_localhost_warning(recwarn, app, client): app.config.update( SECRET_KEY='testing', SERVER_NAME='localhost:5000', @@ -363,14 +344,13 @@ def test_session_localhost_warning(recwarn): flask.session['testing'] = 42 return 'testing' - rv = app.test_client().get('/', 'http://localhost:5000/') + rv = client.get('/', 'http://localhost:5000/') assert 'domain' not in rv.headers['set-cookie'].lower() w = recwarn.pop(UserWarning) assert '"localhost" is not a valid cookie domain' in str(w.message) -def test_session_ip_warning(recwarn): - app = flask.Flask(__name__) +def test_session_ip_warning(recwarn, app, client): app.config.update( SECRET_KEY='testing', SERVER_NAME='127.0.0.1:5000', @@ -381,7 +361,7 @@ def test_session_ip_warning(recwarn): flask.session['testing'] = 42 return 'testing' - rv = app.test_client().get('/', 'http://127.0.0.1:5000/') + rv = client.get('/', 'http://127.0.0.1:5000/') assert 'domain=127.0.0.1' in rv.headers['set-cookie'].lower() w = recwarn.pop(UserWarning) assert 'cookie domain is an IP' in str(w.message) @@ -393,15 +373,15 @@ def test_missing_session(): def expect_exception(f, *args, **kwargs): e = pytest.raises(RuntimeError, f, *args, **kwargs) assert e.value.args and 'session is unavailable' in e.value.args[0] + with app.test_request_context(): 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(): +def test_session_expiration(app, client): permanent = True - app = flask.Flask(__name__) app.secret_key = 'testkey' @app.route('/') @@ -414,7 +394,6 @@ def test_session_expiration(): def test(): return text_type(flask.session.permanent) - client = app.test_client() rv = client.get('/') assert 'set-cookie' in rv.headers match = re.search(r'(?i)\bexpires=([^;]+)', rv.headers['set-cookie']) @@ -428,16 +407,14 @@ def test_session_expiration(): assert rv.data == b'True' permanent = False - rv = app.test_client().get('/') + rv = client.get('/') assert 'set-cookie' in rv.headers match = re.search(r'\bexpires=([^;]+)', rv.headers['set-cookie']) assert match is None -def test_session_stored_last(): - app = flask.Flask(__name__) +def test_session_stored_last(app, client): app.secret_key = 'development-key' - app.testing = True @app.after_request def modify_session(response): @@ -448,15 +425,12 @@ def test_session_stored_last(): def dump_session_contents(): return repr(flask.session.get('foo')) - c = app.test_client() - assert c.get('/').data == b'None' - assert c.get('/').data == b'42' + assert client.get('/').data == b'None' + assert client.get('/').data == b'42' -def test_session_special_types(): - app = flask.Flask(__name__) +def test_session_special_types(app, client): app.secret_key = 'development-key' - app.testing = True now = datetime.utcnow().replace(microsecond=0) the_uuid = uuid.uuid4() @@ -473,9 +447,8 @@ def test_session_special_types(): def dump_session_contents(): return pickle.dumps(dict(flask.session)) - c = app.test_client() - c.get('/') - rv = pickle.loads(c.get('/').data) + client.get('/') + rv = pickle.loads(client.get('/').data) assert rv['m'] == flask.Markup('Hello!') assert type(rv['m']) == flask.Markup assert rv['dt'] == now @@ -485,9 +458,7 @@ def test_session_special_types(): assert rv['t'] == (1, 2, 3) -def test_session_cookie_setting(): - app = flask.Flask(__name__) - app.testing = True +def test_session_cookie_setting(app): app.secret_key = 'dev key' is_permanent = True @@ -529,8 +500,7 @@ def test_session_cookie_setting(): run_test(expect_header=False) -def test_session_vary_cookie(): - app = flask.Flask(__name__) +def test_session_vary_cookie(app, client): app.secret_key = 'testkey' @app.route('/set') @@ -554,10 +524,8 @@ def test_session_vary_cookie(): def no_vary_header(): return '' - c = app.test_client() - def expect(path, header=True): - rv = c.get(path) + rv = client.get(path) if header: assert rv.headers['Vary'] == 'Cookie' @@ -571,29 +539,25 @@ def test_session_vary_cookie(): expect('/no-vary-header', False) -def test_flashes(): - app = flask.Flask(__name__) +def test_flashes(app, req_ctx): app.secret_key = 'testkey' - with app.test_request_context(): - assert not flask.session.modified - flask.flash('Zap') - flask.session.modified = False - flask.flash('Zip') - assert flask.session.modified - assert list(flask.get_flashed_messages()) == ['Zap', 'Zip'] + assert not flask.session.modified + flask.flash('Zap') + flask.session.modified = False + flask.flash('Zip') + assert flask.session.modified + assert list(flask.get_flashed_messages()) == ['Zap', 'Zip'] -def test_extended_flashing(): +def test_extended_flashing(app): # Be sure app.testing=True below, else tests can fail silently. # # Specifically, if app.testing is not set to True, the AssertionErrors # in the view functions will cause a 500 response to the test client # instead of propagating exceptions. - app = flask.Flask(__name__) app.secret_key = 'testkey' - app.testing = True @app.route('/') def index(): @@ -651,29 +615,24 @@ def test_extended_flashing(): # Create new test client on each test to clean flashed messages. - c = app.test_client() - c.get('/') - c.get('/test/') - - c = app.test_client() - c.get('/') - c.get('/test_with_categories/') + client = app.test_client() + client.get('/') + client.get('/test_with_categories/') - c = app.test_client() - c.get('/') - c.get('/test_filter/') + client = app.test_client() + client.get('/') + client.get('/test_filter/') - c = app.test_client() - c.get('/') - c.get('/test_filters/') + client = app.test_client() + client.get('/') + client.get('/test_filters/') - c = app.test_client() - c.get('/') - c.get('/test_filters_without_returning_categories/') + client = app.test_client() + client.get('/') + client.get('/test_filters_without_returning_categories/') -def test_request_processing(): - app = flask.Flask(__name__) +def test_request_processing(app, client): evts = [] @app.before_request @@ -691,14 +650,14 @@ def test_request_processing(): assert 'before' in evts assert 'after' not in evts return 'request' + assert 'after' not in evts - rv = app.test_client().get('/').data + rv = client.get('/').data assert 'after' in evts assert rv == b'request|after' -def test_request_preprocessing_early_return(): - app = flask.Flask(__name__) +def test_request_preprocessing_early_return(app, client): evts = [] @app.before_request @@ -720,31 +679,28 @@ def test_request_preprocessing_early_return(): evts.append('index') return "damnit" - rv = app.test_client().get('/').data.strip() + rv = client.get('/').data.strip() assert rv == b'hello' assert evts == [1, 2] -def test_after_request_processing(): - app = flask.Flask(__name__) - app.testing = True - +def test_after_request_processing(app, client): @app.route('/') def index(): @flask.after_this_request def foo(response): response.headers['X-Foo'] = 'a header' return response + return 'Test' - c = app.test_client() - resp = c.get('/') + + resp = client.get('/') assert resp.status_code == 200 assert resp.headers['X-Foo'] == 'a header' -def test_teardown_request_handler(): +def test_teardown_request_handler(app, client): called = [] - app = flask.Flask(__name__) @app.teardown_request def teardown_request(exc): @@ -754,16 +710,15 @@ def test_teardown_request_handler(): @app.route('/') def root(): return "Response" - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.status_code == 200 assert b'Response' in rv.data assert len(called) == 1 -def test_teardown_request_handler_debug_mode(): +def test_teardown_request_handler_debug_mode(app, client): called = [] - app = flask.Flask(__name__) - app.testing = True @app.teardown_request def teardown_request(exc): @@ -773,16 +728,17 @@ def test_teardown_request_handler_debug_mode(): @app.route('/') def root(): return "Response" - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.status_code == 200 assert b'Response' in rv.data assert len(called) == 1 -def test_teardown_request_handler_error(): +def test_teardown_request_handler_error(app, client): called = [] - app = flask.Flask(__name__) app.config['LOGGER_HANDLER_POLICY'] = 'never' + app.testing = False @app.teardown_request def teardown_request1(exc): @@ -811,15 +767,15 @@ def test_teardown_request_handler_error(): @app.route('/') def fails(): 1 // 0 - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.status_code == 500 assert b'Internal Server Error' in rv.data assert len(called) == 2 -def test_before_after_request_order(): +def test_before_after_request_order(app, client): called = [] - app = flask.Flask(__name__) @app.before_request def before1(): @@ -850,14 +806,15 @@ def test_before_after_request_order(): @app.route('/') def index(): return '42' - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'42' assert called == [1, 2, 3, 4, 5, 6] -def test_error_handling(): - app = flask.Flask(__name__) +def test_error_handling(app, client): app.config['LOGGER_HANDLER_POLICY'] = 'never' + app.testing = False @app.errorhandler(404) def not_found(e): @@ -882,21 +839,21 @@ def test_error_handling(): @app.route('/forbidden') def error2(): flask.abort(403) - c = app.test_client() - rv = c.get('/') + + rv = client.get('/') assert rv.status_code == 404 assert rv.data == b'not found' - rv = c.get('/error') + rv = client.get('/error') assert rv.status_code == 500 assert b'internal server error' == rv.data - rv = c.get('/forbidden') + rv = client.get('/forbidden') assert rv.status_code == 403 assert b'forbidden' == rv.data -def test_error_handling_processing(): - app = flask.Flask(__name__) +def test_error_handling_processing(app, client): app.config['LOGGER_HANDLER_POLICY'] = 'never' + app.testing = False @app.errorhandler(500) def internal_server_error(e): @@ -911,32 +868,28 @@ def test_error_handling_processing(): resp.mimetype = 'text/x-special' return resp - with app.test_client() as c: - resp = c.get('/') - assert resp.mimetype == 'text/x-special' - assert resp.data == b'internal server error' + resp = client.get('/') + assert resp.mimetype == 'text/x-special' + assert resp.data == b'internal server error' -def test_baseexception_error_handling(): - app = flask.Flask(__name__) +def test_baseexception_error_handling(app, client): app.config['LOGGER_HANDLER_POLICY'] = 'never' + app.testing = False @app.route('/') def broken_func(): raise KeyboardInterrupt() - with app.test_client() as c: - with pytest.raises(KeyboardInterrupt): - c.get('/') + with pytest.raises(KeyboardInterrupt): + client.get('/') ctx = flask._request_ctx_stack.top assert ctx.preserved assert type(ctx._preserved_exc) is KeyboardInterrupt -def test_before_request_and_routing_errors(): - app = flask.Flask(__name__) - +def test_before_request_and_routing_errors(app, client): @app.before_request def attach_something(): flask.g.something = 'value' @@ -944,17 +897,16 @@ def test_before_request_and_routing_errors(): @app.errorhandler(404) def return_something(error): return flask.g.something, 404 - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.status_code == 404 assert rv.data == b'value' -def test_user_error_handling(): +def test_user_error_handling(app, client): class MyException(Exception): pass - app = flask.Flask(__name__) - @app.errorhandler(MyException) def handle_my_exception(e): assert isinstance(e, MyException) @@ -964,16 +916,13 @@ def test_user_error_handling(): def index(): raise MyException() - c = app.test_client() - assert c.get('/').data == b'42' + assert client.get('/').data == b'42' -def test_http_error_subclass_handling(): +def test_http_error_subclass_handling(app, client): class ForbiddenSubclass(Forbidden): pass - app = flask.Flask(__name__) - @app.errorhandler(ForbiddenSubclass) def handle_forbidden_subclass(e): assert isinstance(e, ForbiddenSubclass) @@ -997,19 +946,16 @@ def test_http_error_subclass_handling(): def index3(): raise Forbidden() - c = app.test_client() - assert c.get('/1').data == b'banana' - assert c.get('/2').data == b'apple' - assert c.get('/3').data == b'apple' + assert client.get('/1').data == b'banana' + assert client.get('/2').data == b'apple' + assert client.get('/3').data == b'apple' -def test_trapping_of_bad_request_key_errors(): - app = flask.Flask(__name__) - app.testing = True - +def test_trapping_of_bad_request_key_errors(app): @app.route('/fail') def fail(): flask.request.form['missing_key'] + c = app.test_client() assert c.get('/fail').status_code == 400 @@ -1020,23 +966,19 @@ def test_trapping_of_bad_request_key_errors(): assert e.errisinstance(BadRequest) -def test_trapping_of_all_http_exceptions(): - app = flask.Flask(__name__) - app.testing = True +def test_trapping_of_all_http_exceptions(app, client): app.config['TRAP_HTTP_EXCEPTIONS'] = True @app.route('/fail') def fail(): flask.abort(404) - c = app.test_client() with pytest.raises(NotFound): - c.get('/fail') + client.get('/fail') -def test_enctype_debug_helper(): +def test_enctype_debug_helper(app, client): from flask.debughelpers import DebugFilesKeyError - app = flask.Flask(__name__) app.debug = True @app.route('/fail', methods=['POST']) @@ -1046,7 +988,7 @@ def test_enctype_debug_helper(): # 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: + with client as c: with pytest.raises(DebugFilesKeyError) as e: c.post('/fail', data={'foo': 'index.txt'}) assert 'no file contents were transmitted' in str(e.value) @@ -1230,7 +1172,7 @@ def test_jsonify_no_prettyprint(): "submsg": "W00t" }, "msg2": "foobar" - } + } rv = flask.make_response( flask.jsonify(uncompressed_msg), 200) @@ -1241,8 +1183,8 @@ def test_jsonify_prettyprint(): app = flask.Flask(__name__) app.config.update({"JSONIFY_PRETTYPRINT_REGULAR": True}) with app.test_request_context(): - compressed_msg = {"msg":{"submsg":"W00t"},"msg2":"foobar"} - pretty_response =\ + compressed_msg = {"msg": {"submsg": "W00t"}, "msg2": "foobar"} + pretty_response = \ b'{\n "msg": {\n "submsg": "W00t"\n }, \n "msg2": "foobar"\n}\n' rv = flask.make_response( @@ -1276,10 +1218,11 @@ def test_url_generation(): @app.route('/hello/', methods=['POST']) def hello(): pass + with app.test_request_context(): assert flask.url_for('hello', name='test x') == '/hello/test%20x' assert flask.url_for('hello', name='test x', _external=True) == \ - 'http://localhost/hello/test%20x' + 'http://localhost/hello/test%20x' def test_build_error_handler(): @@ -1305,6 +1248,7 @@ def test_build_error_handler(): def handler(error, endpoint, values): # Just a test. return '/test_handler/' + app.url_build_error_handlers.append(handler) with app.test_request_context(): assert flask.url_for('spam') == '/test_handler/' @@ -1316,6 +1260,7 @@ def test_build_error_handler_reraise(): # Test a custom handler which reraises the BuildError def handler_raises_build_error(error, endpoint, values): raise error + app.url_build_error_handlers.append(handler_raises_build_error) with app.test_request_context(): @@ -1343,19 +1288,20 @@ def test_custom_converters(): 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('/') def index(args): return '|'.join(args) + c = app.test_client() assert c.get('/1,2,3').data == b'1|2|3' @@ -1368,7 +1314,7 @@ def test_static_files(): assert rv.data.strip() == b'

Hello World!

' with app.test_request_context(): assert flask.url_for('static', filename='index.html') == \ - '/static/index.html' + '/static/index.html' rv.close() @@ -1403,8 +1349,8 @@ def test_static_route_with_host_matching(): assert rv.status_code == 200 rv.close() with app.test_request_context(): - rv = flask.url_for('static', filename='index.html', _external=True) - assert rv == 'http://example.com/static/index.html' + rv = flask.url_for('static', filename='index.html', _external=True) + assert rv == 'http://example.com/static/index.html' # Providing static_host without host_matching=True should error. with pytest.raises(Exception): flask.Flask(__name__, static_host='example.com') @@ -1485,6 +1431,7 @@ def test_exception_propagation(): @app.route('/') def index(): 1 // 0 + c = app.test_client() if config_key is not None: app.config[config_key] = True @@ -1550,7 +1497,7 @@ def test_url_processors(): @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'): + app.url_map.is_endpoint_expecting(endpoint, 'lang_code'): values.setdefault('lang_code', flask.g.lang_code) @app.url_value_preprocessor @@ -1622,6 +1569,7 @@ def test_debug_mode_complains_after_first_request(): @app.route('/') def index(): return 'Awesome' + assert not app.got_first_request assert app.test_client().get('/').data == b'Awesome' with pytest.raises(AssertionError) as e: @@ -1635,6 +1583,7 @@ def test_debug_mode_complains_after_first_request(): @app.route('/foo') def working(): return 'Meh' + assert app.test_client().get('/foo').data == b'Meh' assert app.got_first_request @@ -1646,6 +1595,7 @@ def test_before_first_request_functions(): @app.before_first_request def foo(): got.append(42) + c = app.test_client() c.get('/') assert got == [42] @@ -1683,6 +1633,7 @@ def test_routing_redirect_debugging(): @app.route('/foo/', methods=['GET', 'POST']) def foo(): return 'success' + with app.test_client() as c: with pytest.raises(AssertionError) as e: c.post('/foo', data={}) @@ -1747,8 +1698,7 @@ def test_preserve_only_once(): assert flask._app_ctx_stack.top is None -def test_preserve_remembers_exception(): - app = flask.Flask(__name__) +def test_preserve_remembers_exception(app, client): app.debug = True errors = [] @@ -1764,51 +1714,40 @@ def test_preserve_remembers_exception(): def teardown_handler(exc): errors.append(exc) - c = app.test_client() - # After this failure we did not yet call the teardown handler with pytest.raises(ZeroDivisionError): - c.get('/fail') + client.get('/fail') assert errors == [] # But this request triggers it, and it's an error - c.get('/success') + client.get('/success') assert len(errors) == 2 assert isinstance(errors[0], ZeroDivisionError) # At this point another request does nothing. - c.get('/success') + client.get('/success') assert len(errors) == 3 assert errors[1] is None -def test_get_method_on_g(): - app = flask.Flask(__name__) - app.testing = True - - with app.app_context(): - assert flask.g.get('x') is None - assert flask.g.get('x', 11) == 11 - flask.g.x = 42 - assert flask.g.get('x') == 42 - assert flask.g.x == 42 - +def test_get_method_on_g(app_ctx): + assert flask.g.get('x') is None + assert flask.g.get('x', 11) == 11 + flask.g.x = 42 + assert flask.g.get('x') == 42 + assert flask.g.x == 42 -def test_g_iteration_protocol(): - app = flask.Flask(__name__) - app.testing = True - with app.app_context(): - flask.g.foo = 23 - flask.g.bar = 42 - assert 'foo' in flask.g - assert 'foos' not in flask.g - assert sorted(flask.g) == ['bar', 'foo'] +def test_g_iteration_protocol(app_ctx): + flask.g.foo = 23 + flask.g.bar = 42 + assert 'foo' in flask.g + assert 'foos' not in flask.g + assert sorted(flask.g) == ['bar', 'foo'] -def test_subdomain_basic_support(): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'localhost' +def test_subdomain_basic_support(app, client): + app.config['SERVER_NAME'] = 'localhost.localdomain' @app.route('/') def normal_index(): @@ -1818,57 +1757,49 @@ def test_subdomain_basic_support(): def test_index(): return 'test index' - c = app.test_client() - rv = c.get('/', 'http://localhost/') + rv = client.get('/', 'http://localhost.localdomain/') assert rv.data == b'normal index' - rv = c.get('/', 'http://test.localhost/') + rv = client.get('/', 'http://test.localhost.localdomain/') assert rv.data == b'test index' -def test_subdomain_matching(): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'localhost' +def test_subdomain_matching(app, client): + app.config['SERVER_NAME'] = 'localhost.localdomain' @app.route('/', subdomain='') def index(user): return 'index for %s' % user - c = app.test_client() - rv = c.get('/', 'http://mitsuhiko.localhost/') + rv = client.get('/', 'http://mitsuhiko.localhost.localdomain/') assert rv.data == b'index for mitsuhiko' -def test_subdomain_matching_with_ports(): - app = flask.Flask(__name__) - app.config['SERVER_NAME'] = 'localhost:3000' +def test_subdomain_matching_with_ports(app, client): + app.config['SERVER_NAME'] = 'localhost.localdomain:3000' @app.route('/', subdomain='') def index(user): return 'index for %s' % user - c = app.test_client() - rv = c.get('/', 'http://mitsuhiko.localhost:3000/') + rv = client.get('/', 'http://mitsuhiko.localhost.localdomain:3000/') assert rv.data == b'index for mitsuhiko' -def test_multi_route_rules(): - app = flask.Flask(__name__) - +def test_multi_route_rules(app, client): @app.route('/') @app.route('//') def index(test='a'): return test - rv = app.test_client().open('/') + rv = client.open('/') assert rv.data == b'a' - rv = app.test_client().open('/b/') + rv = client.open('/b/') assert rv.data == b'b' -def test_multi_route_class_views(): +def test_multi_route_class_views(app, client): class View(object): - def __init__(self, app): app.add_url_rule('/', 'index', self.index) app.add_url_rule('//', 'index', self.index) @@ -1876,35 +1807,32 @@ def test_multi_route_class_views(): def index(self, test='a'): return test - app = flask.Flask(__name__) _ = View(app) - rv = app.test_client().open('/') + rv = client.open('/') assert rv.data == b'a' - rv = app.test_client().open('/b/') + rv = client.open('/b/') assert rv.data == b'b' -def test_run_defaults(monkeypatch): +def test_run_defaults(monkeypatch, app): rv = {} # Mocks werkzeug.serving.run_simple method def run_simple_mock(*args, **kwargs): rv['result'] = 'running...' - app = flask.Flask(__name__) monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock) app.run() assert rv['result'] == 'running...' -def test_run_server_port(monkeypatch): +def test_run_server_port(monkeypatch, app): rv = {} # Mocks werkzeug.serving.run_simple method def run_simple_mock(hostname, port, application, *args, **kwargs): rv['result'] = 'running on %s:%s ...' % (hostname, port) - app = flask.Flask(__name__) monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock) hostname, port = 'localhost', 8000 app.run(hostname, port, debug=True) @@ -1912,17 +1840,16 @@ def test_run_server_port(monkeypatch): @pytest.mark.parametrize('host,port,expect_host,expect_port', ( - (None, None, 'pocoo.org', 8080), - ('localhost', None, 'localhost', 8080), - (None, 80, 'pocoo.org', 80), - ('localhost', 80, 'localhost', 80), + (None, None, 'pocoo.org', 8080), + ('localhost', None, 'localhost', 8080), + (None, 80, 'pocoo.org', 80), + ('localhost', 80, 'localhost', 80), )) -def test_run_from_config(monkeypatch, host, port, expect_host, expect_port): +def test_run_from_config(monkeypatch, host, port, expect_host, expect_port, app): def run_simple_mock(hostname, port, *args, **kwargs): assert hostname == expect_host assert port == expect_port monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock) - app = flask.Flask(__name__) app.config['SERVER_NAME'] = 'pocoo.org:8080' app.run(host, port) diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py index 5c5119c0..434fca37 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -18,7 +18,7 @@ from werkzeug.http import parse_cache_control_header from jinja2 import TemplateNotFound -def test_blueprint_specific_error_handling(): +def test_blueprint_specific_error_handling(app, client): frontend = flask.Blueprint('frontend', __name__) backend = flask.Blueprint('backend', __name__) sideend = flask.Blueprint('sideend', __name__) @@ -43,7 +43,6 @@ def test_blueprint_specific_error_handling(): def sideend_no(): flask.abort(403) - app = flask.Flask(__name__) app.register_blueprint(frontend) app.register_blueprint(backend) app.register_blueprint(sideend) @@ -52,15 +51,15 @@ def test_blueprint_specific_error_handling(): def app_forbidden(e): return 'application itself says no', 403 - c = app.test_client() + assert client.get('/frontend-no').data == b'frontend says no' + assert client.get('/backend-no').data == b'backend says no' + assert client.get('/what-is-a-sideend').data == b'application itself says no' - assert c.get('/frontend-no').data == b'frontend says no' - assert c.get('/backend-no').data == b'backend says no' - assert c.get('/what-is-a-sideend').data == b'application itself says no' -def test_blueprint_specific_user_error_handling(): +def test_blueprint_specific_user_error_handling(app, client): class MyDecoratorException(Exception): pass + class MyFunctionException(Exception): pass @@ -74,32 +73,30 @@ def test_blueprint_specific_user_error_handling(): def my_function_exception_handler(e): assert isinstance(e, MyFunctionException) return 'bam' + blue.register_error_handler(MyFunctionException, my_function_exception_handler) @blue.route('/decorator') def blue_deco_test(): raise MyDecoratorException() + @blue.route('/function') def blue_func_test(): raise MyFunctionException() - app = flask.Flask(__name__) app.register_blueprint(blue) - c = app.test_client() + assert client.get('/decorator').data == b'boom' + assert client.get('/function').data == b'bam' - assert c.get('/decorator').data == b'boom' - assert c.get('/function').data == b'bam' -def test_blueprint_app_error_handling(): +def test_blueprint_app_error_handling(app, client): errors = flask.Blueprint('errors', __name__) @errors.app_errorhandler(403) def forbidden_handler(e): return 'you shall not pass', 403 - app = flask.Flask(__name__) - @app.route('/forbidden') def app_forbidden(): flask.abort(403) @@ -113,12 +110,11 @@ def test_blueprint_app_error_handling(): app.register_blueprint(errors) app.register_blueprint(forbidden_bp) - c = app.test_client() + assert client.get('/forbidden').data == b'you shall not pass' + assert client.get('/nope').data == b'you shall not pass' - assert c.get('/forbidden').data == b'you shall not pass' - assert c.get('/nope').data == b'you shall not pass' -def test_blueprint_url_definitions(): +def test_blueprint_url_definitions(app, client): bp = flask.Blueprint('test', __name__) @bp.route('/foo', defaults={'baz': 42}) @@ -129,17 +125,16 @@ def test_blueprint_url_definitions(): def bar(bar): return text_type(bar) - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/1', url_defaults={'bar': 23}) app.register_blueprint(bp, url_prefix='/2', url_defaults={'bar': 19}) - c = app.test_client() - assert c.get('/1/foo').data == b'23/42' - assert c.get('/2/foo').data == b'19/42' - assert c.get('/1/bar').data == b'23' - assert c.get('/2/bar').data == b'19' + assert client.get('/1/foo').data == b'23/42' + assert client.get('/2/foo').data == b'19/42' + assert client.get('/1/bar').data == b'23' + assert client.get('/2/bar').data == b'19' -def test_blueprint_url_processors(): + +def test_blueprint_url_processors(app, client): bp = flask.Blueprint('frontend', __name__, url_prefix='/') @bp.url_defaults @@ -158,28 +153,26 @@ def test_blueprint_url_processors(): def about(): return flask.url_for('.index') - app = flask.Flask(__name__) app.register_blueprint(bp) - c = app.test_client() + assert client.get('/de/').data == b'/de/about' + assert client.get('/de/about').data == b'/de/' - assert c.get('/de/').data == b'/de/about' - assert c.get('/de/about').data == b'/de/' def test_templates_and_static(test_apps): from blueprintapp import app - c = app.test_client() + client = app.test_client() - rv = c.get('/') + rv = client.get('/') assert rv.data == b'Hello from the Frontend' - rv = c.get('/admin/') + rv = client.get('/admin/') assert rv.data == b'Hello from the Admin' - rv = c.get('/admin/index2') + rv = client.get('/admin/index2') assert rv.data == b'Hello from the Admin' - rv = c.get('/admin/static/test.txt') + rv = client.get('/admin/static/test.txt') assert rv.data.strip() == b'Admin File' rv.close() - rv = c.get('/admin/static/css/test.css') + rv = client.get('/admin/static/css/test.css') assert rv.data.strip() == b'/* nested file */' rv.close() @@ -190,7 +183,7 @@ def test_templates_and_static(test_apps): if app.config['SEND_FILE_MAX_AGE_DEFAULT'] == expected_max_age: expected_max_age = 7200 app.config['SEND_FILE_MAX_AGE_DEFAULT'] = expected_max_age - rv = c.get('/admin/static/css/test.css') + rv = client.get('/admin/static/css/test.css') cc = parse_cache_control_header(rv.headers['Cache-Control']) assert cc.max_age == expected_max_age rv.close() @@ -208,8 +201,10 @@ def test_templates_and_static(test_apps): with flask.Flask(__name__).test_request_context(): assert flask.render_template('nested/nested.txt') == 'I\'m nested' + def test_default_static_cache_timeout(): app = flask.Flask(__name__) + class MyBlueprint(flask.Blueprint): def get_send_file_max_age(self, filename): return 100 @@ -232,12 +227,14 @@ def test_default_static_cache_timeout(): finally: app.config['SEND_FILE_MAX_AGE_DEFAULT'] = max_age_default + def test_templates_list(test_apps): from blueprintapp import app templates = sorted(app.jinja_env.list_templates()) assert templates == ['admin/index.html', 'frontend/index.html'] -def test_dotted_names(): + +def test_dotted_names(app, client): frontend = flask.Blueprint('myapp.frontend', __name__) backend = flask.Blueprint('myapp.backend', __name__) @@ -253,18 +250,15 @@ def test_dotted_names(): def backend_index(): return flask.url_for('myapp.frontend.frontend_index') - app = flask.Flask(__name__) app.register_blueprint(frontend) app.register_blueprint(backend) - c = app.test_client() - assert c.get('/fe').data.strip() == b'/be' - assert c.get('/fe2').data.strip() == b'/fe' - assert c.get('/be').data.strip() == b'/fe' + assert client.get('/fe').data.strip() == b'/be' + assert client.get('/fe2').data.strip() == b'/fe' + assert client.get('/be').data.strip() == b'/fe' -def test_dotted_names_from_app(): - app = flask.Flask(__name__) - app.testing = True + +def test_dotted_names_from_app(app, client): test = flask.Blueprint('test', __name__) @app.route('/') @@ -277,11 +271,11 @@ def test_dotted_names_from_app(): app.register_blueprint(test) - with app.test_client() as c: - rv = c.get('/') - assert rv.data == b'/test/' + rv = client.get('/') + assert rv.data == b'/test/' -def test_empty_url_defaults(): + +def test_empty_url_defaults(app, client): bp = flask.Blueprint('bp', __name__) @bp.route('/', defaults={'page': 1}) @@ -289,15 +283,13 @@ def test_empty_url_defaults(): def something(page): return str(page) - app = flask.Flask(__name__) app.register_blueprint(bp) - c = app.test_client() - assert c.get('/').data == b'1' - assert c.get('/page/2').data == b'2' + assert client.get('/').data == b'1' + assert client.get('/page/2').data == b'2' -def test_route_decorator_custom_endpoint(): +def test_route_decorator_custom_endpoint(app, client): bp = flask.Blueprint('bp', __name__) @bp.route('/foo') @@ -316,21 +308,20 @@ def test_route_decorator_custom_endpoint(): def bar_foo(): return flask.request.endpoint - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') @app.route('/') def index(): return flask.request.endpoint - c = app.test_client() - assert c.get('/').data == b'index' - assert c.get('/py/foo').data == b'bp.foo' - assert c.get('/py/bar').data == b'bp.bar' - assert c.get('/py/bar/123').data == b'bp.123' - assert c.get('/py/bar/foo').data == b'bp.bar_foo' + assert client.get('/').data == b'index' + assert client.get('/py/foo').data == b'bp.foo' + assert client.get('/py/bar').data == b'bp.bar' + assert client.get('/py/bar/123').data == b'bp.123' + assert client.get('/py/bar/foo').data == b'bp.bar_foo' + -def test_route_decorator_custom_endpoint_with_dots(): +def test_route_decorator_custom_endpoint_with_dots(app, client): bp = flask.Blueprint('bp', __name__) @bp.route('/foo') @@ -371,21 +362,18 @@ def test_route_decorator_custom_endpoint_with_dots(): lambda: None ) - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') - c = app.test_client() - assert c.get('/py/foo').data == b'bp.foo' + assert client.get('/py/foo').data == b'bp.foo' # The rule's didn't actually made it through - rv = c.get('/py/bar') + rv = client.get('/py/bar') assert rv.status_code == 404 - rv = c.get('/py/bar/123') + rv = client.get('/py/bar/123') assert rv.status_code == 404 -def test_endpoint_decorator(): +def test_endpoint_decorator(app, client): from werkzeug.routing import Rule - app = flask.Flask(__name__) app.url_map.add(Rule('/foo', endpoint='bar')) bp = flask.Blueprint('bp', __name__) @@ -396,229 +384,282 @@ def test_endpoint_decorator(): app.register_blueprint(bp, url_prefix='/bp_prefix') - c = app.test_client() - assert c.get('/foo').data == b'bar' - assert c.get('/bp_prefix/bar').status_code == 404 + assert client.get('/foo').data == b'bar' + assert client.get('/bp_prefix/bar').status_code == 404 -def test_template_filter(): +def test_template_filter(app): bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter() def my_reverse(s): return s[::-1] - app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') assert 'my_reverse' in app.jinja_env.filters.keys() assert app.jinja_env.filters['my_reverse'] == my_reverse assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' -def test_add_template_filter(): + +def test_add_template_filter(app): bp = flask.Blueprint('bp', __name__) + def my_reverse(s): return s[::-1] + bp.add_app_template_filter(my_reverse) - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') assert 'my_reverse' in app.jinja_env.filters.keys() assert app.jinja_env.filters['my_reverse'] == my_reverse assert app.jinja_env.filters['my_reverse']('abcd') == 'dcba' -def test_template_filter_with_name(): + +def test_template_filter_with_name(app): bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter('strrev') def my_reverse(s): return s[::-1] - app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') assert 'strrev' in app.jinja_env.filters.keys() assert app.jinja_env.filters['strrev'] == my_reverse assert app.jinja_env.filters['strrev']('abcd') == 'dcba' -def test_add_template_filter_with_name(): + +def test_add_template_filter_with_name(app): bp = flask.Blueprint('bp', __name__) + def my_reverse(s): return s[::-1] + bp.add_app_template_filter(my_reverse, 'strrev') - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') assert 'strrev' in app.jinja_env.filters.keys() assert app.jinja_env.filters['strrev'] == my_reverse assert app.jinja_env.filters['strrev']('abcd') == 'dcba' -def test_template_filter_with_template(): + +def test_template_filter_with_template(app, client): bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter() def super_reverse(s): return s[::-1] - app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'dcba' -def test_template_filter_after_route_with_template(): - app = flask.Flask(__name__) + +def test_template_filter_after_route_with_template(app, client): @app.route('/') def index(): return flask.render_template('template_filter.html', value='abcd') + bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter() def super_reverse(s): return s[::-1] + app.register_blueprint(bp, url_prefix='/py') - rv = app.test_client().get('/') + rv = client.get('/') assert rv.data == b'dcba' -def test_add_template_filter_with_template(): + +def test_add_template_filter_with_template(app, client): bp = flask.Blueprint('bp', __name__) + def super_reverse(s): return s[::-1] + bp.add_app_template_filter(super_reverse) - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'dcba' -def test_template_filter_with_name_and_template(): + +def test_template_filter_with_name_and_template(app, client): bp = flask.Blueprint('bp', __name__) + @bp.app_template_filter('super_reverse') def my_reverse(s): return s[::-1] - app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'dcba' -def test_add_template_filter_with_name_and_template(): + +def test_add_template_filter_with_name_and_template(app, client): bp = flask.Blueprint('bp', __name__) + def my_reverse(s): return s[::-1] + bp.add_app_template_filter(my_reverse, 'super_reverse') - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_filter.html', value='abcd') - rv = app.test_client().get('/') + + rv = client.get('/') assert rv.data == b'dcba' -def test_template_test(): + +def test_template_test(app): bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() def is_boolean(value): return isinstance(value, bool) - app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') assert 'is_boolean' in app.jinja_env.tests.keys() assert app.jinja_env.tests['is_boolean'] == is_boolean assert app.jinja_env.tests['is_boolean'](False) -def test_add_template_test(): + +def test_add_template_test(app): bp = flask.Blueprint('bp', __name__) + def is_boolean(value): return isinstance(value, bool) + bp.add_app_template_test(is_boolean) - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') assert 'is_boolean' in app.jinja_env.tests.keys() assert app.jinja_env.tests['is_boolean'] == is_boolean assert app.jinja_env.tests['is_boolean'](False) -def test_template_test_with_name(): + +def test_template_test_with_name(app): bp = flask.Blueprint('bp', __name__) + @bp.app_template_test('boolean') def is_boolean(value): return isinstance(value, bool) - app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') assert 'boolean' in app.jinja_env.tests.keys() assert app.jinja_env.tests['boolean'] == is_boolean assert app.jinja_env.tests['boolean'](False) -def test_add_template_test_with_name(): + +def test_add_template_test_with_name(app): bp = flask.Blueprint('bp', __name__) + def is_boolean(value): return isinstance(value, bool) + bp.add_app_template_test(is_boolean, 'boolean') - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') assert 'boolean' in app.jinja_env.tests.keys() assert app.jinja_env.tests['boolean'] == is_boolean assert app.jinja_env.tests['boolean'](False) -def test_template_test_with_template(): + +def test_template_test_with_template(app, client): bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() def boolean(value): return isinstance(value, bool) - app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_test.html', value=False) - rv = app.test_client().get('/') + + rv = client.get('/') assert b'Success!' in rv.data -def test_template_test_after_route_with_template(): - app = flask.Flask(__name__) + +def test_template_test_after_route_with_template(app, client): @app.route('/') def index(): return flask.render_template('template_test.html', value=False) + bp = flask.Blueprint('bp', __name__) + @bp.app_template_test() def boolean(value): return isinstance(value, bool) + app.register_blueprint(bp, url_prefix='/py') - rv = app.test_client().get('/') + rv = client.get('/') assert b'Success!' in rv.data -def test_add_template_test_with_template(): + +def test_add_template_test_with_template(app, client): bp = flask.Blueprint('bp', __name__) + def boolean(value): return isinstance(value, bool) + bp.add_app_template_test(boolean) - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_test.html', value=False) - rv = app.test_client().get('/') + + rv = client.get('/') assert b'Success!' in rv.data -def test_template_test_with_name_and_template(): + +def test_template_test_with_name_and_template(app, client): bp = flask.Blueprint('bp', __name__) + @bp.app_template_test('boolean') def is_boolean(value): return isinstance(value, bool) - app = flask.Flask(__name__) + app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_test.html', value=False) - rv = app.test_client().get('/') + + rv = client.get('/') assert b'Success!' in rv.data -def test_add_template_test_with_name_and_template(): + +def test_add_template_test_with_name_and_template(app, client): bp = flask.Blueprint('bp', __name__) + def is_boolean(value): return isinstance(value, bool) + bp.add_app_template_test(is_boolean, 'boolean') - app = flask.Flask(__name__) app.register_blueprint(bp, url_prefix='/py') + @app.route('/') def index(): return flask.render_template('template_test.html', value=False) - rv = app.test_client().get('/') + + rv = client.get('/') assert b'Success!' in rv.data + def test_context_processing(): app = flask.Flask(__name__) answer_bp = flask.Blueprint('answer_bp', __name__) @@ -661,12 +702,15 @@ def test_context_processing(): assert b'42' in answer_page_bytes assert b'43' in answer_page_bytes + def test_template_global(): app = flask.Flask(__name__) bp = flask.Blueprint('bp', __name__) + @bp.app_template_global() def get_answer(): return 42 + # Make sure the function is not in the jinja_env already assert 'get_answer' not in app.jinja_env.globals.keys() app.register_blueprint(bp) diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index 666f7d56..7383604e 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -15,10 +15,8 @@ import flask class TestRequestDeprecation(object): - - def test_request_json(self, recwarn): + def test_request_json(self, recwarn, app, client): """Request.json is deprecated""" - app = flask.Flask(__name__) app.testing = True @app.route('/', methods=['POST']) @@ -27,13 +25,11 @@ class TestRequestDeprecation(object): print(flask.request.json) return 'OK' - c = app.test_client() - c.post('/', data='{"spam": 42}', content_type='application/json') + client.post('/', data='{"spam": 42}', content_type='application/json') recwarn.pop(DeprecationWarning) - def test_request_module(self, recwarn): + def test_request_module(self, recwarn, app, client): """Request.module is deprecated""" - app = flask.Flask(__name__) app.testing = True @app.route('/') @@ -41,6 +37,5 @@ class TestRequestDeprecation(object): assert flask.request.module is None return 'OK' - c = app.test_client() - c.get('/') + client.get('/') recwarn.pop(DeprecationWarning) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 5af95f18..4259d2d9 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -35,240 +35,226 @@ def has_encoding(name): class TestJSON(object): - - def test_ignore_cached_json(self): - app = flask.Flask(__name__) + def test_ignore_cached_json(self, app): with app.test_request_context('/', method='POST', data='malformed', content_type='application/json'): assert flask.request.get_json(silent=True, cache=True) is None with pytest.raises(BadRequest): flask.request.get_json(silent=False, cache=False) - def test_post_empty_json_adds_exception_to_response_content_in_debug(self): - app = flask.Flask(__name__) + def test_post_empty_json_adds_exception_to_response_content_in_debug(self, app, client): app.config['DEBUG'] = True + @app.route('/json', methods=['POST']) def post_json(): flask.request.get_json() return None - c = app.test_client() - rv = c.post('/json', data=None, content_type='application/json') + + rv = client.post('/json', data=None, content_type='application/json') assert rv.status_code == 400 assert b'Failed to decode JSON object' in rv.data - def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self): - app = flask.Flask(__name__) + def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self, app, client): app.config['DEBUG'] = False + @app.route('/json', methods=['POST']) def post_json(): flask.request.get_json() return None - c = app.test_client() - rv = c.post('/json', data=None, content_type='application/json') + + rv = client.post('/json', data=None, content_type='application/json') assert rv.status_code == 400 assert b'Failed to decode JSON object' not in rv.data - def test_json_bad_requests(self): - app = flask.Flask(__name__) + def test_json_bad_requests(self, app, client): + @app.route('/json', methods=['POST']) def return_json(): return flask.jsonify(foo=text_type(flask.request.get_json())) - c = app.test_client() - rv = c.post('/json', data='malformed', content_type='application/json') + + rv = client.post('/json', data='malformed', content_type='application/json') assert rv.status_code == 400 - def test_json_custom_mimetypes(self): - app = flask.Flask(__name__) + def test_json_custom_mimetypes(self, app, client): + @app.route('/json', methods=['POST']) def return_json(): return flask.request.get_json() - c = app.test_client() - rv = c.post('/json', data='"foo"', content_type='application/x+json') + + rv = client.post('/json', data='"foo"', content_type='application/x+json') assert rv.data == b'foo' - def test_json_body_encoding(self): - app = flask.Flask(__name__) + def test_json_body_encoding(self, app, client): app.testing = True + @app.route('/') def index(): return flask.request.get_json() - c = app.test_client() - resp = c.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), - content_type='application/json; charset=iso-8859-15') + resp = client.get('/', data=u'"Hällo Wörld"'.encode('iso-8859-15'), + content_type='application/json; charset=iso-8859-15') assert resp.data == u'Hällo Wörld'.encode('utf-8') - def test_json_as_unicode(self): - app = flask.Flask(__name__) + @pytest.mark.parametrize('test_value,expected', [(True, '"\\u2603"'), (False, u'"\u2603"')]) + def test_json_as_unicode(self, test_value, expected, app, app_ctx): - app.config['JSON_AS_ASCII'] = True - with app.app_context(): - rv = flask.json.dumps(u'\N{SNOWMAN}') - assert rv == '"\\u2603"' + app.config['JSON_AS_ASCII'] = test_value + rv = flask.json.dumps(u'\N{SNOWMAN}') + assert rv == expected - app.config['JSON_AS_ASCII'] = False - with app.app_context(): - rv = flask.json.dumps(u'\N{SNOWMAN}') - assert rv == u'"\u2603"' - - def test_json_dump_to_file(self): - app = flask.Flask(__name__) + def test_json_dump_to_file(self, app, app_ctx): test_data = {'name': 'Flask'} out = StringIO() - with app.app_context(): - flask.json.dump(test_data, out) - out.seek(0) - rv = flask.json.load(out) - assert rv == test_data + flask.json.dump(test_data, out) + out.seek(0) + rv = flask.json.load(out) + assert rv == test_data @pytest.mark.parametrize('test_value', [0, -1, 1, 23, 3.14, 's', "longer string", True, False, None]) - def test_jsonify_basic_types(self, test_value): + def test_jsonify_basic_types(self, test_value, app, client): """Test jsonify with basic types.""" - app = flask.Flask(__name__) - c = app.test_client() url = '/jsonify_basic_types' app.add_url_rule(url, url, lambda x=test_value: flask.jsonify(x)) - rv = c.get(url) + rv = client.get(url) assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data) == test_value - def test_jsonify_dicts(self): + def test_jsonify_dicts(self, app, client): """Test jsonify with dicts and kwargs unpacking.""" - d = dict( - a=0, b=23, c=3.14, d='t', e='Hi', f=True, g=False, - h=['test list', 10, False], - i={'test':'dict'} - ) - app = flask.Flask(__name__) + d = {'a': 0, 'b': 23, 'c': 3.14, 'd': 't', + 'e': 'Hi', 'f': True, 'g': False, + 'h': ['test list', 10, False], + 'i': {'test': 'dict'}} + @app.route('/kw') def return_kwargs(): return flask.jsonify(**d) + @app.route('/dict') def return_dict(): return flask.jsonify(d) - c = app.test_client() + for url in '/kw', '/dict': - rv = c.get(url) + rv = client.get(url) assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data) == d - def test_jsonify_arrays(self): + def test_jsonify_arrays(self, app, client): """Test jsonify of lists and args unpacking.""" l = [ 0, 42, 3.14, 't', 'hello', True, False, ['test list', 2, False], - {'test':'dict'} + {'test': 'dict'} ] - app = flask.Flask(__name__) + @app.route('/args_unpack') def return_args_unpack(): return flask.jsonify(*l) + @app.route('/array') def return_array(): return flask.jsonify(l) - c = app.test_client() + for url in '/args_unpack', '/array': - rv = c.get(url) + rv = client.get(url) assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data) == l - def test_jsonify_date_types(self): + def test_jsonify_date_types(self, app, client): """Test jsonify with datetime.date and datetime.datetime types.""" test_dates = ( datetime.datetime(1973, 3, 11, 6, 30, 45), datetime.date(1975, 1, 5) ) - app = flask.Flask(__name__) - c = app.test_client() for i, d in enumerate(test_dates): url = '/datetest{0}'.format(i) app.add_url_rule(url, str(i), lambda val=d: flask.jsonify(x=val)) - rv = c.get(url) + rv = client.get(url) assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple()) - def test_jsonify_uuid_types(self): + def test_jsonify_uuid_types(self, app, client): """Test jsonify with uuid.UUID types""" test_uuid = uuid.UUID(bytes=b'\xDE\xAD\xBE\xEF' * 4) - app = flask.Flask(__name__) url = '/uuid_test' app.add_url_rule(url, url, lambda: flask.jsonify(x=test_uuid)) - c = app.test_client() - rv = c.get(url) + rv = client.get(url) rv_x = flask.json.loads(rv.data)['x'] assert rv_x == str(test_uuid) rv_uuid = uuid.UUID(rv_x) assert rv_uuid == test_uuid - def test_json_attr(self): - app = flask.Flask(__name__) + def test_json_attr(self, app, client): + @app.route('/add', methods=['POST']) def add(): json = flask.request.get_json() return text_type(json['a'] + json['b']) - c = app.test_client() - rv = c.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), - content_type='application/json') + + rv = client.post('/add', data=flask.json.dumps({'a': 1, 'b': 2}), + content_type='application/json') assert rv.data == b'3' - def test_template_escaping(self): - app = flask.Flask(__name__) + def test_template_escaping(self, app, req_ctx): render = flask.render_template_string - with app.test_request_context(): - rv = flask.json.htmlsafe_dumps('') - assert rv == u'"\\u003c/script\\u003e"' - assert type(rv) == text_type - rv = render('{{ ""|tojson }}') - assert rv == '"\\u003c/script\\u003e"' - rv = render('{{ "<\0/script>"|tojson }}') - assert rv == '"\\u003c\\u0000/script\\u003e"' - rv = render('{{ "