diff --git a/.github/ISSUE_TEMPLATE.rst b/.github/ISSUE_TEMPLATE.rst index 8854961a..7c85c7fc 100644 --- a/.github/ISSUE_TEMPLATE.rst +++ b/.github/ISSUE_TEMPLATE.rst @@ -1,2 +1,33 @@ -The issue tracker is a tool to address bugs. -Please use the #pocoo IRC channel on freenode or Stack Overflow for questions. +**This issue tracker is a tool to address bugs in Flask itself. +Please use the #pocoo IRC channel on freenode or Stack Overflow for general +questions about using Jinja or issues not related to Jinja.** + +If you'd like to report a bug in Flask, fill out the template below. Provide +any any extra information that may be useful / related to your problem. +Ideally, create an [MCVE](http://stackoverflow.com/help/mcve), which helps us +understand the problem and helps check that it is not caused by something in +your code. + +--- + +### Expected Behavior + +Tell us what should happen. + +```python +Paste a minimal example that causes the problem. +``` + +### Actual Behavior + +Tell us what happens instead. + +```pytb +Paste the full traceback if there was an exception. +``` + +### Environment + +* Python version: +* Flask version: +* Werkzeug version: diff --git a/.github/PULL_REQUEST_TEMPLATE.rst b/.github/PULL_REQUEST_TEMPLATE.rst new file mode 100644 index 00000000..9dda856c --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.rst @@ -0,0 +1,16 @@ +Describe what this patch does to fix the issue. + +Link to any relevant issues or pull requests. + + 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/.travis.yml b/.travis.yml index c4cc421c..9984bc3c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,49 +4,25 @@ language: python matrix: include: - python: 3.6 - env: TOXENV=py-release,codecov + env: TOXENV=py,codecov - python: 3.5 - env: TOXENV=py-release,codecov + env: TOXENV=py,codecov - python: 3.4 - env: TOXENV=py-release,codecov + env: TOXENV=py,codecov - python: 3.3 - env: TOXENV=py-release,codecov + env: TOXENV=py,codecov - python: 2.7 - env: TOXENV=py-release,codecov + env: TOXENV=py,codecov - python: 2.6 - env: TOXENV=py-release,codecov + env: TOXENV=py,codecov - python: pypy - env: TOXENV=py-release,codecov + env: TOXENV=py,codecov - python: nightly - env: TOXENV=py-release + env: TOXENV=py - python: 3.6 env: TOXENV=docs-html - python: 3.6 - env: TOXENV=py-release-simplejson,codecov - - python: 2.7 - env: TOXENV=py-release-simplejson,codecov - - python: pypy - env: TOXENV=py-release-simplejson,codecov - - python: 3.6 - env: TOXENV=py-devel,codecov - - python: 3.3 - env: TOXENV=py-devel,codecov - - python: 2.7 - env: TOXENV=py-devel,codecov - - python: 2.6 - env: TOXENV=py-devel,codecov - - python: pypy - env: TOXENV=py-devel,codecov - - python: 3.6 - env: TOXENV=py-lowest,codecov - - python: 3.3 - env: TOXENV=py-lowest,codecov - - python: 2.7 - env: TOXENV=py-lowest,codecov - - python: 2.6 - env: TOXENV=py-lowest,codecov - - python: pypy - env: TOXENV=py-lowest,codecov + env: TOXENV=py-simplejson,codecov install: - pip install tox diff --git a/CHANGES b/CHANGES index ae70644f..dc39a95d 100644 --- a/CHANGES +++ b/CHANGES @@ -8,16 +8,19 @@ Version 0.13 Major release, unreleased -- Make `app.run()` into a noop if a Flask application is run from the - development server on the command line. This avoids some behavior that +- Minimum Werkzeug version bumped to 0.9, but please use the latest version. +- Minimum Click version bumped to 4, but please use the latest version. +- Make ``app.run()`` into a noop if a Flask application is run from the + development server on the command line. This avoids some behavior that was confusing to debug for newcomers. -- 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``) +- Change default configuration ``JSONIFY_PRETTYPRINT_REGULAR=False``. + ``jsonify()`` method returns compressed response by default, and pretty + response in debug mode. (`#2193`_) +- 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``) - ``send_file`` supports Unicode in ``attachment_filename``. (`#2223`_) - Pass ``_scheme`` argument from ``url_for`` to ``handle_build_error``. (`#2017`_) @@ -46,18 +49,41 @@ Major release, unreleased work with the ``flask`` command. If they take a single parameter or a parameter named ``script_info``, the ``ScriptInfo`` object will be passed. (`#2319`_) +- FLASK_APP=myproject.app:create_app('dev') support. +- ``FLASK_APP`` can be set to an app factory, with arguments if needed, for + example ``FLASK_APP=myproject.app:create_app('dev')``. (`#2326`_) +- ``View.provide_automatic_options = True`` is set on the view function from + ``View.as_view``, to be detected in ``app.add_url_rule``. (`#2316`_) +- Error handling will try handlers registered for ``blueprint, code``, + ``app, code``, ``blueprint, exception``, ``app, exception``. (`#2314`_) +- ``Cookie`` is added to the response's ``Vary`` header if the session is + accessed at all during the request (and it wasn't deleted). (`#2288`_) +- ``app.test_request_context()`` take ``subdomain`` and ``url_scheme`` + parameters for use when building base URL. (`#1621`_) +- Set ``APPLICATION_ROOT = '/'`` by default. This was already the implicit + default when it was set to ``None``. +- ``TRAP_BAD_REQUEST_ERRORS`` is enabled by default in debug mode. + ``BadRequestKeyError`` has a message with the bad key in debug mode instead + of the generic bad request message. (`#2348`_) .. _#1489: https://github.com/pallets/flask/pull/1489 +.. _#1621: https://github.com/pallets/flask/pull/1621 .. _#1898: https://github.com/pallets/flask/pull/1898 .. _#1936: https://github.com/pallets/flask/pull/1936 .. _#2017: https://github.com/pallets/flask/pull/2017 +.. _#2193: https://github.com/pallets/flask/pull/2193 .. _#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 .. _#2282: https://github.com/pallets/flask/pull/2282 +.. _#2288: https://github.com/pallets/flask/pull/2288 .. _#2297: https://github.com/pallets/flask/pull/2297 +.. _#2314: https://github.com/pallets/flask/pull/2314 +.. _#2316: https://github.com/pallets/flask/pull/2316 .. _#2319: https://github.com/pallets/flask/pull/2319 +.. _#2326: https://github.com/pallets/flask/pull/2326 +.. _#2348: https://github.com/pallets/flask/pull/2348 Version 0.12.2 -------------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 86aa7578..f6ff7015 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -1,46 +1,75 @@ -========================== How to contribute to Flask ========================== -Thanks for considering contributing to Flask. +Thank you for considering contributing to Flask! Support questions -================= +----------------- + +Please, don't use the issue tracker for this. Use one of the following +resources for questions about your own code: + +* The IRC channel ``#pocoo`` on FreeNode. +* The IRC channel ``#python`` on FreeNode for more general questions. +* The mailing list flask@python.org for long term discussion or larger issues. +* Ask on `Stack Overflow`_. Search with Google first using: + ``site:stackoverflow.com flask {search term, exception message, etc.}`` -Please, don't use the issue tracker for this. Check whether the ``#pocoo`` IRC -channel on Freenode can help with your issue. If your problem is not strictly -Werkzeug or Flask specific, ``#python`` is generally more active. -`Stack Overflow `_ is also worth considering. +.. _Stack Overflow: https://stackoverflow.com/questions/tagged/flask?sort=linked Reporting issues -================ +---------------- -- Under which versions of Python does this happen? This is even more important - if your issue is encoding related. +- Describe what you expected to happen. +- If possible, include a `minimal, complete, and verifiable example`_ to help + us identify the issue. This also helps check that the issue is not with your + own code. +- Describe what actually happened. Include the full traceback if there was an + exception. +- List your Python, Flask, and Werkzeug versions. If possible, check if this + issue is already fixed in the repository. -- Under which versions of Werkzeug does this happen? Check if this issue is - fixed in the repository. +.. _minimal, complete, and verifiable example: https://stackoverflow.com/help/mcve Submitting patches -================== +------------------ - 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. +- Try to follow `PEP8`_, but you may ignore the line length limit if following + it would make the code uglier. First time setup ----------------- +~~~~~~~~~~~~~~~~ - Download and install the `latest version of git`_. -- Configure git with your `username`_ and `email`_. +- Configure git with your `username`_ and `email`_:: + + git config --global user.name 'your name' + git config --global user.email 'your 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`` +- `Clone`_ your GitHub fork locally:: + + git clone https://github.com/{username}/flask + cd flask + +- Add the main repository as a remote to update later:: + + git remote add pallets https://github.com/pallets/flask + git fetch pallets + +- Create a virtualenv:: + + python3 -m venv env + . env/bin/activate + # or "env\Scripts\activate" on Windows + +- Install Flask in editable mode with development dependencies:: + + pip install -e ".[dev]" .. _GitHub account: https://github.com/join .. _latest version of git: https://git-scm.com/downloads @@ -50,7 +79,7 @@ First time setup .. _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``) @@ -68,98 +97,70 @@ Start coding .. _contributing-testsuite: -Running the testsuite ---------------------- +Running the tests +~~~~~~~~~~~~~~~~~ -You probably want to set up a `virtualenv -`_. +Run the basic test suite with:: -The minimal requirement for running the testsuite is ``pytest``. You can -install it with:: + pytest - pip install pytest +This only runs the tests for the current environment. Whether this is relevant +depends on which part of Flask you're working on. Travis-CI will run the full +suite when you submit your pull request. -Clone this repository:: +The full test suite takes a long time to run because it tests multiple +combinations of Python and dependencies. You need to have Python 2.6, 2.7, 3.3, +3.4, 3.5 3.6, and PyPy 2.7 installed to run all of the environments. Then run:: - git clone https://github.com/pallets/flask.git - -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:: - - pytest tests/ - -**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. - -If you really want to test everything, you will have to install ``tox`` instead -of ``pytest``. You can install it with:: - - pip install tox - -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. + tox Running test coverage ---------------------- -Generating a report of lines that do not have unit test coverage can indicate where -to start contributing. ``pytest`` integrates with ``coverage.py``, using the ``pytest-cov`` -plugin. This assumes you have already run the testsuite (see previous section):: +~~~~~~~~~~~~~~~~~~~~~ - pip install pytest-cov +Generating a report of lines that do not have test coverage can indicate +where to start contributing. Run ``pytest`` using ``coverage`` and generate a +report on the terminal and as an interactive HTML document:: -After this has been installed, you can output a report to the command line using this command:: + coverage run -m pytest + coverage report + coverage html + # then open htmlcov/index.html - pytest --cov=flask tests/ +Read more about `coverage `_. -Generate a HTML report can be done using this command:: +Running the full test suite with ``tox`` will combine the coverage reports +from all runs. - pytest --cov-report html --cov=flask tests/ +``make`` targets +~~~~~~~~~~~~~~~~ -Full docs on ``coverage.py`` are here: https://coverage.readthedocs.io +Flask provides a ``Makefile`` with various shortcuts. They will ensure that +all dependencies are installed. -**Shortcut**: ``make cov`` will ensure ``pytest-cov`` is installed, run it, display the results, *and* save the HTML report. +- ``make test`` runs the basic test suite with ``pytest`` +- ``make cov`` runs the basic test suite with ``coverage`` +- ``make test-all`` runs the full test suite with ``tox`` +- ``make docs`` builds the HTML documentation +Caution: zero-padded file modes +------------------------------- -Caution -======= -pushing -------- -This repository contains several zero-padded file modes that may cause issues when pushing this repository to git hosts other than github. Fixing this is destructive to the commit history, so we suggest ignoring these warnings. If it fails to push and you're using a self-hosted git service like Gitlab, you can turn off repository checks in the admin panel. +This repository contains several zero-padded file modes that may cause issues +when pushing this repository to git hosts other than GitHub. Fixing this is +destructive to the commit history, so we suggest ignoring these warnings. If it +fails to push and you're using a self-hosted git service like GitLab, you can +turn off repository checks in the admin panel. - -cloning -------- -The zero-padded file modes files above can cause issues while cloning, too. If you have - -:: +These files can also cause issues while cloning. If you have :: [fetch] fsckobjects = true -or - -:: +or :: [receive] fsckObjects = true - -set in your git configuration file, cloning this repository will fail. The only solution is to set both of the above settings to false while cloning, and then setting them back to true after the cloning is finished. +set in your git configuration file, cloning this repository will fail. The only +solution is to set both of the above settings to false while cloning, and then +setting them back to true after the cloning is finished. diff --git a/Makefile b/Makefile index 9a0a1696..aef8a782 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,28 @@ -.PHONY: clean-pyc ext-test test tox-test test-with-mem upload-docs docs audit +.PHONY: all install-dev test coverage cov test-all tox docs audit release clean-pyc upload-docs ebook -all: clean-pyc test +all: test -test: - pip install -r test-requirements.txt - tox -e py-release +install-dev: + pip install -q -e .[dev] -cov: - pip install -r test-requirements.txt -q - FLASK_DEBUG= py.test --cov-report term --cov-report html --cov=flask --cov=examples tests examples +test: clean-pyc install-dev + pytest + +coverage: clean-pyc install-dev + pip install -q -e .[test] + coverage run -m pytest + coverage report + coverage html + +cov: coverage + +test-all: install-dev + tox + +tox: test-all + +docs: clean-pyc install-dev + $(MAKE) -C docs html audit: python setup.py audit @@ -16,9 +30,6 @@ audit: release: python scripts/make-release.py -ext-test: - python tests/flaskext_test.py --browse - clean-pyc: find . -name '*.pyc' -exec rm -f {} + find . -name '*.pyo' -exec rm -f {} + @@ -40,6 +51,3 @@ ebook: @echo 'Requires X-forwarding for Qt features used in conversion (ssh -X).' @echo 'Do not mind "Invalid value for ..." CSS errors if .mobi renders.' ssh -X pocoo.org ebook-convert /var/www/flask.pocoo.org/docs/flask-docs.epub /var/www/flask.pocoo.org/docs/flask-docs.mobi --cover http://flask.pocoo.org/docs/_images/logo-full.png --authors 'Armin Ronacher' - -docs: - $(MAKE) -C docs html 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/appcontext.rst b/docs/appcontext.rst index 166c5aa3..976609b6 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -5,31 +5,37 @@ The Application Context .. versionadded:: 0.9 -One of the design ideas behind Flask is that there are two different -“states” in which code is executed. The application setup state in which -the application implicitly is on the module level. It starts when the -:class:`Flask` object is instantiated, and it implicitly ends when the -first request comes in. While the application is in this state a few -assumptions are true: - -- the programmer can modify the application object safely. -- no request handling happened so far -- you have to have a reference to the application object in order to - modify it, there is no magic proxy that can give you a reference to - the application object you're currently creating or modifying. - -In contrast, during request handling, a couple of other rules exist: - -- while a request is active, the context local objects - (:data:`flask.request` and others) point to the current request. -- any code can get hold of these objects at any time. - -There is a third state which is sitting in between a little bit. -Sometimes you are dealing with an application in a way that is similar to -how you interact with applications during request handling; just that there -is no request active. Consider, for instance, that you're sitting in an -interactive Python shell and interacting with the application, or a -command line application. +One of the design ideas behind Flask is that there are at least two +different “states” in which code is executed: + +1. The application setup state, in which the application implicitly is +on the module level. + + This state starts when the :class:`Flask` object is instantiated, and + it implicitly ends when the first request comes in. While the + application is in this state, a few assumptions are true: + + - the programmer can modify the application object safely. + - no request handling happened so far + - you have to have a reference to the application object in order to + modify it, there is no magic proxy that can give you a reference to + the application object you're currently creating or modifying. + +2. In contrast, in the request handling state, a couple of other rules +exist: + + - while a request is active, the context local objects + (:data:`flask.request` and others) point to the current request. + - any code can get hold of these objects at any time. + +3. There is also a third state somewhere in between 'module-level' and +'request-handling': + + Sometimes you are dealing with an application in a way that is similar to + how you interact with applications during request handling, but without + there being an active request. Consider, for instance, that you're + sitting in an interactive Python shell and interacting with the + application, or a command line application. The application context is what powers the :data:`~flask.current_app` context local. diff --git a/docs/conf.py b/docs/conf.py index 47168c65..5f6bd624 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -22,7 +22,6 @@ BUILD_DATE = datetime.datetime.utcfromtimestamp(int(os.environ.get('SOURCE_DATE_ # 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 ----------------------------------------------------- @@ -121,7 +120,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". @@ -273,21 +272,9 @@ intersphinx_mapping = { 'blinker': ('https://pythonhosted.org/blinker/', None) } -try: - __import__('flask_theme_support') - pygments_style = 'flask_theme_support.FlaskyStyle' - 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) - +html_theme_options = { + 'touch_icon': 'touch-icon.png' +} # unwrap decorators def unwrap_decorators(): diff --git a/docs/config.rst b/docs/config.rst index 714b54c8..1da7d2ee 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -3,8 +3,6 @@ Configuration Handling ====================== -.. versionadded:: 0.3 - Applications need some kind of configuration. There are different settings you might want to change depending on the application environment like toggling the debug mode, setting the secret key, and other such @@ -54,182 +52,253 @@ method:: $ flask run (On Windows you need to use ``set`` instead of ``export``). - - ``app.debug`` and ``app.config['DEBUG']`` are not compatible with + + ``app.debug`` and ``app.config['DEBUG']`` are not compatible with   the :command:`flask` script. They only worked when using ``Flask.run()`` method. - + Builtin Configuration Values ---------------------------- The following configuration values are used internally by Flask: -.. tabularcolumns:: |p{6.5cm}|p{8.5cm}| - -================================= ========================================= -``DEBUG`` enable/disable debug mode when using - ``Flask.run()`` method to start server -``TESTING`` enable/disable testing mode -``PROPAGATE_EXCEPTIONS`` explicitly enable or disable the - propagation of exceptions. If not set or - explicitly set to ``None`` this is - implicitly true if either ``TESTING`` or - ``DEBUG`` is true. -``PRESERVE_CONTEXT_ON_EXCEPTION`` By default if the application is in - debug mode the request context is not - popped on exceptions to enable debuggers - to introspect the data. This can be - disabled by this key. You can also use - this setting to force-enable it for non - debug execution which might be useful to - debug production applications (but also - very risky). -``SECRET_KEY`` the secret key -``SESSION_COOKIE_NAME`` the name of the session cookie -``SESSION_COOKIE_DOMAIN`` the domain for the session cookie. If - this is not set, the cookie will be - valid for all subdomains of - ``SERVER_NAME``. -``SESSION_COOKIE_PATH`` the path for the session cookie. If - this is not set the cookie will be valid - for all of ``APPLICATION_ROOT`` or if - that is not set for ``'/'``. -``SESSION_COOKIE_HTTPONLY`` controls if the cookie should be set - with the httponly flag. Defaults to - ``True``. -``SESSION_COOKIE_SECURE`` controls if the cookie should be set - with the secure flag. Defaults to - ``False``. -``PERMANENT_SESSION_LIFETIME`` the lifetime of a permanent session as - :class:`datetime.timedelta` object. - Starting with Flask 0.8 this can also be - an integer representing seconds. -``SESSION_REFRESH_EACH_REQUEST`` this flag controls how permanent - sessions are refreshed. If set to ``True`` - (which is the default) then the cookie - is refreshed each request which - automatically bumps the lifetime. If - set to ``False`` a `set-cookie` header is - only sent if the session is modified. - Non permanent sessions are not affected - by this. -``USE_X_SENDFILE`` enable/disable x-sendfile -``LOGGER_NAME`` the name of the logger -``LOGGER_HANDLER_POLICY`` the policy of the default logging - handler. The default is ``'always'`` - which means that the default logging - handler is always active. ``'debug'`` - will only activate logging in debug - mode, ``'production'`` will only log in - production and ``'never'`` disables it - entirely. -``SERVER_NAME`` the name and port number of the server. - Required for subdomain support (e.g.: - ``'myapp.dev:5000'``) Note that - localhost does not support subdomains so - setting this to “localhost” does not - help. Setting a ``SERVER_NAME`` also - by default enables URL generation - without a request context but with an - application context. -``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 - returning a 413 status code. -``SEND_FILE_MAX_AGE_DEFAULT`` Default cache control max age to use with - :meth:`~flask.Flask.send_static_file` (the - default static file handler) and - :func:`~flask.send_file`, as - :class:`datetime.timedelta` or as seconds. - Override this value on a per-file - basis using the - :meth:`~flask.Flask.get_send_file_max_age` - hook on :class:`~flask.Flask` or - :class:`~flask.Blueprint`, - respectively. Defaults to 43200 (12 hours). -``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will - not execute the error handlers of HTTP - exceptions but instead treat the - exception like any other and bubble it - through the exception stack. This is - helpful for hairy debugging situations - where you have to find out where an HTTP - exception is coming from. -``TRAP_BAD_REQUEST_ERRORS`` Werkzeug's internal data structures that - deal with request specific data will - raise special key errors that are also - bad request exceptions. Likewise many - operations can implicitly fail with a - BadRequest exception for consistency. - Since it's nice for debugging to know - why exactly it failed this flag can be - used to debug those situations. If this - config is set to ``True`` you will get - a regular traceback instead. -``PREFERRED_URL_SCHEME`` The URL scheme that should be used for - URL generation if no URL scheme is - available. This defaults to ``http``. -``JSON_AS_ASCII`` By default Flask serialize object to - ascii-encoded JSON. If this is set to - ``False`` Flask will not encode to ASCII - and output strings as-is and return - unicode strings. ``jsonify`` will - automatically encode it in ``utf-8`` - then for transport for instance. -``JSON_SORT_KEYS`` By default Flask will serialize JSON - objects in a way that the keys are - ordered. This is done in order to - ensure that independent of the hash seed - of the dictionary the return value will - be consistent to not trash external HTTP - caches. You can override the default - behavior by changing this variable. - This is not recommended but might give - you a performance improvement on the - cost of cacheability. -``JSONIFY_PRETTYPRINT_REGULAR`` If this is set to ``True`` or the Flask app - is running in debug mode, jsonify responses - will be pretty printed. -``JSONIFY_MIMETYPE`` MIME type used for jsonify responses. -``TEMPLATES_AUTO_RELOAD`` Whether to check for modifications of - the template source and reload it - automatically. By default the value is - ``None`` which means that Flask checks - original file only in debug mode. -``EXPLAIN_TEMPLATE_LOADING`` If this is enabled then every attempt to - load a template will write an info - message to the logger explaining the - attempts to locate the template. This - can be useful to figure out why - templates cannot be found or wrong - templates appear to be loaded. -================================= ========================================= - -.. admonition:: More on ``SERVER_NAME`` - - The ``SERVER_NAME`` key is used for the subdomain support. Because - Flask cannot guess the subdomain part without the knowledge of the - actual server name, this is required if you want to work with - subdomains. This is also used for the session cookie. - - Please keep in mind that not only Flask has the problem of not knowing - what subdomains are, your web browser does as well. Most modern web - browsers will not allow cross-subdomain cookies to be set on a - server name without dots in it. So if your server name is - ``'localhost'`` you will not be able to set a cookie for - ``'localhost'`` and every subdomain of it. Please choose a different - server name in that case, like ``'myapplication.local'`` and add - this name + the subdomains you want to use into your host config - or setup a local `bind`_. - -.. _bind: https://www.isc.org/downloads/bind/ +.. py:data:: DEBUG + + Enable debug mode. When using the development server with ``flask run`` or + ``app.run``, an interactive debugger will be shown for unhanlded + exceptions, and the server will be reloaded when code changes. + + **Do not enable debug mode in production.** + + Default: ``False`` + +.. py:data:: TESTING + + Enable testing mode. Exceptions are propagated rather than handled by the + the app's error handlers. Extensions may also change their behavior to + facilitate easier testing. You should enable this in your own tests. + + Default: ``False`` + +.. py:data:: PROPAGATE_EXCEPTIONS + + Exceptions are re-raised rather than being handled by the app's error + handlers. If not set, this is implicitly true if ``TESTING`` or ``DEBUG`` + is enabled. + + Default: ``None`` + +.. py:data:: PRESERVE_CONTEXT_ON_EXCEPTION + + Don't pop the request context when an exception occurs. If not set, this + is true if ``DEBUG`` is true. This allows debuggers to introspect the + request data on errors, and should normally not need to be set directly. + + Default: ``None`` + +.. py:data:: TRAP_HTTP_EXCEPTIONS + + If there is no handler for an ``HTTPException``-type exception, re-raise it + to be handled by the interactive debugger instead of returning it as a + simple error response. + + Default: ``False`` + +.. py:data:: TRAP_BAD_REQUEST_ERRORS`` + + Trying to access a key that doesn't exist from request dicts like ``args`` + and ``form`` will return a 400 Bad Request error page. Enable this to treat + the error as an unhandled exception instead so that you get the interactive + debugger. This is a more specific version of ``TRAP_HTTP_EXCEPTIONS``. If + unset, it is enabled in debug mode. + + Default: ``None`` + +.. py:data:: SECRET_KEY + + A secret key that will be used for securely signing the session cookie + and can be used for any other security related needs by extensions or your + application. It should be a long random string of bytes, although unicode + is accepted too. For example, copy the output of this to your config:: + + python -c 'import os; print(os.urandom(32))' + + **Do not reveal the secret key when posting questions or committing code.** + + Default: ``None`` + +.. py:data:: SESSION_COOKIE_NAME + + The name of the session cookie. Can be changed in case you already have a + cookie with the same name. + + Default: ``'session'`` + +.. py:data:: SESSION_COOKIE_DOMAIN + + The domain match rule that the session cookie will be valid for. If not + set, the cookie will be valid for all subdomains of ``SERVER_NAME``. If + ``False``, the cookie's domain will not be set. + + Default: ``None`` + +.. py:data:: SESSION_COOKIE_PATH + + The path that the session cookie will be valid for. If not set, the cookie + will be valid underneath ``APPLICATION_ROOT`` or ``/`` if that is not set. + + Default: ``None`` + +.. py:data:: SESSION_COOKIE_HTTPONLY + + Browsers will not allow JavaScript access to cookies marked as "HTTP only" + for security. + + Default: ``True`` + +.. py:data:: SESSION_COOKIE_SECURE + + Browsers will only send cookies with requests over HTTPS if the cookie is + marked "secure". The application must be served over HTTPS for this to make + sense. + + Default: ``False`` + +.. py:data:: PERMANENT_SESSION_LIFETIME + + If ``session.permanent`` is true, the cookie's max age will be set to this + number of seconds. Can either be a :class:`datetime.timedelta` or an + ``int``. + + Default: ``timedelta(days=31)`` (``2678400`` seconds) + +.. py:data:: SESSION_REFRESH_EACH_REQUEST + + Control whether the cookie is sent with every response when + ``session.permanent`` is true. Sending the cookie every time (the default) + can more reliably keep the session from expiring, but uses more bandwidth. + Non-permanent sessions are not affected. + + Default: ``True`` + +.. py:data:: USE_X_SENDFILE + + When serving files, set the ``X-Sendfile`` header instead of serving the + data with Flask. Some web servers, such as Apache, recognize this and serve + the data more efficiently. This only makes sense when using such a server. + + Default: ``False`` + +.. py:data:: SEND_FILE_MAX_AGE_DEFAULT + + When serving files, set the cache control max age to this number of + seconds. Can either be a :class:`datetime.timedelta` or an ``int``. + Override this value on a per-file basis using + :meth:`~flask.Flask.get_send_file_max_age` on the application or blueprint. + + Default: ``timedelta(hours=12)`` (``43200`` seconds) + +.. py:data:: LOGGER_NAME + + The name of the logger that the Flask application sets up. If not set, + it will take the import name passed to ``Flask.__init__``. + + Default: ``None`` + +.. py:data:: LOGGER_HANDLER_POLICY + + When to activate the application's logger handler. ``'always'`` always + enables it, ``'debug'`` only activates it in debug mode, ``'production'`` + only activates it when not in debug mode, and ``'never'`` never enables it. + + Default: ``'always'`` + +.. py:data:: SERVER_NAME + + Inform the application what host and port it is bound to. Required for + subdomain route matching support. + + If set, will be used for the session cookie domain if + ``SESSION_COOKIE_DOMAIN`` is not set. Modern web browsers will not allow + setting cookies for domains without a dot. To use a domain locally, + add any names that should route to the app to your ``hosts`` file. :: + + 127.0.0.1 localhost.dev + + If set, ``url_for`` can generate external URLs with only an application + context instead of a request context. + + Default: ``None`` + +.. py:data:: APPLICATION_ROOT + + Inform the application what path it is mounted under by the application / + web server. + + Will be used for the session cookie path if ``SESSION_COOKIE_PATH`` is not + set. + + Default: ``'/'`` + +.. py:data:: PREFERRED_URL_SCHEME + + Use this scheme for generating external URLs when not in a request context. + + Default: ``'http'`` + +.. py:data:: MAX_CONTENT_LENGTH + + Don't read more than this many bytes from the incoming request data. If not + set and the request does not specify a ``CONTENT_LENGTH``, no data will be + read for security. + + Default: ``None`` + +.. py:data:: JSON_AS_ASCII + + Serialize objects to ASCII-encoded JSON. If this is disabled, the JSON + will be returned as a Unicode string, or encoded as ``UTF-8`` by + ``jsonify``. This has security implications when rendering the JSON in + to JavaScript in templates, and should typically remain enabled. + + Default: ``True`` + +.. py:data:: JSON_SORT_KEYS + + Sort the keys of JSON objects alphabetically. This is useful for caching + because it ensures the data is serialized the same way no matter what + Python's hash seed is. While not recommended, you can disable this for a + possible performance improvement at the cost of caching. + + Default: ``True`` + +.. py:data:: JSONIFY_PRETTYPRINT_REGULAR + + ``jsonify`` responses will be output with newlines, spaces, and indentation + for easier reading by humans. Always enabled in debug mode. + + Default: ``False`` + +.. py:data:: JSONIFY_MIMETYPE + + The mimetype of ``jsonify`` responses. + + Default: ``'application/json'`` + +.. py:data:: TEMPLATES_AUTO_RELOAD + + Reload templates when they are changed. If not set, it will be enabled in + debug mode. + + Default: ``None`` + +.. py:data:: EXPLAIN_TEMPLATE_LOADING + + Log debugging information tracing how a template file was loaded. This can + be useful to figure out why a template was not loaded or the wrong file + appears to be loaded. + + Default: ``False`` .. versionadded:: 0.4 ``LOGGER_NAME`` @@ -477,3 +546,4 @@ Example usage for both:: # or via open_instance_resource: with app.open_instance_resource('application.cfg') as f: config = f.read() + diff --git a/docs/contents.rst.inc b/docs/contents.rst.inc index 8b25e61d..e77f7b60 100644 --- a/docs/contents.rst.inc +++ b/docs/contents.rst.inc @@ -59,3 +59,4 @@ Design notes, legal information and changelog are here for the interested. upgrading changelog license + contributing diff --git a/docs/contributing.rst b/docs/contributing.rst new file mode 100644 index 00000000..e582053e --- /dev/null +++ b/docs/contributing.rst @@ -0,0 +1 @@ +.. include:: ../CONTRIBUTING.rst diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index 2791fec3..84c649ce 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -76,49 +76,72 @@ Error handlers You might want to show custom error pages to the user when an error occurs. This can be done by registering error handlers. -Error handlers are normal :ref:`views` but instead of being registered for -routes, they are registered for exceptions that are raised while trying to -do something else. +An error handler is a normal view function that return a response, but instead +of being registered for a route, it is registered for an exception or HTTP +status code that would is raised while trying to handle a request. Registering ``````````` -Register error handlers using :meth:`~flask.Flask.errorhandler` or -:meth:`~flask.Flask.register_error_handler`:: +Register handlers by decorating a function with +:meth:`~flask.Flask.errorhandler`. Or use +:meth:`~flask.Flask.register_error_handler` to register the function later. +Remember to set the error code when returning the response. :: @app.errorhandler(werkzeug.exceptions.BadRequest) def handle_bad_request(e): - return 'bad request!' + return 'bad request!', 400 - app.register_error_handler(400, lambda e: 'bad request!') + # or, without the decorator + app.register_error_handler(400, handle_bad_request) -Those two ways are equivalent, but the first one is more clear and leaves -you with a function to call on your whim (and in tests). Note that :exc:`werkzeug.exceptions.HTTPException` subclasses like -:exc:`~werkzeug.exceptions.BadRequest` from the example and their HTTP codes -are interchangeable when handed to the registration methods or decorator -(``BadRequest.code == 400``). +:exc:`~werkzeug.exceptions.BadRequest` and their HTTP codes are interchangeable +when registering handlers. (``BadRequest.code == 400``) -You are however not limited to :exc:`~werkzeug.exceptions.HTTPException` -or HTTP status codes but can register a handler for every exception class you -like. +Non-standard HTTP codes cannot be registered by code because they are not known +by Werkzeug. Instead, define a subclass of +:class:`~werkzeug.exceptions.HTTPException` with the appropriate code and +register and raise that exception class. :: -.. versionchanged:: 0.11 + class InsufficientStorage(werkzeug.exceptions.HTTPException): + code = 507 + description = 'Not enough storage space.' + + app.register_error_handler(InsuffcientStorage, handle_507) - Errorhandlers are now prioritized by specificity of the exception classes - they are registered for instead of the order they are registered in. + raise InsufficientStorage() + +Handlers can be registered for any exception class, not just +:exc:`~werkzeug.exceptions.HTTPException` subclasses or HTTP status +codes. Handlers can be registered for a specific class, or for all subclasses +of a parent class. Handling ```````` -Once an exception instance is raised, its class hierarchy is traversed, -and searched for in the exception classes for which handlers are registered. -The most specific handler is selected. +When an exception is caught by Flask while handling a request, it is first +looked up by code. If no handler is registered for the code, it is looked up +by its class hierarchy; the most specific handler is chosen. If no handler is +registered, :class:`~werkzeug.exceptions.HTTPException` subclasses show a +generic message about their code, while other exceptions are converted to a +generic 500 Internal Server Error. -E.g. if an instance of :exc:`ConnectionRefusedError` is raised, and a handler +For example, if an instance of :exc:`ConnectionRefusedError` is raised, and a handler is registered for :exc:`ConnectionError` and :exc:`ConnectionRefusedError`, -the more specific :exc:`ConnectionRefusedError` handler is called on the -exception instance, and its response is shown to the user. +the more specific :exc:`ConnectionRefusedError` handler is called with the +exception instance to generate the response. + +Handlers registered on the blueprint take precedence over those registered +globally on the application, assuming a blueprint is handling the request that +raises the exception. However, the blueprint cannot handle 404 routing errors +because the 404 occurs at the routing level before the blueprint can be +determined. + +.. versionchanged:: 0.11 + + Handlers are prioritized by specificity of the exception classes they are + registered for instead of the order they are registered in. Error Mails ----------- 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' - } diff --git a/docs/security.rst b/docs/security.rst index ad0d1244..0d4cfdeb 100644 --- a/docs/security.rst +++ b/docs/security.rst @@ -104,3 +104,105 @@ vulnerabilities `_, so this behavior was changed and :func:`~flask.jsonify` now supports serializing arrays. + +Security Headers +---------------- + +Browsers recognize various response headers in order to control security. We +recommend reviewing each of the headers below for use in your application. +The `Flask-Talisman`_ extension can be used to manage HTTPS and the security +headers for you. + +.. _Flask-Talisman: https://github.com/GoogleCloudPlatform/flask-talisman + +HTTP Strict Transport Security (HSTS) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tells the browser to convert all HTTP requests to HTTPS, preventing +man-in-the-middle (MITM) attacks. :: + + response.haders['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains' + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security + +Content Security Policy (CSP) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Tell the browser where it can load various types of resource from. This header +should be used whenever possible, but requires some work to define the correct +policy for your site. A very strict policy would be:: + + response.headers['Content-Security-Policy'] = "default-src: 'self'" + +- https://csp.withgoogle.com/docs/index.html +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +X-Content-Type-Options +~~~~~~~~~~~~~~~~~~~~~~ + +Forces the browser to honor the response content type instead of trying to +detect it, which can be abused to generate a cross-site scripting (XSS) +attack. :: + + response.headers['X-Content-Type-Options'] = 'nosniff' + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options + +X-Frame-Options +~~~~~~~~~~~~~~~ + +Prevents external sites from embedding your site in an ``iframe``. This +prevents a class of attacks where clicks in the outer frame can be translated +invisibly to clicks on your page's elements. This is also known as +"clickjacking". :: + + response.headers['X-Frame-Options'] = 'SAMEORIGIN' + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options + +X-XSS-Protection +~~~~~~~~~~~~~~~~ + +The browser will try to prevent reflected XSS attacks by not loading the page +if the request contains something that looks like JavaScript and the response +contains the same data. :: + + response.headers['X-XSS-Protection'] = '1; mode=block' + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-XSS-Protection + +Set-Cookie options +~~~~~~~~~~~~~~~~~~ + +These options can be added to a ``Set-Cookie`` header to improve their +security. Flask has configuration options to set these on the session cookie. +They can be set on other cookies too. + +- ``Secure`` limits cookies to HTTPS traffic only. +- ``HttpOnly`` protects the contents of cookies from being read with + JavaScript. +- ``SameSite`` ensures that cookies can only be requested from the same + domain that created them. It is not supported by Flask yet. + +:: + + app.config.update( + SESSION_COOKIE_SECURE=True, + SESSION_COOKIE_HTTPONLY=True, + ) + + response.set_cookie('username', 'flask', secure=True, httponly=True) + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Secure_and_HttpOnly_cookies + +HTTP Public Key Pinning (HPKP) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This tells the browser to authenticate with the server using only the specific +certificate key to prevent MITM attacks. + +.. warning:: + Be careful when enabling this, as it is very difficult to undo if you set up + or upgrade your key incorrectly. + +- https://developer.mozilla.org/en-US/docs/Web/HTTP/Public_Key_Pinning diff --git a/examples/flaskr/README b/examples/flaskr/README index 90860ff2..f60287fa 100644 --- a/examples/flaskr/README +++ b/examples/flaskr/README @@ -9,9 +9,11 @@ ~ How do I use it? - 1. edit the configuration in the flaskr.py file or + 1. edit the configuration in the factory.py file or export an FLASKR_SETTINGS environment variable - pointing to a configuration file. + pointing to a configuration file or pass in a + dictionary with config values using the create_app + function. 2. install the app from the root of the project directory @@ -19,7 +21,7 @@ 3. Instruct flask to use the right application - export FLASK_APP=flaskr + export FLASK_APP=flaskr.factory:create_app() 4. initialize the database with this command: diff --git a/examples/flaskr/flaskr/__init__.py b/examples/flaskr/flaskr/__init__.py index d096b1e7..e69de29b 100644 --- a/examples/flaskr/flaskr/__init__.py +++ b/examples/flaskr/flaskr/__init__.py @@ -1 +0,0 @@ -from .flaskr import app diff --git a/examples/flaskr/flaskr/blueprints/__init__.py b/examples/flaskr/flaskr/blueprints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/flaskr/flaskr/flaskr.py b/examples/flaskr/flaskr/blueprints/flaskr.py similarity index 55% rename from examples/flaskr/flaskr/flaskr.py rename to examples/flaskr/flaskr/blueprints/flaskr.py index b4c1d6bd..7b64dd9e 100644 --- a/examples/flaskr/flaskr/flaskr.py +++ b/examples/flaskr/flaskr/blueprints/flaskr.py @@ -10,29 +10,18 @@ :license: BSD, see LICENSE for more details. """ -import os from sqlite3 import dbapi2 as sqlite3 -from flask import Flask, request, session, g, redirect, url_for, abort, \ - render_template, flash +from flask import Blueprint, request, session, g, redirect, url_for, abort, \ + render_template, flash, current_app -# create our little application :) -app = Flask(__name__) - -# Load default config and override config from an environment variable -app.config.update(dict( - DATABASE=os.path.join(app.root_path, 'flaskr.db'), - DEBUG=True, - SECRET_KEY='development key', - USERNAME='admin', - PASSWORD='default' -)) -app.config.from_envvar('FLASKR_SETTINGS', silent=True) +# create our blueprint :) +bp = Blueprint('flaskr', __name__) def connect_db(): """Connects to the specific database.""" - rv = sqlite3.connect(app.config['DATABASE']) + rv = sqlite3.connect(current_app.config['DATABASE']) rv.row_factory = sqlite3.Row return rv @@ -40,18 +29,11 @@ def connect_db(): def init_db(): """Initializes the database.""" db = get_db() - with app.open_resource('schema.sql', mode='r') as f: + with current_app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) db.commit() -@app.cli.command('initdb') -def initdb_command(): - """Creates the database tables.""" - init_db() - print('Initialized the database.') - - def get_db(): """Opens a new database connection if there is none yet for the current application context. @@ -61,14 +43,7 @@ def get_db(): return g.sqlite_db -@app.teardown_appcontext -def close_db(error): - """Closes the database again at the end of the request.""" - if hasattr(g, 'sqlite_db'): - g.sqlite_db.close() - - -@app.route('/') +@bp.route('/') def show_entries(): db = get_db() cur = db.execute('select title, text from entries order by id desc') @@ -76,7 +51,7 @@ def show_entries(): return render_template('show_entries.html', entries=entries) -@app.route('/add', methods=['POST']) +@bp.route('/add', methods=['POST']) def add_entry(): if not session.get('logged_in'): abort(401) @@ -85,26 +60,26 @@ def add_entry(): [request.form['title'], request.form['text']]) db.commit() flash('New entry was successfully posted') - return redirect(url_for('show_entries')) + return redirect(url_for('flaskr.show_entries')) -@app.route('/login', methods=['GET', 'POST']) +@bp.route('/login', methods=['GET', 'POST']) def login(): error = None if request.method == 'POST': - if request.form['username'] != app.config['USERNAME']: + if request.form['username'] != current_app.config['USERNAME']: error = 'Invalid username' - elif request.form['password'] != app.config['PASSWORD']: + elif request.form['password'] != current_app.config['PASSWORD']: error = 'Invalid password' else: session['logged_in'] = True flash('You were logged in') - return redirect(url_for('show_entries')) + return redirect(url_for('flaskr.show_entries')) return render_template('login.html', error=error) -@app.route('/logout') +@bp.route('/logout') def logout(): session.pop('logged_in', None) flash('You were logged out') - return redirect(url_for('show_entries')) + return redirect(url_for('flaskr.show_entries')) diff --git a/examples/flaskr/flaskr/factory.py b/examples/flaskr/flaskr/factory.py new file mode 100644 index 00000000..07de0aa6 --- /dev/null +++ b/examples/flaskr/flaskr/factory.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +""" + Flaskr + ~~~~~~ + + A microblog example application written as Flask tutorial with + Flask and sqlite3. + + :copyright: (c) 2015 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +import os +from flask import Flask, g +from werkzeug.utils import find_modules, import_string +from flaskr.blueprints.flaskr import init_db + + +def create_app(config=None): + app = Flask('flaskr') + + app.config.update(dict( + DATABASE=os.path.join(app.root_path, 'flaskr.db'), + DEBUG=True, + SECRET_KEY='development key', + USERNAME='admin', + PASSWORD='default' + )) + app.config.update(config or {}) + app.config.from_envvar('FLASKR_SETTINGS', silent=True) + + register_blueprints(app) + register_cli(app) + register_teardowns(app) + + return app + + +def register_blueprints(app): + """Register all blueprint modules + + Reference: Armin Ronacher, "Flask for Fun and for Profit" PyBay 2016. + """ + for name in find_modules('flaskr.blueprints'): + mod = import_string(name) + if hasattr(mod, 'bp'): + app.register_blueprint(mod.bp) + return None + + +def register_cli(app): + @app.cli.command('initdb') + def initdb_command(): + """Creates the database tables.""" + init_db() + print('Initialized the database.') + + +def register_teardowns(app): + @app.teardown_appcontext + def close_db(error): + """Closes the database again at the end of the request.""" + if hasattr(g, 'sqlite_db'): + g.sqlite_db.close() diff --git a/examples/flaskr/flaskr/templates/layout.html b/examples/flaskr/flaskr/templates/layout.html index 737b51b2..862a9f4a 100644 --- a/examples/flaskr/flaskr/templates/layout.html +++ b/examples/flaskr/flaskr/templates/layout.html @@ -5,9 +5,9 @@

Flaskr

{% if not session.logged_in %} - log in + log in {% else %} - log out + log out {% endif %}
{% for message in get_flashed_messages() %} diff --git a/examples/flaskr/flaskr/templates/login.html b/examples/flaskr/flaskr/templates/login.html index ed09aeba..505d2f66 100644 --- a/examples/flaskr/flaskr/templates/login.html +++ b/examples/flaskr/flaskr/templates/login.html @@ -2,7 +2,7 @@ {% block body %}

Login

{% if error %}

Error: {{ error }}{% endif %} -

+
Username:
diff --git a/examples/flaskr/flaskr/templates/show_entries.html b/examples/flaskr/flaskr/templates/show_entries.html index 2f68b9d3..cf8fbb86 100644 --- a/examples/flaskr/flaskr/templates/show_entries.html +++ b/examples/flaskr/flaskr/templates/show_entries.html @@ -1,7 +1,7 @@ {% extends "layout.html" %} {% block body %} {% if session.logged_in %} - +
Title:
diff --git a/examples/flaskr/setup.cfg b/examples/flaskr/setup.cfg index db50667a..9af7e6f1 100644 --- a/examples/flaskr/setup.cfg +++ b/examples/flaskr/setup.cfg @@ -1,2 +1,2 @@ -[tool:pytest] -test=pytest +[aliases] +test=pytest \ No newline at end of file diff --git a/examples/flaskr/setup.py b/examples/flaskr/setup.py index 910f23ac..7f1dae53 100644 --- a/examples/flaskr/setup.py +++ b/examples/flaskr/setup.py @@ -1,8 +1,19 @@ -from setuptools import setup +# -*- coding: utf-8 -*- +""" + Flaskr Tests + ~~~~~~~~~~~~ + + Tests the Flaskr application. + + :copyright: (c) 2015 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + +from setuptools import setup, find_packages setup( name='flaskr', - packages=['flaskr'], + packages=find_packages(), include_package_data=True, install_requires=[ 'flask', diff --git a/examples/flaskr/tests/test_flaskr.py b/examples/flaskr/tests/test_flaskr.py index 493067ee..b5ade2ec 100644 --- a/examples/flaskr/tests/test_flaskr.py +++ b/examples/flaskr/tests/test_flaskr.py @@ -12,19 +12,38 @@ import os import tempfile import pytest -from flaskr import flaskr +from flaskr.factory import create_app +from flaskr.blueprints.flaskr import init_db @pytest.fixture -def client(): - 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']) +def app(request): + + db_fd, temp_db_location = tempfile.mkstemp() + config = { + 'DATABASE': temp_db_location, + 'TESTING': True, + 'DB_FD': db_fd + } + + app = create_app(config=config) + + with app.app_context(): + init_db() + yield app + + +@pytest.fixture +def client(request, app): + + client = app.test_client() + + def teardown(): + os.close(app.config['DB_FD']) + os.unlink(app.config['DATABASE']) + request.addfinalizer(teardown) + + return client def login(client, username, password): @@ -44,25 +63,25 @@ def test_empty_db(client): assert b'No entries here so far' in rv.data -def test_login_logout(client): +def test_login_logout(client, app): """Make sure login and logout works""" - rv = login(client, flaskr.app.config['USERNAME'], - flaskr.app.config['PASSWORD']) + rv = login(client, app.config['USERNAME'], + 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,app.config['USERNAME'] + 'x', + 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, app.config['USERNAME'], + app.config['PASSWORD'] + 'x') assert b'Invalid password' in rv.data -def test_messages(client): +def test_messages(client, app): """Test that messages work""" - login(client, flaskr.app.config['USERNAME'], - flaskr.app.config['PASSWORD']) + login(client, app.config['USERNAME'], + app.config['PASSWORD']) rv = client.post('/add', data=dict( title='', text='HTML allowed here' diff --git a/examples/minitwit/minitwit/minitwit.py b/examples/minitwit/minitwit/minitwit.py index 69840267..abef98e8 100644 --- a/examples/minitwit/minitwit/minitwit.py +++ b/examples/minitwit/minitwit/minitwit.py @@ -25,7 +25,7 @@ DEBUG = True SECRET_KEY = 'development key' # create our little application :) -app = Flask(__name__) +app = Flask('minitwit') app.config.from_object(__name__) app.config.from_envvar('MINITWIT_SETTINGS', silent=True) diff --git a/examples/patterns/largerapp/yourapplication/__init__.py b/examples/patterns/largerapp/yourapplication/__init__.py index 089d2937..09407711 100644 --- a/examples/patterns/largerapp/yourapplication/__init__.py +++ b/examples/patterns/largerapp/yourapplication/__init__.py @@ -1,4 +1,4 @@ from flask import Flask -app = Flask(__name__) +app = Flask('yourapplication') -import yourapplication.views \ No newline at end of file +import yourapplication.views diff --git a/flask/app.py b/flask/app.py index 51b50d34..342dde86 100644 --- a/flask/app.py +++ b/flask/app.py @@ -10,6 +10,7 @@ """ import os import sys +import warnings from datetime import timedelta from functools import update_wrapper from itertools import chain @@ -17,7 +18,8 @@ from threading import Lock from werkzeug.datastructures import ImmutableDict, Headers from werkzeug.exceptions import BadRequest, HTTPException, \ - InternalServerError, MethodNotAllowed, default_exceptions + InternalServerError, MethodNotAllowed, default_exceptions, \ + BadRequestKeyError from werkzeug.routing import BuildError, Map, RequestRedirect, Rule from . import cli, json @@ -307,7 +309,7 @@ class Flask(_PackageBoundObject): 'LOGGER_NAME': None, 'LOGGER_HANDLER_POLICY': 'always', 'SERVER_NAME': None, - 'APPLICATION_ROOT': None, + 'APPLICATION_ROOT': '/', 'SESSION_COOKIE_NAME': 'session', 'SESSION_COOKIE_DOMAIN': None, 'SESSION_COOKIE_PATH': None, @@ -316,7 +318,7 @@ class Flask(_PackageBoundObject): 'SESSION_REFRESH_EACH_REQUEST': True, 'MAX_CONTENT_LENGTH': None, 'SEND_FILE_MAX_AGE_DEFAULT': timedelta(hours=12), - 'TRAP_BAD_REQUEST_ERRORS': False, + 'TRAP_BAD_REQUEST_ERRORS': None, 'TRAP_HTTP_EXCEPTIONS': False, 'EXPLAIN_TEMPLATE_LOADING': False, 'PREFERRED_URL_SCHEME': 'http', @@ -925,8 +927,17 @@ class Flask(_PackageBoundObject): :attr:`secret_key` is set. Instead of overriding this method we recommend replacing the :class:`session_interface`. + .. deprecated: 1.0 + Will be removed in 1.1. Use ``session_interface.open_session`` + instead. + :param request: an instance of :attr:`request_class`. """ + + warnings.warn(DeprecationWarning( + '"open_session" is deprecated and will be removed in 1.1. Use' + ' "session_interface.open_session" instead.' + )) return self.session_interface.open_session(self, request) def save_session(self, session, response): @@ -934,19 +945,37 @@ class Flask(_PackageBoundObject): implementation, check :meth:`open_session`. Instead of overriding this method we recommend replacing the :class:`session_interface`. + .. deprecated: 1.0 + Will be removed in 1.1. Use ``session_interface.save_session`` + instead. + :param session: the session to be saved (a :class:`~werkzeug.contrib.securecookie.SecureCookie` object) :param response: an instance of :attr:`response_class` """ + + warnings.warn(DeprecationWarning( + '"save_session" is deprecated and will be removed in 1.1. Use' + ' "session_interface.save_session" instead.' + )) return self.session_interface.save_session(self, session, response) def make_null_session(self): """Creates a new instance of a missing session. Instead of overriding this method we recommend replacing the :class:`session_interface`. + .. deprecated: 1.0 + Will be removed in 1.1. Use ``session_interface.make_null_session`` + instead. + .. versionadded:: 0.7 """ + + warnings.warn(DeprecationWarning( + '"make_null_session" is deprecated and will be removed in 1.1. Use' + ' "session_interface.make_null_session" instead.' + )) return self.session_interface.make_null_session(self) @setupmethod @@ -1137,7 +1166,9 @@ class Flask(_PackageBoundObject): @setupmethod def errorhandler(self, code_or_exception): - """A decorator that is used to register a function given an + """Register a function to handle errors by code or exception class. + + A decorator that is used to register a function given an error code. Example:: @app.errorhandler(404) @@ -1150,21 +1181,6 @@ class Flask(_PackageBoundObject): def special_exception_handler(error): return 'Database connection failed', 500 - You can also register a function as error handler without using - the :meth:`errorhandler` decorator. The following example is - equivalent to the one above:: - - def page_not_found(error): - return 'This page does not exist', 404 - app.error_handler_spec[None][404] = page_not_found - - Setting error handlers via assignments to :attr:`error_handler_spec` - however is discouraged as it requires fiddling with nested dictionaries - and the special case for arbitrary exception types. - - The first ``None`` refers to the active blueprint. If the error - handler should be application wide ``None`` shall be used. - .. versionadded:: 0.7 Use :meth:`register_error_handler` instead of modifying :attr:`error_handler_spec` directly, for application wide error @@ -1183,6 +1199,7 @@ class Flask(_PackageBoundObject): return f return decorator + @setupmethod def register_error_handler(self, code_or_exception, f): """Alternative error attach function to the :meth:`errorhandler` decorator that is more straightforward to use for non decorator @@ -1201,11 +1218,18 @@ class Flask(_PackageBoundObject): """ if isinstance(code_or_exception, HTTPException): # old broken behavior raise ValueError( - 'Tried to register a handler for an exception instance {0!r}. ' - 'Handlers can only be registered for exception classes or HTTP error codes.' - .format(code_or_exception)) + 'Tried to register a handler for an exception instance {0!r}.' + ' Handlers can only be registered for exception classes or' + ' HTTP error codes.'.format(code_or_exception) + ) - exc_class, code = self._get_exc_class_and_code(code_or_exception) + try: + exc_class, code = self._get_exc_class_and_code(code_or_exception) + except KeyError: + raise KeyError( + "'{0}' is not a recognized HTTP error code. Use a subclass of" + " HTTPException with that code instead.".format(code_or_exception) + ) handlers = self.error_handler_spec.setdefault(key, {}).setdefault(code, {}) handlers[exc_class] = f @@ -1310,7 +1334,7 @@ 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. @@ -1438,12 +1462,12 @@ class Flask(_PackageBoundObject): """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. """ @@ -1515,12 +1539,20 @@ class Flask(_PackageBoundObject): traceback. This is helpful for debugging implicitly raised HTTP exceptions. + .. versionchanged:: 1.0 + Bad request errors are not trapped by default in debug mode. + .. versionadded:: 0.8 """ if self.config['TRAP_HTTP_EXCEPTIONS']: return True - if self.config['TRAP_BAD_REQUEST_ERRORS']: + + trap_bad_request = self.config['TRAP_BAD_REQUEST_ERRORS'] + + # if unset, trap based on debug mode + if (trap_bad_request is None and self.debug) or trap_bad_request: return isinstance(e, BadRequest) + return False def handle_user_exception(self, e): @@ -1531,16 +1563,30 @@ class Flask(_PackageBoundObject): function will either return a response value or reraise the exception with the same traceback. + .. versionchanged:: 1.0 + Key errors raised from request data like ``form`` show the the bad + key in debug mode rather than a generic bad request message. + .. versionadded:: 0.7 """ exc_type, exc_value, tb = sys.exc_info() assert exc_value is e - # ensure not to trash sys.exc_info() at that point in case someone # wants the traceback preserved in handle_http_exception. Of course # we cannot prevent users from trashing it themselves in a custom # trap_http_exception method so that's their fault then. + # MultiDict passes the key to the exception, but that's ignored + # when generating the response message. Set an informative + # description for key errors in debug mode or when trapping errors. + if ( + (self.debug or self.config['TRAP_BAD_REQUEST_ERRORS']) + and isinstance(e, BadRequestKeyError) + # only set it if it's still the default description + and e.description is BadRequestKeyError.description + ): + e.description = "KeyError: '{0}'".format(*e.args) + if isinstance(e, HTTPException) and not self.trap_http_exception(e): return self.handle_http_exception(e) @@ -1732,10 +1778,10 @@ class Flask(_PackageBoundObject): ``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 @@ -1744,13 +1790,13 @@ class Flask(_PackageBoundObject): 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. @@ -1845,7 +1891,7 @@ class Flask(_PackageBoundObject): if self.config['SERVER_NAME'] is not None: return self.url_map.bind( self.config['SERVER_NAME'], - script_name=self.config['APPLICATION_ROOT'] or '/', + script_name=self.config['APPLICATION_ROOT'], url_scheme=self.config['PREFERRED_URL_SCHEME']) def inject_url_defaults(self, endpoint, values): @@ -1887,7 +1933,7 @@ class Flask(_PackageBoundObject): :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. @@ -1932,7 +1978,7 @@ class Flask(_PackageBoundObject): for handler in funcs: response = handler(response) if not self.session_interface.is_null_session(ctx.session): - self.save_session(ctx.session, response) + self.session_interface.save_session(self, ctx.session, response) return response def do_teardown_request(self, exc=_sentinel): @@ -2017,10 +2063,19 @@ class Flask(_PackageBoundObject): def test_request_context(self, *args, **kwargs): """Creates a WSGI environment from the given values (see :class:`werkzeug.test.EnvironBuilder` for more information, this - function accepts the same arguments). + function accepts the same arguments plus two additional). + + Additional arguments (only if ``base_url`` is not specified): + + :param subdomain: subdomain to use for route matching + :param url_scheme: scheme for the request, default + ``PREFERRED_URL_SCHEME`` or ``http``. """ + from flask.testing import make_test_environ_builder + builder = make_test_environ_builder(self, *args, **kwargs) + try: return self.request_context(builder.get_environ()) finally: diff --git a/flask/cli.py b/flask/cli.py index e6e8f65f..346f0bf8 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -9,7 +9,10 @@ :license: BSD, see LICENSE for more details. """ +import ast +import inspect import os +import re import sys import traceback from functools import update_wrapper @@ -55,20 +58,20 @@ def find_best_app(script_info, module): ' one.'.format(module=module.__name__) ) - # Search for app factory callables. + # Search for app factory functions. for attr_name in ('create_app', 'make_app'): app_factory = getattr(module, attr_name, None) - if callable(app_factory): + if inspect.isfunction(app_factory): try: app = call_factory(app_factory, script_info) if isinstance(app, Flask): return app except TypeError: raise NoAppException( - 'Auto-detected "{callable}()" in module "{module}", but ' + 'Auto-detected "{function}()" in module "{module}", but ' 'could not call it without specifying arguments.'.format( - callable=attr_name, module=module.__name__ + function=attr_name, module=module.__name__ ) ) @@ -79,18 +82,69 @@ def find_best_app(script_info, module): ) -def call_factory(func, script_info): - """Checks if the given app factory function has an argument named - ``script_info`` or just a single argument and calls the function passing - ``script_info`` if so. Otherwise, calls the function without any arguments - and returns the result. +def call_factory(app_factory, script_info, arguments=()): + """Takes an app factory, a ``script_info` object and optionally a tuple + of arguments. Checks for the existence of a script_info argument and calls + the app_factory depending on that and the arguments provided. """ - arguments = getargspec(func).args - if 'script_info' in arguments: - return func(script_info=script_info) - elif len(arguments) == 1: - return func(script_info) - return func() + args_spec = getargspec(app_factory) + arg_names = args_spec.args + arg_defaults = args_spec.defaults + + if 'script_info' in arg_names: + return app_factory(*arguments, script_info=script_info) + elif arguments: + return app_factory(*arguments) + elif not arguments and len(arg_names) == 1 and arg_defaults is None: + return app_factory(script_info) + return app_factory() + + +def find_app_by_string(string, script_info, module): + """Checks if the given string is a variable name or a function. If it is + a function, it checks for specified arguments and whether it takes + a ``script_info`` argument and calls the function with the appropriate + arguments.""" + from . import Flask + function_regex = r'^(?P\w+)(?:\((?P.*)\))?$' + match = re.match(function_regex, string) + if match: + name, args = match.groups() + try: + if args is not None: + args = args.rstrip(' ,') + if args: + args = ast.literal_eval( + "({args}, )".format(args=args)) + else: + args = () + app_factory = getattr(module, name, None) + app = call_factory(app_factory, script_info, args) + else: + attr = getattr(module, name, None) + if inspect.isfunction(attr): + app = call_factory(attr, script_info) + else: + app = attr + + if isinstance(app, Flask): + return app + else: + raise RuntimeError('Failed to find application in module ' + '"{name}"'.format(name=module)) + except TypeError as e: + new_error = NoAppException( + '{e}\nThe app factory "{factory}" in module "{module}" could' + ' not be called with the specified arguments (and a' + ' script_info argument automatically added if applicable).' + ' Did you make sure to use the right number of arguments as' + ' well as not using keyword arguments or' + ' non-literals?'.format(e=e, factory=string, module=module)) + reraise(NoAppException, new_error, sys.exc_info()[2]) + else: + raise NoAppException( + 'The provided string "{string}" is not a valid variable name' + 'or function expression.'.format(string=string)) def prepare_exec_for_file(filename): @@ -148,14 +202,9 @@ def locate_app(script_info, app_id): mod = sys.modules[module] if app_obj is None: - app = find_best_app(script_info, mod) + return find_best_app(script_info, mod) else: - app = getattr(mod, app_obj, None) - if app is None: - raise RuntimeError('Failed to find application in module "%s"' - % module) - - return app + return find_app_by_string(app_obj, script_info, mod) def find_default_import_path(): diff --git a/flask/ctx.py b/flask/ctx.py index 480d9c5c..70743de6 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -329,9 +329,11 @@ class RequestContext(object): # available. This allows a custom open_session method to use the # request context (e.g. code that access database information # stored on `g` instead of the appcontext). - self.session = self.app.open_session(self.request) + session_interface = self.app.session_interface + self.session = session_interface.open_session(self.app, self.request) + if self.session is None: - self.session = self.app.make_null_session() + self.session = session_interface.make_null_session(self.app) def pop(self, exc=_sentinel): """Pops the request context and unbinds it by doing that. This will diff --git a/flask/helpers.py b/flask/helpers.py index 2bcdc10b..f37be677 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -1004,17 +1004,3 @@ def is_ip(value): return True return False - - -def patch_vary_header(response, value): - """Add a value to the ``Vary`` header if it is not already present.""" - - header = response.headers.get('Vary', '') - headers = [h for h in (h.strip() for h in header.split(',')) if h] - lower_value = value.lower() - - if not any(h.lower() == lower_value for h in headers): - headers.append(value) - - updated_header = ', '.join(headers) - response.headers['Vary'] = updated_header diff --git a/flask/sessions.py b/flask/sessions.py index 4f2c84f5..a334e703 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -18,7 +18,6 @@ from itsdangerous import BadSignature, URLSafeTimedSerializer from werkzeug.datastructures import CallbackDict from werkzeug.http import http_date, parse_date -from flask.helpers import patch_vary_header from . import Markup, json from ._compat import iteritems, text_type from .helpers import is_ip, total_seconds @@ -347,8 +346,8 @@ class SessionInterface(object): config var if it's set, and falls back to ``APPLICATION_ROOT`` or uses ``/`` if it's ``None``. """ - return app.config['SESSION_COOKIE_PATH'] or \ - app.config['APPLICATION_ROOT'] or '/' + return app.config['SESSION_COOKIE_PATH'] \ + or app.config['APPLICATION_ROOT'] def get_cookie_httponly(self, app): """Returns True if the session cookie should be httponly. This @@ -466,7 +465,7 @@ class SecureCookieSessionInterface(SessionInterface): # Add a "Vary: Cookie" header if the session was accessed at all. if session.accessed: - patch_vary_header(response, 'Cookie') + response.vary.add('Cookie') if not self.should_set_cookie(app, session): return diff --git a/flask/testing.py b/flask/testing.py index 31600245..5f9269f2 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -21,19 +21,37 @@ except ImportError: from urlparse import urlsplit as url_parse -def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): +def make_test_environ_builder( + app, path='/', base_url=None, subdomain=None, url_scheme=None, + *args, **kwargs +): """Creates a new test builder with some application defaults thrown in.""" - http_host = app.config.get('SERVER_NAME') - app_root = app.config.get('APPLICATION_ROOT') + + assert ( + not (base_url or subdomain or url_scheme) + or (base_url is not None) != bool(subdomain or url_scheme) + ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".' + if base_url is None: + http_host = app.config.get('SERVER_NAME') or 'localhost' + app_root = app.config['APPLICATION_ROOT'] + + if subdomain: + http_host = '{0}.{1}'.format(subdomain, http_host) + + if url_scheme is None: + url_scheme = app.config['PREFERRED_URL_SCHEME'] + url = url_parse(path) - base_url = 'http://%s/' % (url.netloc or http_host or 'localhost') - if app_root: - base_url += app_root.lstrip('/') - if url.netloc: - path = url.path - if url.query: - path += '?' + url.query + base_url = '{0}://{1}/{2}'.format( + url_scheme, url.netloc or http_host, app_root.lstrip('/') + ) + path = url.path + + if url.query: + sep = b'?' if isinstance(url.query, bytes) else '?' + path += sep + url.query + return EnvironBuilder(path, base_url, *args, **kwargs) @@ -87,7 +105,8 @@ class FlaskClient(Client): self.cookie_jar.inject_wsgi(environ_overrides) outer_reqctx = _request_ctx_stack.top with app.test_request_context(*args, **kwargs) as c: - sess = app.open_session(c.request) + session_interface = app.session_interface + sess = session_interface.open_session(app, c.request) if sess is None: raise RuntimeError('Session backend did not open a session. ' 'Check the configuration') @@ -106,8 +125,8 @@ class FlaskClient(Client): _request_ctx_stack.pop() resp = app.response_class() - if not app.session_interface.is_null_session(sess): - app.save_session(sess, resp) + if not session_interface.is_null_session(sess): + session_interface.save_session(app, sess, resp) headers = resp.get_wsgi_headers(c.request.environ) self.cookie_jar.extract_wsgi(c.request.environ, headers) diff --git a/setup.py b/setup.py index d8cd874a..24cd9d83 100644 --- a/setup.py +++ b/setup.py @@ -48,14 +48,12 @@ import re import ast from setuptools import setup - _version_re = re.compile(r'__version__\s+=\s+(.*)') with open('flask/__init__.py', 'rb') as f: version = str(ast.literal_eval(_version_re.search( f.read().decode('utf-8')).group(1))) - setup( name='Flask', version=version, @@ -76,6 +74,17 @@ setup( 'itsdangerous>=0.21', 'click>=4.0', ], + extras_require={ + 'dev': [ + 'blinker', + 'greenlet', + 'pytest>=3', + 'coverage', + 'tox', + 'sphinx', + 'sphinxcontrib-log-cabinet' + ], + }, classifiers=[ 'Development Status :: 4 - Beta', 'Environment :: Web Environment', @@ -90,6 +99,7 @@ setup( 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ], diff --git a/tests/test_apps/cliapp/factory.py b/tests/test_apps/cliapp/factory.py new file mode 100644 index 00000000..b0d4771e --- /dev/null +++ b/tests/test_apps/cliapp/factory.py @@ -0,0 +1,15 @@ +from __future__ import absolute_import, print_function + +from flask import Flask + + +def create_app(): + return Flask('create_app') + + +def create_app2(foo, bar): + return Flask("_".join(['create_app2', foo, bar])) + + +def create_app3(foo, bar, script_info): + return Flask("_".join(['create_app3', foo, bar])) diff --git a/tests/test_basic.py b/tests/test_basic.py index 8bb079d3..e4f68832 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -9,20 +9,21 @@ :license: BSD, see LICENSE for more details. """ -import pytest - +import pickle import re -import uuid import time -import flask -import pickle +import uuid from datetime import datetime from threading import Thread -from flask._compat import text_type -from werkzeug.exceptions import BadRequest, NotFound, Forbidden + +import pytest +import werkzeug.serving +from werkzeug.exceptions import BadRequest, Forbidden, NotFound from werkzeug.http import parse_date from werkzeug.routing import BuildError -import werkzeug.serving + +import flask +from flask._compat import text_type def test_options_work(app, client): @@ -529,14 +530,14 @@ def test_session_vary_cookie(app, client): @app.route('/vary-cookie-header-set') def vary_cookie_header_set(): response = flask.Response() - response.headers['Vary'] = 'Cookie' + response.vary.add('Cookie') flask.session['test'] = 'test' return response @app.route('/vary-header-set') def vary_header_set(): response = flask.Response() - response.headers['Vary'] = 'Accept-Encoding, Accept-Language' + response.vary.update(('Accept-Encoding', 'Accept-Language')) flask.session['test'] = 'test' return response @@ -875,6 +876,13 @@ def test_error_handling(app, client): assert b'forbidden' == rv.data +def test_error_handler_unknown_code(app): + with pytest.raises(KeyError) as exc_info: + app.register_error_handler(999, lambda e: ('999', 999)) + + assert 'Use a subclass' in exc_info.value.args[0] + + def test_error_handling_processing(app, client): app.config['LOGGER_HANDLER_POLICY'] = 'never' app.testing = False @@ -980,12 +988,17 @@ def test_trapping_of_bad_request_key_errors(app, client): def fail(): flask.request.form['missing_key'] - assert client.get('/fail').status_code == 400 + rv = client.get('/fail') + assert rv.status_code == 400 + assert b'missing_key' not in rv.data app.config['TRAP_BAD_REQUEST_ERRORS'] = True + with pytest.raises(KeyError) as e: client.get("/fail") + assert e.errisinstance(BadRequest) + assert 'missing_key' in e.value.description def test_trapping_of_all_http_exceptions(app, client): diff --git a/tests/test_cli.py b/tests/test_cli.py index 459d6ef9..899fb1f0 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -150,15 +150,37 @@ def test_locate_app(test_apps): 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.factory").name == "create_app" + assert locate_app( + script_info, "cliapp.factory").name == "create_app" + assert locate_app( + script_info, "cliapp.factory:create_app").name == "create_app" + assert locate_app( + script_info, "cliapp.factory:create_app()").name == "create_app" + assert locate_app( + script_info, "cliapp.factory:create_app2('foo', 'bar')" + ).name == "create_app2_foo_bar" + assert locate_app( + script_info, "cliapp.factory:create_app2('foo', 'bar', )" + ).name == "create_app2_foo_bar" + assert locate_app( + script_info, "cliapp.factory:create_app3('baz', 'qux')" + ).name == "create_app3_baz_qux" 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") + 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.factory:create_app2('foo')") + pytest.raises( + NoAppException, locate_app, + script_info, "cliapp.factory:create_app ()") + pytest.raises( + NoAppException, locate_app, script_info, "cliapp.importerrorapp") def test_find_default_import_path(test_apps, monkeypatch, tmpdir): diff --git a/tests/test_helpers.py b/tests/test_helpers.py index a67fed0b..a2d22dd6 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -44,6 +44,7 @@ class TestJSON(object): def test_post_empty_json_adds_exception_to_response_content_in_debug(self, app, client): app.config['DEBUG'] = True + app.config['TRAP_BAD_REQUEST_ERRORS'] = False @app.route('/json', methods=['POST']) def post_json(): @@ -56,6 +57,7 @@ class TestJSON(object): def test_post_empty_json_wont_add_exception_to_response_if_no_debug(self, app, client): app.config['DEBUG'] = False + app.config['TRAP_BAD_REQUEST_ERRORS'] = False @app.route('/json', methods=['POST']) def post_json(): diff --git a/tests/test_testing.py b/tests/test_testing.py index 7ec0c993..0c8ddebe 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -73,6 +73,38 @@ def test_environ_base_modified(app, client, app_ctx): assert flask.g.user_agent == 'Bar' +def test_specify_url_scheme(app, client): + @app.route('/') + def index(): + return flask.request.url + + ctx = app.test_request_context(url_scheme='https') + assert ctx.request.url == 'https://localhost/' + + rv = client.get('/', url_scheme='https') + assert rv.data == b'https://localhost/' + + +def test_blueprint_with_subdomain(app, client): + app.config['SERVER_NAME'] = 'example.com:1234' + app.config['APPLICATION_ROOT'] = '/foo' + + bp = flask.Blueprint('company', __name__, subdomain='xxx') + + @bp.route('/') + def index(): + return flask.request.url + + app.register_blueprint(bp) + + ctx = app.test_request_context('/', subdomain='xxx') + assert ctx.request.url == 'http://xxx.example.com:1234/foo/' + assert ctx.request.blueprint == bp.name + + rv = client.get('/', subdomain='xxx') + assert rv.data == b'http://xxx.example.com:1234/foo/' + + def test_redirect_keep_session(app, client, app_ctx): app.secret_key = 'testing' diff --git a/tox.ini b/tox.ini index 1a9f4d9d..cb6dd342 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = - py{36,35,34,33,27,26,py}-release - py{36,27,py}-release-simplejson + py{36,35,34,33,27,26,py} + py{36,27,py}-simplejson py{36,33,27,26,py}-devel py{36,33,27,26,py}-lowest docs-html @@ -35,12 +35,10 @@ commands = pip install -e examples/patterns/largerapp -q # pytest-cov doesn't seem to play nice with -p - coverage run -p -m pytest + coverage run -p -m pytest tests examples [testenv:docs-html] -deps = - sphinx - flask-sphinx-themes +deps = sphinx commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html [testenv:docs-linkcheck] @@ -63,3 +61,10 @@ commands = coverage combine coverage report codecov + +[testenv:detox] +skip_install = true +deps = detox +commands = + detox -e py{36,35,34,33,27,26,py},py{36,27,py}-simplejson,py{36,33,27,26,py}-devel,py{36,33,27,26,py}-lowest,docs-html + tox -e coverage-report