diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..8383fff9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +CHANGES merge=union diff --git a/.github/ISSUE_TEMPLATE.rst b/.github/ISSUE_TEMPLATE.rst new file mode 100644 index 00000000..8854961a --- /dev/null +++ b/.github/ISSUE_TEMPLATE.rst @@ -0,0 +1,2 @@ +The issue tracker is a tool to address bugs. +Please use the #pocoo IRC channel on freenode or Stack Overflow for questions. diff --git a/.gitignore b/.gitignore index 9bf4f063..fb9baf35 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,9 @@ _mailinglist .tox .cache/ .idea/ + +# Coverage reports +htmlcov +.coverage +.coverage.* +*,cover diff --git a/.travis.yml b/.travis.yml index 8d2d3c60..32247c58 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,22 +8,35 @@ python: - "3.3" - "3.4" - "3.5" + - "3.6" env: - REQUIREMENTS=lowest + - REQUIREMENTS=lowest-simplejson - REQUIREMENTS=release + - REQUIREMENTS=release-simplejson - REQUIREMENTS=devel + - REQUIREMENTS=devel-simplejson matrix: exclude: # Python 3 support currently does not work with lowest requirements - python: "3.3" env: REQUIREMENTS=lowest + - python: "3.3" + env: REQUIREMENTS=lowest-simplejson - python: "3.4" env: REQUIREMENTS=lowest + - python: "3.4" + env: REQUIREMENTS=lowest-simplejson - python: "3.5" env: REQUIREMENTS=lowest - + - python: "3.5" + env: REQUIREMENTS=lowest-simplejson + - python: "3.6" + env: REQUIREMENTS=lowest + - python: "3.6" + env: REQUIREMENTS=lowest-simplejson install: - pip install tox diff --git a/AUTHORS b/AUTHORS index b0a4b6f3..33210243 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,9 +15,13 @@ Patches and Suggestions - Chris Grindstaff - Christopher Grebs - Daniel Neuhäuser +- Dan Sully +- David Lord @davidism - Edmond Burnett - Florent Xicluna - Georg Brandl +- Jeff Widman @jeffwidman +- Joshua Bronson @jab - Justin Quick - Kenneth Reitz - Keyan Pishdadian @@ -32,4 +36,3 @@ Patches and Suggestions - Stephane Wirtel - Thomas Schranz - Zhao Xiaohong -- David Lord @davidism diff --git a/CHANGES b/CHANGES index e3d9b0eb..11ac6430 100644 --- a/CHANGES +++ b/CHANGES @@ -3,11 +3,103 @@ Flask Changelog Here you can see the full list of changes between each Flask release. -Version 1.0 ------------ +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 + 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``) +- ``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`_). +- ``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`_) +- ``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 +.. _#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 +.. _#2256: https://github.com/pallets/flask/pull/2256 + +Version 0.12.1 +-------------- + +Bugfix release, released on March 31st 2017 + +- Prevent `flask run` from showing a NoAppException when an ImportError occurs + within the imported application module. +- Fix encoding behavior of ``app.config.from_pyfile`` for Python 3. Fix + ``#2118``. +- Use the ``SERVER_NAME`` config if it is present as default values for + ``app.run``. ``#2109``, ``#2152`` +- Call `ctx.auto_pop` with the exception object instead of `None`, in the + event that a `BaseException` such as `KeyboardInterrupt` is raised in a + request handler. + +Version 0.12 +------------ + +Released on December 21st 2016, codename Punsch. + +- the cli command now responds to `--version`. +- Mimetype guessing and ETag generation for file-like objects in ``send_file`` + has been removed, as per issue ``#104``. See pull request ``#1849``. +- Mimetype guessing in ``send_file`` now fails loudly and doesn't fall back to + ``application/octet-stream``. See pull request ``#1988``. +- Make ``flask.safe_join`` able to join multiple paths like ``os.path.join`` + (pull request ``#1730``). +- Revert a behavior change that made the dev server crash instead of returning + a Internal Server Error (pull request ``#2006``). +- Correctly invoke response handlers for both regular request dispatching as + well as error handlers. +- Disable logger propagation by default for the app logger. +- Add support for range requests in ``send_file``. +- ``app.test_client`` includes preset default environment, which can now be + directly set, instead of per ``client.get``. + +Version 0.11.2 +-------------- + +Bugfix release, unreleased + +- Fix crash when running under PyPy3, see pull request ``#1814``. + +Version 0.11.1 +-------------- + +Bugfix release, released on June 7th 2016. + +- Fixed a bug that prevented ``FLASK_APP=foobar/__init__.py`` from working. See + pull request ``#1872``. + +Version 0.11 +------------ -(release date to be announced, codename to be selected) +Released on May 29th 2016, codename Absinthe. +- Added support to serializing top-level arrays to :func:`flask.jsonify`. This + introduces a security risk in ancient browsers. See + :ref:`json-security` for details. - Added before_render_template signal. - Added `**kwargs` to :meth:`flask.Test.test_client` to support passing additional keyword arguments to the constructor of @@ -23,7 +115,7 @@ Version 1.0 from a view function. - Added :meth:`flask.Config.from_json`. - Added :attr:`flask.Flask.config_class`. -- Added :meth:`flask.config.Config.get_namespace`. +- Added :meth:`flask.Config.get_namespace`. - Templates are no longer automatically reloaded outside of debug mode. This can be configured with the new ``TEMPLATES_AUTO_RELOAD`` config key. - Added a workaround for a limitation in Python 3.3's namespace loader. @@ -57,7 +149,7 @@ Version 1.0 - JSON responses are now terminated with a newline character, because it is a convention that UNIX text files end with a newline and some clients don't deal well when this newline is missing. See - https://github.com/mitsuhiko/flask/pull/1262 -- this came up originally as a + https://github.com/pallets/flask/pull/1262 -- this came up originally as a part of https://github.com/kennethreitz/httpbin/issues/168 - The automatically provided ``OPTIONS`` method is now correctly disabled if the user registered an overriding rule with the lowercase-version @@ -70,6 +162,12 @@ 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``). +- ``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``). +- Added the ``JSONIFY_MIMETYPE`` configuration variable (pull request ``#1728``). +- Exceptions during teardown handling will no longer leave bad application + contexts lingering around. Version 0.10.2 -------------- @@ -292,7 +390,7 @@ Released on September 29th 2011, codename Rakija - Applications now not only have a root path where the resources and modules are located but also an instance path which is the designated place to drop files that are modified at runtime (uploads etc.). Also this is - conceptionally only instance depending and outside version control so it's + conceptually only instance depending and outside version control so it's the perfect place to put configuration files etc. For more information see :ref:`instance-folders`. - Added the ``APPLICATION_ROOT`` configuration variable. diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 12083f52..66766512 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -10,7 +10,7 @@ Support questions 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. -`StackOverflow `_ is also worth considering. +`Stack Overflow `_ is also worth considering. Reporting issues ================ @@ -28,7 +28,7 @@ Submitting patches clearly under which circumstances the bug happens. Make sure the test fails without your patch. -- Try to follow `PEP8 `_, but you +- Try to follow `PEP8 `_, but you may ignore the line-length-limit if following it would make the code uglier. @@ -36,16 +36,16 @@ Running the testsuite --------------------- You probably want to set up a `virtualenv -`_. +`_. -The minimal requirement for running the testsuite is ``py.test``. You can +The minimal requirement for running the testsuite is ``pytest``. You can install it with:: pip install pytest Clone this repository:: - git clone https://github.com/mitsuhiko/flask.git + git clone https://github.com/pallets/flask.git Install Flask as an editable package using the current source:: @@ -54,19 +54,61 @@ Install Flask as an editable package using the current source:: Then you can run the testsuite with:: - py.test + pytest -With only py.test installed, a large part of the testsuite will get skipped +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``. Currently we're depending on a development version of Tox -because the released version is missing features we absolutely need. You can -install it with:: +of ``pytest``. You can install it with:: - pip install hg+https://bitbucket.org/hpk42/tox + pip install tox The ``tox`` command will then run all tests against multiple combinations Python versions and dependency versions. + +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 + +After this has been installed, you can output a report to the command line using this command:: + + pytest --cov=flask tests/ + +Generate a HTML report can be done using this command:: + + pytest --cov-report html --cov=flask tests/ + +Full docs on ``coverage.py`` are here: https://coverage.readthedocs.io + +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. + + +cloning +------- +The zero-padded file modes files above can cause issues while cloning, too. If you have + +:: + + [fetch] + fsckobjects = true + +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. diff --git a/MANIFEST.in b/MANIFEST.in index ce115864..f8d9c2ae 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,13 +1,11 @@ include Makefile CHANGES LICENSE AUTHORS -recursive-include artwork * -recursive-include tests * -recursive-include examples * -recursive-include docs * -recursive-exclude docs *.pyc -recursive-exclude docs *.pyo -recursive-exclude tests *.pyc -recursive-exclude tests *.pyo -recursive-exclude examples *.pyc -recursive-exclude examples *.pyo + +graft artwork +graft tests +graft examples +graft docs + +global-exclude *.py[co] + prune docs/_build prune docs/_themes diff --git a/Makefile b/Makefile index 1268a1b4..f76c2099 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,8 @@ all: clean-pyc test test: - py.test tests examples - -tox-test: - tox + pip install -r test-requirements.txt + tox -e py-release audit: python setup.py audit diff --git a/README b/README index d0e3c521..75c5e7b1 100644 --- a/README +++ b/README @@ -33,9 +33,11 @@ Good that you're asking. The tests are in the tests/ folder. To run the tests use the - `py.test` testing tool: + `pytest` testing tool: - $ py.test + $ pytest + + Details on contributing can be found in CONTRIBUTING.rst ~ Where can I get help? diff --git a/docs/_templates/sidebarintro.html b/docs/_templates/sidebarintro.html index 25245591..71fcd73b 100644 --- a/docs/_templates/sidebarintro.html +++ b/docs/_templates/sidebarintro.html @@ -16,7 +16,7 @@

Useful Links

diff --git a/docs/advanced_foreword.rst b/docs/advanced_foreword.rst index 22e333f8..82b3dc58 100644 --- a/docs/advanced_foreword.rst +++ b/docs/advanced_foreword.rst @@ -46,24 +46,10 @@ spam, links to malicious software, and the like. Flask is no different from any other framework in that you the developer must build with caution, watching for exploits when building to your requirements. -The Status of Python 3 ----------------------- - -Currently the Python community is in the process of improving libraries to -support the new iteration of the Python programming language. While the -situation is greatly improving there are still some issues that make it -hard for users to switch over to Python 3 just now. These problems are -partially caused by changes in the language that went unreviewed for too -long, partially also because we have not quite worked out how the lower- -level API should change to account for the Unicode differences in Python 3. - -We strongly recommend using Python 2.7 with activated Python 3 -warnings during development. If you plan on upgrading to Python 3 in the -near future we strongly recommend that you read `How to write forwards -compatible Python code -`_. +Python 3 Support in Flask +------------------------- -If you do want to dive into Python 3 already have a look at the -:ref:`python3-support` page. +Flask, its dependencies, and most Flask extensions all support Python 3. +If you want to use Flask with Python 3 have a look at the :ref:`python3-support` page. Continue to :ref:`installation` or the :ref:`quickstart`. diff --git a/docs/api.rst b/docs/api.rst index 3da975e9..b5009907 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -30,61 +30,12 @@ Incoming Request Data .. autoclass:: Request :members: - - .. attribute:: form - - A :class:`~werkzeug.datastructures.MultiDict` with the parsed form data from ``POST`` - or ``PUT`` requests. Please keep in mind that file uploads will not - end up here, but instead in the :attr:`files` attribute. - - .. attribute:: args - - A :class:`~werkzeug.datastructures.MultiDict` with the parsed contents of the query - string. (The part in the URL after the question mark). - - .. attribute:: values - - A :class:`~werkzeug.datastructures.CombinedMultiDict` with the contents of both - :attr:`form` and :attr:`args`. - - .. attribute:: cookies - - A :class:`dict` with the contents of all cookies transmitted with - the request. - - .. attribute:: stream - - If the incoming form data was not encoded with a known mimetype - the data is stored unmodified in this stream for consumption. Most - of the time it is a better idea to use :attr:`data` which will give - you that data as a string. The stream only returns the data once. - - .. attribute:: headers - - The incoming request headers as a dictionary like object. - - .. attribute:: data - - Contains the incoming request data as string in case it came with - a mimetype Flask does not handle. - - .. attribute:: files - - A :class:`~werkzeug.datastructures.MultiDict` with files uploaded as part of a - ``POST`` or ``PUT`` request. Each file is stored as - :class:`~werkzeug.datastructures.FileStorage` object. It basically behaves like a - standard file object you know from Python, with the difference that - it also has a :meth:`~werkzeug.datastructures.FileStorage.save` function that can - store the file on the filesystem. + :inherited-members: .. attribute:: environ The underlying WSGI environment. - .. attribute:: method - - The current request method (``POST``, ``GET`` etc.) - .. attribute:: path .. attribute:: full_path .. attribute:: script_root @@ -114,15 +65,8 @@ Incoming Request Data `url_root` ``u'http://www.example.com/myapplication/'`` ============= ====================================================== - .. attribute:: is_xhr - - ``True`` if the request was triggered via a JavaScript - `XMLHttpRequest`. This only works with libraries that support the - ``X-Requested-With`` header and set it to `XMLHttpRequest`. - Libraries that do that are prototype, jQuery and Mochikit and - probably some more. -.. class:: request +.. attribute:: request To access incoming request data, you can use the global `request` object. Flask parses incoming request data for you and gives you @@ -289,7 +233,7 @@ thing, like it does for :class:`request` and :class:`session`. It's now also possible to use the ``in`` operator on it to see if an attribute is defined and it yields all keys on iteration. - As of 1.0 you can use :meth:`pop` and :meth:`setdefault` in the same + As of 0.11 you can use :meth:`pop` and :meth:`setdefault` in the same way you would use them on a dictionary. This is a proxy. See :ref:`notes-on-proxies` for more information. @@ -316,13 +260,7 @@ Useful Functions and Classes .. autofunction:: url_for -.. function:: abort(code) - - Raises an :exc:`~werkzeug.exceptions.HTTPException` for the given - status code. For example to abort request handling with a page not - found exception, you would call ``abort(404)``. - - :param code: the HTTP error code. +.. autofunction:: abort .. autofunction:: redirect @@ -751,6 +689,8 @@ The following converters are available: `int` accepts integers `float` like `int` but for floating point values `path` like the default but also accepts slashes +`any` matches one of the items provided +`uuid` accepts UUID strings =========== =============================================== Custom converters can be defined using :attr:`flask.Flask.url_map`. @@ -890,13 +830,6 @@ Command Line Interface Marks a function so that an instance of :class:`ScriptInfo` is passed as first argument to the click callback. -.. autofunction:: script_info_option - - A special decorator that informs a click callback to be passed the - script info object as first argument. This is normally not useful - unless you implement very special commands like the run command which - does not want the application to be loaded yet. - .. autodata:: run_command .. autodata:: shell_command diff --git a/docs/appcontext.rst b/docs/appcontext.rst index 07e44f94..166c5aa3 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -26,8 +26,8 @@ In contrast, during request handling, a couple of other rules exist: 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 +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. @@ -74,6 +74,11 @@ The application context is also used by the :func:`~flask.url_for` function in case a ``SERVER_NAME`` was configured. This allows you to generate URLs even in the absence of a request. +If no request context has been pushed and an application context has +not been explicitly set, a ``RuntimeError`` will be raised. :: + + RuntimeError: Working outside of application context. + Locality of the Context ----------------------- diff --git a/docs/becomingbig.rst b/docs/becomingbig.rst index 8b0a2743..0facbfee 100644 --- a/docs/becomingbig.rst +++ b/docs/becomingbig.rst @@ -12,7 +12,7 @@ Flask started in part to demonstrate how to build your own framework on top of existing well-used tools Werkzeug (WSGI) and Jinja (templating), and as it developed, it became useful to a wide audience. As you grow your codebase, don't just use Flask -- understand it. Read the source. Flask's code is -written to be read; it's documentation is published so you can use its internal +written to be read; its documentation is published so you can use its internal APIs. Flask sticks to documented APIs in upstream libraries, and documents its internal utilities so that you can find the hook points needed for your project. @@ -35,7 +35,7 @@ Subclass. The :class:`~flask.Flask` class has many methods designed for subclassing. You can quickly add or customize behavior by subclassing :class:`~flask.Flask` (see the linked method docs) and using that subclass wherever you instantiate an -application class. This works well with :ref:`app-factories`. +application class. This works well with :ref:`app-factories`. See :doc:`/patterns/subclassing` for an example. Wrap with middleware. --------------------- diff --git a/docs/blueprints.rst b/docs/blueprints.rst index d22220b2..89d3701e 100644 --- a/docs/blueprints.rst +++ b/docs/blueprints.rst @@ -176,16 +176,25 @@ the `template_folder` parameter to the :class:`Blueprint` constructor:: admin = Blueprint('admin', __name__, template_folder='templates') -As for static files, the path can be absolute or relative to the blueprint -resource folder. The template folder is added to the searchpath 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. +For static files, the path can be absolute or relative to the blueprint +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. +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 +template path the first blueprint registered takes precedence over the others. + So if you have a blueprint in the folder ``yourapplication/admin`` and you want to render the template ``'admin/index.html'`` and you have provided ``templates`` as a `template_folder` you will have to create a file like -this: :file:`yourapplication/admin/templates/admin/index.html`. +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. 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 diff --git a/docs/cli.rst b/docs/cli.rst index 141176ce..d0b033f6 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -3,11 +3,11 @@ Command Line Interface ====================== -.. versionadded:: 1.0 +.. versionadded:: 0.11 .. currentmodule:: flask -One of the nice new features in Flask 1.0 is the built-in integration of +One of the nice new features in Flask 0.11 is the built-in integration of the `click `_ command line interface. This enables a wide range of new features for the Flask ecosystem and your own applications. @@ -15,41 +15,38 @@ applications. Basic Usage ----------- -After installation of Flask you will now find a :command:`flask` script installed -into your virtualenv. If you don't want to install Flask or you have a -special use-case you can also use ``python -m flask`` to accomplish exactly -the same. +After installation of Flask you will now find a :command:`flask` script +installed into your virtualenv. If you don't want to install Flask or you +have a special use-case you can also use ``python -m flask`` to accomplish +exactly the same. The way this script works is by providing access to all the commands on your Flask application's :attr:`Flask.cli` instance as well as some built-in commands that are always there. Flask extensions can also register more commands there if they desire so. -For the :command:`flask` script to work, an application needs to be discovered. -The two most common ways are either an environment variable -(``FLASK_APP``) or the :option:`--app` / :option:`-a` parameter. It should be the -import path for your application or the path to a Python file. In the -latter case Flask will attempt to setup the Python path for you -automatically and discover the module name but that might not always work. +For the :command:`flask` script to work, an application needs to be +discovered. This is achieved by exporting the ``FLASK_APP`` environment +variable. It can be either set to an import path or to a filename of a +Python module that contains a Flask application. In that imported file the name of the app needs to be called ``app`` or -optionally be specified after a colon. +optionally be specified after a colon. For instance +``mymodule:application`` would tell it to use the `application` object in +the :file:`mymodule.py` file. -Given a :file:`hello.py` file with the application in it named ``app`` this is -how it can be run. +Given a :file:`hello.py` file with the application in it named ``app`` +this is how it can be run. Environment variables (On Windows use ``set`` instead of ``export``):: export FLASK_APP=hello flask run -Parameters:: +Or with a filename:: - flask --app=hello run - -File names:: - - flask --app=hello.py run + export FLASK_APP=/path/to/hello.py + flask run Virtualenv Integration ---------------------- @@ -59,19 +56,33 @@ 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 ---------- -The :command:`flask` script can be run with :option:`--debug` or :option:`--no-debug` to -automatically flip the debug flag of the application. This can also be -configured by setting ``FLASK_DEBUG`` to ``1`` or ``0``. +The :command:`flask` script can also be instructed to enable the debug +mode of the application automatically by exporting ``FLASK_DEBUG``. If +set to ``1`` debug is enabled or ``0`` disables it:: + + export FLASK_DEBUG=1 Running a Shell --------------- To run an interactive Python shell you can use the ``shell`` command:: - flask --app=hello shell + flask shell This will start up an interactive Python shell, setup the correct application context and setup the local variables in the shell. This is @@ -86,6 +97,7 @@ easily. Flask uses `click`_ for the command interface which makes creating custom commands very easy. For instance if you want a shell command to initialize the database you can do this:: + import click from flask import Flask app = Flask(__name__) @@ -93,11 +105,11 @@ command to initialize the database you can do this:: @app.cli.command() def initdb(): """Initialize the database.""" - print 'Init the db' + click.echo('Init the db') The command will then show up on the command line:: - $ flask -a hello.py initdb + $ flask initdb Init the db Application Context @@ -122,12 +134,12 @@ Factory Functions ----------------- In case you are using factory functions to create your application (see -:ref:`app-factories`) you will discover that the :command:`flask` command cannot -work with them directly. Flask won't be able to figure out how to +:ref:`app-factories`) you will discover that the :command:`flask` command +cannot work with them directly. Flask won't be able to figure out how to instantiate your application properly by itself. Because of this reason the recommendation is to create a separate file that instantiates -applications. This is by far not the only way to make this work. Another -is the :ref:`custom-scripts` support. +applications. This is not the only way to make this work. Another is the +:ref:`custom-scripts` support. For instance if you have a factory function that creates an application from a filename you could make a separate file that creates such an @@ -139,8 +151,8 @@ This could be a file named :file:`autoapp.py` with these contents:: from yourapplication import create_app app = create_app(os.environ['YOURAPPLICATION_CONFIG']) -Once this has happened you can make the flask command automatically pick -it up:: +Once this has happened you can make the :command:`flask` command automatically +pick it up:: export YOURAPPLICATION_CONFIG=/path/to/config.cfg export FLASK_APP=/path/to/autoapp.py @@ -152,9 +164,9 @@ From this point onwards :command:`flask` will find your application. Custom Scripts -------------- -While the most common way is to use the :command:`flask` command, you can also -make your own "driver scripts". Since Flask uses click for the scripts -there is no reason you cannot hook these scripts into any click +While the most common way is to use the :command:`flask` command, you can +also make your own "driver scripts". Since Flask uses click for the +scripts there is no reason you cannot hook these scripts into any click application. There is one big caveat and that is, that commands registered to :attr:`Flask.cli` will expect to be (indirectly at least) launched from a :class:`flask.cli.FlaskGroup` click group. This is @@ -162,38 +174,32 @@ necessary so that the commands know which Flask application they have to work with. To understand why you might want custom scripts you need to understand how -click finds and executes the Flask application. If you use the :command:`flask` -script you specify the application to work with on the command line or -environment variable as an import name. This is simple but it has some -limitations. Primarily it does not work with application factory -functions (see :ref:`app-factories`). +click finds and executes the Flask application. If you use the +:command:`flask` script you specify the application to work with on the +command line or environment variable as an import name. This is simple +but it has some limitations. Primarily it does not work with application +factory functions (see :ref:`app-factories`). With a custom script you don't have this problem as you can fully customize how the application will be created. This is very useful if you write reusable applications that you want to ship to users and they should be presented with a custom management script. -If you are used to writing click applications this will look familiar but -at the same time, slightly different because of how commands are loaded. -We won't go into detail now about the differences but if you are curious -you can have a look at the :ref:`script-info-object` section to learn all -about it. - To explain all of this, here is an example :file:`manage.py` script that manages a hypothetical wiki application. We will go through the details afterwards:: + import os import click - from flask.cli import FlaskGroup, script_info_option + from flask.cli import FlaskGroup def create_wiki_app(info): from yourwiki import create_app - config = info.data.get('config') or 'wikiconfig.py' - return create_app(config=config) + return create_app( + config=os.environ.get('WIKI_CONFIG', 'wikiconfig.py')) @click.group(cls=FlaskGroup, create_app=create_wiki_app) - @script_info_option('--config', script_info_key='config') - def cli(**params): + def cli(): """This is a management script for the wiki application.""" if __name__ == '__main__': @@ -204,56 +210,51 @@ step. 1. First we import the ``click`` library as well as the click extensions from the ``flask.cli`` package. Primarily we are here interested - in the :class:`~flask.cli.FlaskGroup` click group and the - :func:`~flask.cli.script_info_option` decorator. + in the :class:`~flask.cli.FlaskGroup` click group. 2. The next thing we do is defining a function that is invoked with the - script info object (:ref:`script-info-object`) from Flask and its + script info object (:class:`~flask.cli.ScriptInfo`) from Flask and its purpose is to fully import and create the application. This can either directly import an application object or create it (see - :ref:`app-factories`). - - What is ``info.data``? It's a dictionary of arbitrary data on the - script info that can be filled by options or through other means. We - will come back to this later. + :ref:`app-factories`). In this case we load the config from an + environment variable. 3. Next step is to create a :class:`FlaskGroup`. In this case we just make an empty function with a help doc string that just does nothing and then pass the ``create_wiki_app`` function as a factory function. Whenever click now needs to operate on a Flask application it will call that function with the script info and ask for it to be created. -4. In step 2 you could see that the config is passed to the actual - creation function. This config comes from the :func:`script_info_option` - decorator for the main script. It accepts a :option:`--config` option and - then stores it in the script info so we can use it to create the - application. -5. All is rounded up by invoking the script. +4. All is rounded up by invoking the script. + +CLI Plugins +----------- -.. _script-info-object: +Flask extensions can always patch the :attr:`Flask.cli` instance with more +commands if they want. However there is a second way to add CLI plugins +to Flask which is through ``setuptools``. If you make a Python package that +should export a Flask command line plugin you can ship a :file:`setup.py` file +that declares an entrypoint that points to a click command: -The Script Info ---------------- +Example :file:`setup.py`:: + + from setuptools import setup + + setup( + name='flask-my-extension', + ... + entry_points=''' + [flask.commands] + my-command=mypackage.commands:cli + ''', + ) + +Inside :file:`mypackage/commands.py` you can then export a Click object:: + + import click + + @click.command() + def cli(): + """This is an example command.""" -The Flask script integration might be confusing at first, but there is a reason -why it's done this way. The reason for this is that Flask wants to -both provide custom commands to click as well as not loading your -application unless it has to. The reason for this is added flexibility. - -This way an application can provide custom commands, but even in the -absence of an application the :command:`flask` script is still operational on a -basic level. In addition to that it means that the individual commands -have the option to avoid creating an instance of the Flask application -unless required. This is very useful as it allows the server commands for -instance to load the application on a first request instead of -immediately, therefore giving a better debug experience. - -All of this is provided through the :class:`flask.cli.ScriptInfo` object -and some helper utilities around. The basic way it operates is that when -the :class:`flask.cli.FlaskGroup` executes as a script it creates a script -info and keeps it around. From that point onwards modifications on the -script info can be done through click options. To simplify this pattern -the :func:`flask.cli.script_info_option` decorator was added. - -Once Flask actually needs the individual Flask application it will invoke -the :meth:`flask.cli.ScriptInfo.load_app` method. This happens when the -server starts, when the shell is launched or when the script looks for an -application-provided click command. +Once that package is installed in the same virtualenv as Flask itself you +can run ``flask my-command`` to invoke your command. This is useful to +provide extra functionality that Flask itself cannot ship. diff --git a/docs/conf.py b/docs/conf.py index 285c8981..f53d72fa 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,14 +10,20 @@ # # All configuration values have a default; values that are commented out # serve to show the default. +from __future__ import print_function +import os +import sys +import pkg_resources +import time +import datetime -import sys, os +BUILD_DATE = datetime.datetime.utcfromtimestamp(int(os.environ.get('SOURCE_DATE_EPOCH', time.time()))) # 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.abspath('_themes')) -sys.path.append(os.path.abspath('.')) +sys.path.append(os.path.join(os.path.dirname(__file__), '_themes')) +sys.path.append(os.path.dirname(__file__)) # -- General configuration ----------------------------------------------------- @@ -26,8 +32,11 @@ sys.path.append(os.path.abspath('.')) # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', - 'flaskdocext'] +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.intersphinx', + 'flaskdocext' +] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -43,24 +52,21 @@ master_doc = 'index' # General information about the project. project = u'Flask' -copyright = u'2015, Armin Ronacher' +copyright = u'2010 - {0}, Armin Ronacher'.format(BUILD_DATE.year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. -import pkg_resources try: release = pkg_resources.get_distribution('Flask').version except pkg_resources.DistributionNotFound: - print 'To build the documentation, The distribution information of Flask' - print 'Has to be available. Either install the package into your' - print 'development environment or run "setup.py develop" to setup the' - print 'metadata. A virtualenv is recommended!' + print('Flask must be installed to build the documentation.') + print('Install from source using `pip install -e .` in a virtualenv.') sys.exit(1) -del pkg_resources if 'dev' in release: - release = release.split('dev')[0] + 'dev' + release = ''.join(release.partition('dev')[:2]) + version = '.'.join(release.split('.')[:2]) # The language for content autogenerated by Sphinx. Refer to documentation @@ -99,14 +105,12 @@ exclude_patterns = ['_build'] # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'flask' +# html_theme = 'default' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = { - 'touch_icon': 'touch-icon.png' -} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_themes'] @@ -125,7 +129,7 @@ html_theme_path = ['_themes'] # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -html_favicon = "flask-favicon.ico" +html_favicon = '_static/flask-favicon.ico' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, @@ -142,9 +146,18 @@ html_static_path = ['_static'] # Custom sidebar templates, maps document names to template names. html_sidebars = { - 'index': ['sidebarintro.html', 'sourcelink.html', 'searchbox.html'], - '**': ['sidebarlogo.html', 'localtoc.html', 'relations.html', - 'sourcelink.html', 'searchbox.html'] + 'index': [ + 'sidebarintro.html', + 'sourcelink.html', + 'searchbox.html' + ], + '**': [ + 'sidebarlogo.html', + 'localtoc.html', + 'relations.html', + 'sourcelink.html', + 'searchbox.html' + ] } # Additional templates that should be rendered to pages, maps page names to @@ -186,8 +199,7 @@ htmlhelp_basename = 'Flaskdoc' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('latexindex', 'Flask.tex', u'Flask Documentation', - u'Armin Ronacher', 'manual'), + ('latexindex', 'Flask.tex', u'Flask Documentation', u'Armin Ronacher', 'manual'), ] # Documents to append as an appendix to all manuals. @@ -197,10 +209,10 @@ latex_documents = [ latex_use_modindex = False latex_elements = { - 'fontpkg': r'\usepackage{mathpazo}', - 'papersize': 'a4paper', - 'pointsize': '12pt', - 'preamble': r'\usepackage{flaskstyle}' + 'fontpkg': r'\usepackage{mathpazo}', + 'papersize': 'a4paper', + 'pointsize': '12pt', + 'preamble': r'\usepackage{flaskstyle}' } latex_use_parts = True @@ -222,7 +234,7 @@ latex_additional_files = ['flaskstyle.sty', 'logo.pdf'] # The scheme of the identifier. Typical schemes are ISBN or URL. #epub_scheme = '' -# The unique identifier of the text. This can be a ISBN number +# The unique identifier of the text. This can be an ISBN number # or the project homepage. #epub_identifier = '' @@ -244,31 +256,29 @@ latex_additional_files = ['flaskstyle.sty', 'logo.pdf'] #epub_tocdepth = 3 intersphinx_mapping = { - 'https://docs.python.org/dev': None, - 'http://werkzeug.pocoo.org/docs/': None, - 'http://click.pocoo.org/': None, - 'http://jinja.pocoo.org/docs/': None, - 'http://www.sqlalchemy.org/docs/': None, - 'https://wtforms.readthedocs.org/en/latest/': None, - 'https://pythonhosted.org/blinker/': None + 'python': ('https://docs.python.org/3/', None), + 'werkzeug': ('http://werkzeug.pocoo.org/docs/', None), + 'click': ('http://click.pocoo.org/', None), + 'jinja': ('http://jinja.pocoo.org/docs/', None), + 'sqlalchemy': ('https://docs.sqlalchemy.org/en/latest/', None), + 'wtforms': ('https://wtforms.readthedocs.io/en/latest/', None), + 'blinker': ('https://pythonhosted.org/blinker/', None) } -pygments_style = 'flask_theme_support.FlaskyStyle' - -# fall back if theme is not there try: __import__('flask_theme_support') -except ImportError, e: - 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 - - pygments_style = 'tango' - html_theme = 'default' - html_theme_options = {} + 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) # unwrap decorators diff --git a/docs/config.rst b/docs/config.rst index 245d3bab..75ce239a 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -44,6 +44,21 @@ method:: SECRET_KEY='...' ) +.. admonition:: Debug Mode with the ``flask`` Script + + If you use the :command:`flask` script to start a local development + server, to enable the debug mode, you need to export the ``FLASK_DEBUG`` + environment variable before running the server:: + + $ export FLASK_DEBUG=1 + $ flask run + + (On Windows you need to use ``set`` instead of ``export``). + + ``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 ---------------------------- @@ -52,7 +67,8 @@ The following configuration values are used internally by Flask: .. tabularcolumns:: |p{6.5cm}|p{8.5cm}| ================================= ========================================= -``DEBUG`` enable/disable debug mode +``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 @@ -177,12 +193,11 @@ The following configuration values are used internally by Flask: behavior by changing this variable. This is not recommended but might give you a performance improvement on the - cost of cachability. -``JSONIFY_PRETTYPRINT_REGULAR`` If this is set to ``True`` (the default) - jsonify responses will be pretty printed - if they are not requested by an - XMLHttpRequest object (controlled by - the ``X-Requested-With`` header) + 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 @@ -240,7 +255,7 @@ The following configuration values are used internally by Flask: .. versionadded:: 0.10 ``JSON_AS_ASCII``, ``JSON_SORT_KEYS``, ``JSONIFY_PRETTYPRINT_REGULAR`` -.. versionadded:: 1.0 +.. versionadded:: 0.11 ``SESSION_REFRESH_EACH_REQUEST``, ``TEMPLATES_AUTO_RELOAD``, ``LOGGER_HANDLER_POLICY``, ``EXPLAIN_TEMPLATE_LOADING`` @@ -261,7 +276,7 @@ So a common pattern is this:: This first loads the configuration from the `yourapplication.default_settings` module and then overrides the values -with the contents of the file the :envvar:``YOURAPPLICATION_SETTINGS`` +with the contents of the file the :envvar:`YOURAPPLICATION_SETTINGS` environment variable points to. This environment variable can be set on Linux or OS X with the export command in the shell before starting the server:: @@ -309,6 +324,7 @@ that experience: limit yourself to request-only accesses to the configuration you can reconfigure the object later on as needed. +.. _config-dev-prod: Development / Production ------------------------ @@ -330,7 +346,7 @@ there are alternative ways as well. For example you could use imports or subclassing. What is very popular in the Django world is to make the import explicit in -the config file by adding an ``from yourapplication.default_settings +the config file by adding ``from yourapplication.default_settings import *`` to the top of the file and then overriding the changes by hand. You could also inspect an environment variable like ``YOURAPPLICATION_MODE`` and set that to `production`, `development` etc diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index c0beae0c..5ca2a084 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -144,7 +144,7 @@ A basic FastCGI configuration for lighttpd looks like that:: ) alias.url = ( - "/static/" => "/path/to/your/static" + "/static/" => "/path/to/your/static/" ) url.rewrite-once = ( @@ -159,7 +159,7 @@ work in the URL root you have to work around a lighttpd bug with the Make sure to apply it only if you are mounting the application the URL root. Also, see the Lighty docs for more information on `FastCGI and Python -`_ (note that +`_ (note that explicitly passing a socket to run() is no longer necessary). Configuring nginx @@ -234,7 +234,7 @@ python path. Common problems are: web server. - Different python interpreters being used. -.. _nginx: http://nginx.org/ -.. _lighttpd: http://www.lighttpd.net/ +.. _nginx: https://nginx.org/ +.. _lighttpd: https://www.lighttpd.net/ .. _cherokee: http://cherokee-project.com/ .. _flup: https://pypi.python.org/pypi/flup diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index ba506d6f..6950e47a 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -19,12 +19,12 @@ Hosted options - `Deploying Flask on Heroku `_ - `Deploying Flask on OpenShift `_ -- `Deploying WSGI on dotCloud `_ - with `Flask-specific notes `_ - `Deploying Flask on Webfaction `_ - `Deploying Flask on Google App Engine `_ +- `Deploying Flask on AWS Elastic Beanstalk `_ - `Sharing your Localhost Server with Localtunnel `_ - `Deploying on Azure (IIS) `_ +- `Deploying on PythonAnywhere `_ Self-hosted options ------------------- diff --git a/docs/deploying/mod_wsgi.rst b/docs/deploying/mod_wsgi.rst index 9145b4f9..ca694b7d 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -13,7 +13,7 @@ If you are using the `Apache`_ webserver, consider using `mod_wsgi`_. not called because this will always start a local WSGI server which we do not want if we deploy that application to mod_wsgi. -.. _Apache: http://httpd.apache.org/ +.. _Apache: https://httpd.apache.org/ Installing `mod_wsgi` --------------------- @@ -114,7 +114,7 @@ refuse to run with the above configuration. On a Windows system, eliminate those Note: There have been some changes in access control configuration for `Apache 2.4`_. -.. _Apache 2.4: http://httpd.apache.org/docs/trunk/upgrading.html +.. _Apache 2.4: https://httpd.apache.org/docs/trunk/upgrading.html Most notably, the syntax for directory permissions has changed from httpd 2.2 @@ -130,12 +130,12 @@ to httpd 2.4 syntax Require all granted -For more information consult the `mod_wsgi wiki`_. +For more information consult the `mod_wsgi documentation`_. -.. _mod_wsgi: http://code.google.com/p/modwsgi/ -.. _installation instructions: http://code.google.com/p/modwsgi/wiki/QuickInstallationGuide +.. _mod_wsgi: https://github.com/GrahamDumpleton/mod_wsgi +.. _installation instructions: https://modwsgi.readthedocs.io/en/develop/installation.html .. _virtual python: https://pypi.python.org/pypi/virtualenv -.. _mod_wsgi wiki: http://code.google.com/p/modwsgi/w/list +.. _mod_wsgi documentation: https://modwsgi.readthedocs.io/en/develop/index.html Troubleshooting --------------- @@ -143,7 +143,7 @@ Troubleshooting If your application does not run, follow this guide to troubleshoot: **Problem:** application does not run, errorlog shows SystemExit ignored - You have a ``app.run()`` call in your application file that is not + You have an ``app.run()`` call in your application file that is not guarded by an ``if __name__ == '__main__':`` condition. Either remove that :meth:`~flask.Flask.run` call from the file and move it into a separate :file:`run.py` file or put it into such an if block. diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index 2d15440f..50c85fb2 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -29,20 +29,16 @@ Given a flask application in myapp.py, use the following command: .. sourcecode:: text - $ uwsgi -s /tmp/uwsgi.sock --manage-script-name --mount /yourapplication=myapp:app + $ uwsgi -s /tmp/yourapplication.sock --manage-script-name --mount /yourapplication=myapp:app -Or, if you prefer: - -.. sourcecode:: text - - $ uwsgi -s /tmp/uwsgi.sock --manage-script-name --mount /yourapplication=myapp:app - -The ``--manage-script-name`` will move the handling of ``SCRIPT_NAME`` to -uwsgi, since its smarter about that. It is used together with the ``--mount`` -directive which will make requests to ``/yourapplication`` be directed to -``myapp:app``, where ``myapp`` refers to the name of the file of your flask -application (without extension). ``app`` is the callable inside of your -application (usually the line reads ``app = Flask(__name__)``. +The ``--manage-script-name`` will move the handling of ``SCRIPT_NAME`` to uwsgi, +since its smarter about that. It is used together with the ``--mount`` directive +which will make requests to ``/yourapplication`` be directed to ``myapp:app``. +If your application is accessible at root level, you can use a single ``/`` +instead of ``/yourapplication``. ``myapp`` refers to the name of the file of +your flask application (without extension) or the module which provides ``app``. +``app`` is the callable inside of your application (usually the line reads +``app = Flask(__name__)``. If you want to deploy your flask application inside of a virtual environment, you need to also add ``--virtualenv /path/to/virtual/environment``. You might @@ -70,7 +66,7 @@ to have it in the URL root its a bit simpler:: uwsgi_pass unix:/tmp/yourapplication.sock; } -.. _nginx: http://nginx.org/ -.. _lighttpd: http://www.lighttpd.net/ +.. _nginx: https://nginx.org/ +.. _lighttpd: https://www.lighttpd.net/ .. _cherokee: http://cherokee-project.com/ .. _uwsgi: http://projects.unbit.it/uwsgi/ diff --git a/docs/deploying/wsgi-standalone.rst b/docs/deploying/wsgi-standalone.rst index bd7b1006..ad43c144 100644 --- a/docs/deploying/wsgi-standalone.rst +++ b/docs/deploying/wsgi-standalone.rst @@ -25,7 +25,7 @@ For example, to run a Flask application with 4 worker processes (``-w .. _Gunicorn: http://gunicorn.org/ .. _eventlet: http://eventlet.net/ -.. _greenlet: http://greenlet.readthedocs.org/en/latest/ +.. _greenlet: https://greenlet.readthedocs.io/en/latest/ Gevent ------- @@ -41,7 +41,7 @@ event loop:: http_server.serve_forever() .. _Gevent: http://www.gevent.org/ -.. _greenlet: http://greenlet.readthedocs.org/en/latest/ +.. _greenlet: https://greenlet.readthedocs.io/en/latest/ .. _libev: http://software.schmorp.de/pkg/libev.html Twisted Web @@ -97,9 +97,10 @@ localhost at port 8000, setting appropriate headers: proxy_pass http://127.0.0.1:8000/; proxy_redirect off; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } } diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index f51d7460..2791fec3 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -28,6 +28,46 @@ exception to the :attr:`~flask.Flask.logger`. But there is more you can do, and we will cover some better setups to deal with errors. +Error Logging Tools +------------------- + +Sending error mails, even if just for critical ones, can become +overwhelming if enough users are hitting the error and log files are +typically never looked at. This is why we recommend using `Sentry +`_ for dealing with application errors. It's +available as an Open Source project `on GitHub +`__ and is also available as a `hosted version +`_ which you can try for free. Sentry +aggregates duplicate errors, captures the full stack trace and local +variables for debugging, and sends you mails based on new errors or +frequency thresholds. + +To use Sentry you need to install the `raven` client:: + + $ pip install raven + +And then add this to your Flask app:: + + from raven.contrib.flask import Sentry + sentry = Sentry(app, dsn='YOUR_DSN_HERE') + +Or if you are using factories you can also init it later:: + + from raven.contrib.flask import Sentry + sentry = Sentry(dsn='YOUR_DSN_HERE') + + def create_app(): + app = Flask(__name__) + sentry.init_app(app) + ... + return app + +The `YOUR_DSN_HERE` value needs to be replaced with the DSN value you get +from your Sentry installation. + +Afterwards failures are automatically reported to Sentry and from there +you can receive error notifications. + .. _error-handlers: Error handlers @@ -37,7 +77,7 @@ 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 rised while trying to +routes, they are registered for exceptions that are raised while trying to do something else. Registering @@ -49,23 +89,24 @@ Register error handlers using :meth:`~flask.Flask.errorhandler` or @app.errorhandler(werkzeug.exceptions.BadRequest) def handle_bad_request(e): return 'bad request!' - + app.register_error_handler(400, lambda e: '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 interchangable when handed to the registration methods or decorator +are interchangeable when handed to the registration methods or decorator (``BadRequest.code == 400``). -You are however not limited to a :exc:`~werkzeug.exceptions.HTTPException` -or its code but can register a handler for every exception class you like. +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. -.. versionchanged:: 1.0 +.. versionchanged:: 0.11 - Errorhandlers are now prioritized by specifity instead of the order they're - registered in. + Errorhandlers are now prioritized by specificity of the exception classes + they are registered for instead of the order they are registered in. Handling ```````` @@ -74,7 +115,7 @@ 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. -E.g. if a instance of :exc:`ConnectionRefusedError` is raised, and a handler +E.g. 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. @@ -130,7 +171,7 @@ Logging to a File Even if you get mails, you probably also want to log warnings. It's a good idea to keep as much information around that might be required to -debug a problem. By default as of Flask 1.0, errors are logged to your +debug a problem. By default as of Flask 0.11, errors are logged to your webserver's log automatically. Warnings however are not. Please note that Flask itself will not issue any warnings in the core system, so it's your responsibility to warn in the code if something seems odd. @@ -175,7 +216,7 @@ A formatter can be instantiated with a format string. Note that tracebacks are appended to the log entry automatically. You don't have to do that in the log formatter format string. -Here some example setups: +Here are some example setups: Email ````` @@ -235,8 +276,9 @@ that this list is not complete, consult the official documentation of the | ``%(lineno)d`` | Source line number where the logging call was | | | issued (if available). | +------------------+----------------------------------------------------+ -| ``%(asctime)s`` | Human-readable time when the LogRecord` was | -| | created. By default this is of the form | +| ``%(asctime)s`` | Human-readable time when the | +| | :class:`~logging.LogRecord` was created. | +| | By default this is of the form | | | ``"2003-07-08 16:49:45,896"`` (the numbers after | | | the comma are millisecond portion of the time). | | | This can be changed by subclassing the formatter | diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 918187fe..9ae6e6f1 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -29,13 +29,7 @@ be something like "Flask-SimpleXML". Make sure to include the name This is how users can then register dependencies to your extension in their :file:`setup.py` files. -Flask sets up a redirect package called :data:`flask.ext` where users -should import the extensions from. If you for instance have a package -called ``flask_something`` users would import it as -``flask.ext.something``. This is done to transition from the old -namespace packages. See :ref:`ext-import-transition` for more details. - -But how do extensions look like themselves? An extension has to ensure +But what do extensions look like themselves? An extension has to ensure that it works with multiple Flask application instances at once. This is a requirement because many people will use patterns like the :ref:`app-factories` pattern to create their application as needed to aid @@ -360,8 +354,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 @@ -394,27 +387,24 @@ extension to be approved you have to follow these guidelines: Python 2.7 -.. _ext-import-transition: - Extension Import Transition --------------------------- -For a while we recommended using namespace packages for Flask extensions. -This turned out to be problematic in practice because many different -competing namespace package systems exist and pip would automatically -switch between different systems and this caused a lot of problems for -users. +In early versions of Flask we recommended using namespace packages for Flask +extensions, of the form ``flaskext.foo``. This turned out to be problematic in +practice because it meant that multiple ``flaskext`` packages coexist. +Consequently we have recommended to name extensions ``flask_foo`` over +``flaskext.foo`` for a long time. -Instead we now recommend naming packages ``flask_foo`` instead of the now -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 0.8 introduced a redirect import system as a compatibility aid for app +developers: Importing ``flask.ext.foo`` would try ``flask_foo`` and +``flaskext.foo`` in that order. -Flask extensions should urge users to import from ``flask.ext.foo`` -instead of ``flask_foo`` or ``flaskext_foo`` so that extensions can -transition to the new package name without affecting users. +As of Flask 0.11, most Flask extensions have transitioned to the new naming +schema. The ``flask.ext.foo`` compatibility alias is still in Flask 0.11 but is +now deprecated -- you should use ``flask_foo``. -.. _OAuth extension: http://pythonhosted.org/Flask-OAuth/ +.. _OAuth extension: https://pythonhosted.org/Flask-OAuth/ .. _mailinglist: http://flask.pocoo.org/mailinglist/ .. _IRC channel: http://flask.pocoo.org/community/irc/ diff --git a/docs/extensions.rst b/docs/extensions.rst index 748b11b3..6deb9652 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -1,3 +1,5 @@ +.. _extensions: + Flask Extensions ================ @@ -18,10 +20,17 @@ Using Extensions Extensions typically have documentation that goes along that shows how to use it. There are no general rules in how extensions are supposed to behave but they are imported from common locations. If you have an -extension called ``Flask-Foo`` or ``Foo-Flask`` it will be always -importable from ``flask.ext.foo``:: +extension called ``Flask-Foo`` or ``Foo-Flask`` it should be always +importable from ``flask_foo``:: - from flask.ext import foo + import flask_foo + +Building Extensions +------------------- + +While `Flask Extension Registry`_ contains many Flask extensions, you may not find +an extension that fits your need. If this is the case, you can always create your own. +Consider reading :ref:`extension-dev` to develop your own Flask extension. Flask Before 0.8 ---------------- @@ -44,5 +53,6 @@ And here is how you can use it:: Once the ``flaskext_compat`` module is activated the :data:`flask.ext` will exist and you can start importing from there. + .. _Flask Extension Registry: http://flask.pocoo.org/extensions/ -.. _flaskext_compat.py: https://raw.githubusercontent.com/mitsuhiko/flask/master/scripts/flaskext_compat.py +.. _flaskext_compat.py: https://raw.githubusercontent.com/pallets/flask/master/scripts/flaskext_compat.py diff --git a/docs/htmlfaq.rst b/docs/htmlfaq.rst index cba99fa1..0b6ff504 100644 --- a/docs/htmlfaq.rst +++ b/docs/htmlfaq.rst @@ -30,7 +30,7 @@ the (X)HTML generation on the web is based on non-XML template engines (such as Jinja, the one used in Flask) which do not protect you from accidentally creating invalid XHTML. There are XML based template engines, such as Kid and the popular Genshi, but they often come with a larger -runtime overhead and, are not as straightforward to use because they have +runtime overhead and are not as straightforward to use because they have to obey XML rules. The majority of users, however, assumed they were properly using XHTML. diff --git a/docs/installation.rst b/docs/installation.rst index 4f1ceaaf..96c363f5 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -40,24 +40,20 @@ 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. -If you are on Mac OS X or Linux, chances are that one of the following two -commands will work for you:: - - $ sudo easy_install virtualenv - -or even better:: +If you are on Mac OS X or Linux, chances are that the following +command will work for you:: $ sudo pip install virtualenv -One of these will probably install virtualenv on your system. Maybe it's even +It will probably install virtualenv on your system. Maybe it's even in your package manager. If you use Ubuntu, try:: $ sudo apt-get install python-virtualenv -If you are on Windows and don't have the :command:`easy_install` command, you must +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 :command:`sudo` prefix. +commands as above, but without the ``sudo`` prefix. 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` @@ -76,7 +72,7 @@ corresponding environment. On OS X and Linux, do the following:: If you are a Windows user, the following command is for you:: - $ venv\scripts\activate + $ venv\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). @@ -99,24 +95,24 @@ System-Wide Installation ------------------------ This is possible as well, though I do not recommend it. Just run -:command:`pip` with root privileges:: +``pip`` with root privileges:: $ sudo pip install Flask (On Windows systems, run it in a command-prompt window with administrator -privileges, and leave out :command:`sudo`.) +privileges, and leave out ``sudo``.) Living on the Edge ------------------ If you want to work with the latest version of Flask, there are two ways: you -can either let :command:`pip` pull in the development version, or you can tell +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. Get the git checkout in a new virtualenv and run in development mode:: - $ git clone http://github.com/mitsuhiko/flask.git + $ git clone https://github.com/pallets/flask.git Initialized empty Git repository in ~/dev/flask/.git/ $ cd flask $ virtualenv venv @@ -131,40 +127,34 @@ 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. - .. _windows-easy-install: `pip` and `setuptools` on Windows --------------------------------- -Sometimes getting the standard "Python packaging tools" like *pip*, *setuptools* -and *virtualenv* can be a little trickier, but nothing very hard. The two crucial -packages you will need are setuptools and pip - these will let you install -anything else (like virtualenv). Fortunately there are two "bootstrap scripts" -you can run to install either. +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. -If you don't currently have either, then `get-pip.py` will install both for you -(you won't need to run ez_setup.py). +If you don't currently have ``pip``, then `get-pip.py` will install it for you. `get-pip.py`_ -To install the latest setuptools, you can use its bootstrap file: - -`ez_setup.py`_ - -Either should be double-clickable once you download them. If you already have pip, +It should be double-clickable once you download it. If you already have ``pip``, you can upgrade them by running:: > pip install --upgrade pip setuptools -Most often, once you pull up a command prompt you want to be able to type :command:`pip` -and :command:`python` which will run those things, but this might not automatically happen +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!). 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 +(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 :command:`python` to bring up the interpreter. +check that you can now just type ``python`` to bring up the interpreter. Finally, to install `virtualenv`_, you can simply run:: @@ -172,5 +162,4 @@ Finally, to install `virtualenv`_, you can simply run:: Then you can be off on your way following the installation instructions above. -.. _get-pip.py: https://raw.githubusercontent.com/pypa/pip/master/contrib/get-pip.py -.. _ez_setup.py: https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py +.. _get-pip.py: https://bootstrap.pypa.io/get-pip.py diff --git a/docs/patterns/apierrors.rst b/docs/patterns/apierrors.rst index ce5c8446..90e8c13d 100644 --- a/docs/patterns/apierrors.rst +++ b/docs/patterns/apierrors.rst @@ -2,7 +2,7 @@ Implementing API Exceptions =========================== It's very common to implement RESTful APIs on top of Flask. One of the -first thing that developers run into is the realization that the builtin +first things that developers run into is the realization that the builtin exceptions are not expressive enough for APIs and that the content type of :mimetype:`text/html` they are emitting is not very useful for API consumers. diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 3ff99e09..726850e2 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -4,11 +4,11 @@ Application Dispatching ======================= Application dispatching is the process of combining multiple Flask -applications on the WSGI level. You can not only combine Flask -applications into something larger but any WSGI application. This would -even allow you to run a Django and a Flask application in the same -interpreter side by side if you want. The usefulness of this depends on -how the applications work internally. +applications on the WSGI level. You can combine not only Flask +applications but any WSGI application. This would allow you to run a +Django and a Flask application in the same interpreter side by side if +you want. The usefulness of this depends on how the applications work +internally. The fundamental difference from the :ref:`module approach ` is that in this case you are running the same or @@ -31,7 +31,7 @@ Note that :func:`run_simple ` is not intended for use in production. Use a :ref:`full-blown WSGI server `. In order to use the interactive debugger, debugging must be enabled both on -the application and the simple server, here is the "hello world" example with +the application and the simple server. Here is the "hello world" example with debugging and :func:`run_simple `:: from flask import Flask @@ -56,7 +56,7 @@ If you have entirely separated applications and you want them to work next to each other in the same Python interpreter process you can take advantage of the :class:`werkzeug.wsgi.DispatcherMiddleware`. The idea here is that each Flask application is a valid WSGI application and they -are combined by the dispatcher middleware into a larger one that +are combined by the dispatcher middleware into a larger one that is dispatched based on prefix. For example you could have your main application run on ``/`` and your diff --git a/docs/patterns/appfactories.rst b/docs/patterns/appfactories.rst index bcac210c..fdbde504 100644 --- a/docs/patterns/appfactories.rst +++ b/docs/patterns/appfactories.rst @@ -6,7 +6,7 @@ Application Factories If you are already using packages and blueprints for your application (:ref:`blueprints`) there are a couple of really nice ways to further improve the experience. A common pattern is creating the application object when -the blueprint is imported. But if you move the creation of this object, +the blueprint is imported. But if you move the creation of this object into a function, you can then create multiple instances of this app later. So why would you want to do this? @@ -60,7 +60,7 @@ Factories & Extensions It's preferable to create your extensions and app factories so that the extension object does not initially get bound to the application. -Using `Flask-SQLAlchemy `_, +Using `Flask-SQLAlchemy `_, as an example, you should not do something along those lines:: def create_app(config_filename): @@ -99,7 +99,8 @@ an application:: It can then be used with the :command:`flask` command:: - flask --app=exampleapp run + export FLASK_APP=exampleapp + flask run Factory Improvements -------------------- diff --git a/docs/patterns/celery.rst b/docs/patterns/celery.rst index 52155f62..673d953b 100644 --- a/docs/patterns/celery.rst +++ b/docs/patterns/celery.rst @@ -36,7 +36,7 @@ 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_BACKEND'], + 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 diff --git a/docs/patterns/deferredcallbacks.rst b/docs/patterns/deferredcallbacks.rst index 83d4fb49..886ae40a 100644 --- a/docs/patterns/deferredcallbacks.rst +++ b/docs/patterns/deferredcallbacks.rst @@ -56,9 +56,9 @@ this the following function needs to be registered as A Practical Example ------------------- -Now we can easily at any point in time register a function to be called at -the end of this particular request. For example you can remember the -current language of the user in a cookie in the before-request function:: +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 diff --git a/docs/patterns/distribute.rst b/docs/patterns/distribute.rst index 997ca89b..f4a07579 100644 --- a/docs/patterns/distribute.rst +++ b/docs/patterns/distribute.rst @@ -1,13 +1,12 @@ .. _distribute-deployment: -Deploying with Distribute +Deploying with Setuptools ========================= -`distribute`_, formerly setuptools, is an extension library that is -commonly used to (like the name says) distribute Python libraries and -extensions. It extends distutils, a basic module installation system -shipped with Python to also support various more complex constructs that -make larger applications easier to distribute: +`Setuptools`_, is an extension library that is commonly used to +distribute Python libraries and extensions. It extends distutils, a basic +module installation system shipped with Python to also support various more +complex constructs that make larger applications easier to distribute: - **support for dependencies**: a library or application can declare a list of other libraries it depends on which will be installed @@ -16,34 +15,32 @@ make larger applications easier to distribute: Python installation. This makes it possible to query information provided by one package from another package. The best known feature of this system is the entry point support which allows one package to - declare an "entry point" another package can hook into to extend the + declare an "entry point" that another package can hook into to extend the other package. -- **installation manager**: :command:`easy_install`, which comes with distribute - can install other libraries for you. You can also use `pip`_ which - sooner or later will replace :command:`easy_install` which does more than just - installing packages for you. +- **installation manager**: :command:`pip` can install other libraries for you. -Flask itself, and all the libraries you can find on the cheeseshop -are distributed with either distribute, the older setuptools or distutils. +If you have Python 2 (>=2.7.9) or Python 3 (>=3.4) installed from python.org, +you will already have pip and setuptools on your system. Otherwise, you +will need to install them yourself. + +Flask itself, and all the libraries you can find on PyPI are distributed with +either setuptools or distutils. In this case we assume your application is called :file:`yourapplication.py` and you are not using a module, but a :ref:`package -`. Distributing resources with standard modules is -not supported by `distribute`_ so we will not bother with it. If you have -not yet converted your application into a package, head over to the -:ref:`larger-applications` pattern to see how this can be done. +`. If you have not yet converted your application into +a package, head over to the :ref:`larger-applications` pattern to see +how this can be done. -A working deployment with distribute is the first step into more complex +A working deployment with setuptools is the first step into more complex and more automated deployment scenarios. If you want to fully automate the process, also read the :ref:`fabric-deployment` chapter. Basic Setup Script ------------------ -Because you have Flask running, you either have setuptools or distribute -available on your system anyways. If you do not, fear not, there is a -script to install it for you: `distribute_setup.py`_. Just download and -run with your Python interpreter. +Because you have Flask installed, you have setuptools available on your system. +Flask already depends upon setuptools. Standard disclaimer applies: :ref:`you better use a virtualenv `. @@ -52,10 +49,6 @@ Your setup code always goes into a file named :file:`setup.py` next to your application. The name of the file is only convention, but because everybody will look for a file with that name, you better not change it. -Yes, even if you are using `distribute`, you are importing from a package -called `setuptools`. `distribute` is fully backwards compatible with -`setuptools`, so it also uses the same import name. - A basic :file:`setup.py` file for a Flask application looks like this:: from setuptools import setup @@ -71,8 +64,8 @@ A basic :file:`setup.py` file for a Flask application looks like this:: ) Please keep in mind that you have to list subpackages explicitly. If you -want distribute to lookup the packages for you automatically, you can use -the `find_packages` function:: +want setuptools to lookup the packages for you automatically, you can use +the ``find_packages`` function:: from setuptools import setup, find_packages @@ -81,17 +74,36 @@ the `find_packages` function:: packages=find_packages() ) -Most parameters to the `setup` function should be self explanatory, -`include_package_data` and `zip_safe` might not be. -`include_package_data` tells distribute to look for a :file:`MANIFEST.in` file +Most parameters to the ``setup`` function should be self explanatory, +``include_package_data`` and ``zip_safe`` might not be. +``include_package_data`` tells setuptools to look for a :file:`MANIFEST.in` file and install all the entries that match as package data. We will use this to distribute the static files and templates along with the Python module -(see :ref:`distributing-resources`). The `zip_safe` flag can be used to +(see :ref:`distributing-resources`). The ``zip_safe`` flag can be used to force or prevent zip Archive creation. In general you probably don't want your packages to be installed as zip files because some tools do not support them and they make debugging a lot harder. +Tagging Builds +-------------- + +It is useful to distinguish between release and development builds. Add a +:file:`setup.cfg` file to configure these options. + + [egg_info] + tag_build = .dev + tag_date = 1 + + [aliases] + release = egg_info -RDb '' + +Running ``python setup.py sdist`` will create a development package +with ".dev" and the current date appended: ``flaskr-1.0.dev20160314.tar.gz``. +Running ``python setup.py release sdist`` will create a release package +with only the version: ``flaskr-1.0.tar.gz``. + + .. _distributing-resources: Distributing Resources @@ -99,7 +111,7 @@ Distributing Resources If you try to install the package you just created, you will notice that folders like :file:`static` or :file:`templates` are not installed for you. The -reason for this is that distribute does not know which files to add for +reason for this is that setuptools does not know which files to add for you. What you should do, is to create a :file:`MANIFEST.in` file next to your :file:`setup.py` file. This file lists all the files that should be added to your tarball:: @@ -109,13 +121,13 @@ your tarball:: Don't forget that even if you enlist them in your :file:`MANIFEST.in` file, they won't be installed for you unless you set the `include_package_data` -parameter of the `setup` function to ``True``! +parameter of the ``setup`` function to ``True``! Declaring Dependencies ---------------------- -Dependencies are declared in the `install_requires` parameter as list. +Dependencies are declared in the ``install_requires`` parameter as a list. Each item in that list is the name of a package that should be pulled from PyPI on installation. By default it will always use the most recent version, but you can also provide minimum and maximum version @@ -127,40 +139,39 @@ requirements. Here some examples:: 'BrokenPackage>=0.7,<=1.0' ] -I mentioned earlier that dependencies are pulled from PyPI. What if you +As mentioned earlier, dependencies are pulled from PyPI. What if you want to depend on a package that cannot be found on PyPI and won't be because it is an internal package you don't want to share with anyone? -Just still do as if there was a PyPI entry for it and provide a list of -alternative locations where distribute should look for tarballs:: +Just do it as if there was a PyPI entry and provide a list of +alternative locations where setuptools should look for tarballs:: dependency_links=['http://example.com/yourfiles'] Make sure that page has a directory listing and the links on the page are pointing to the actual tarballs with their correct filenames as this is -how distribute will find the files. If you have an internal company -server that contains the packages, provide the URL to that server there. +how setuptools will find the files. If you have an internal company +server that contains the packages, provide the URL to that server. Installing / Developing ----------------------- To install your application (ideally into a virtualenv) just run the -:file:`setup.py` script with the `install` parameter. It will install your +:file:`setup.py` script with the ``install`` parameter. It will install your application into the virtualenv's site-packages folder and also download and install all dependencies:: $ python setup.py install If you are developing on the package and also want the requirements to be -installed, you can use the `develop` command instead:: +installed, you can use the ``develop`` command instead:: $ python setup.py develop This has the advantage of just installing a link to the site-packages folder instead of copying the data over. You can then continue to work on -the code without having to run `install` again after each change. +the code without having to run ``install`` again after each change. -.. _distribute: https://pypi.python.org/pypi/distribute .. _pip: https://pypi.python.org/pypi/pip -.. _distribute_setup.py: http://python-distribute.org/distribute_setup.py +.. _Setuptools: https://pypi.python.org/pypi/setuptools diff --git a/docs/patterns/fabric.rst b/docs/patterns/fabric.rst index 7270a569..3dbf2146 100644 --- a/docs/patterns/fabric.rst +++ b/docs/patterns/fabric.rst @@ -43,36 +43,25 @@ virtual environment:: env.hosts = ['server1.example.com', 'server2.example.com'] def pack(): - # create a new source distribution as tarball + # build the package local('python setup.py sdist --formats=gztar', capture=False) def deploy(): - # figure out the release name and version + # figure out the package name and version dist = local('python setup.py --fullname', capture=True).strip() - # upload the source tarball to the temporary folder on the server - put('dist/%s.tar.gz' % dist, '/tmp/yourapplication.tar.gz') - # create a place where we can unzip the tarball, then enter - # that directory and unzip it - run('mkdir /tmp/yourapplication') - with cd('/tmp/yourapplication'): - run('tar xzf /tmp/yourapplication.tar.gz') - # now setup the package with our virtual environment's - # python interpreter - run('/var/www/yourapplication/env/bin/python setup.py install') - # now that all is set up, delete the folder again - run('rm -rf /tmp/yourapplication /tmp/yourapplication.tar.gz') - # and finally touch the .wsgi file so that mod_wsgi triggers - # a reload of the application - run('touch /var/www/yourapplication.wsgi') + filename = '%s.tar.gz' % dist + + # upload the package to the temporary folder on the server + put('dist/%s' % filename, '/tmp/%s' % filename) -The example above is well documented and should be straightforward. Here -a recap of the most common commands fabric provides: + # install the package in the application's virtualenv with pip + run('/var/www/yourapplication/env/bin/pip install /tmp/%s' % filename) -- `run` - executes a command on a remote server -- `local` - executes a command on the local machine -- `put` - uploads a file to the remote server -- `cd` - changes the directory on the serverside. This has to be used - in combination with the ``with`` statement. + # remove the uploaded package + run('rm -r /tmp/%s' % filename) + + # touch the .wsgi file to trigger a reload in mod_wsgi + run('touch /var/www/yourapplication.wsgi') Running Fabfiles ---------------- @@ -156,6 +145,7 @@ location where it's expected (eg: :file:`/var/www/yourapplication`). Either way, in our case here we only expect one or two servers and we can upload them ahead of time by hand. + First Deployment ---------------- diff --git a/docs/patterns/favicon.rst b/docs/patterns/favicon.rst index acdee24b..21ea767f 100644 --- a/docs/patterns/favicon.rst +++ b/docs/patterns/favicon.rst @@ -49,5 +49,5 @@ web server's documentation. See also -------- -* The `Favicon `_ article on +* The `Favicon `_ article on Wikipedia diff --git a/docs/patterns/fileuploads.rst b/docs/patterns/fileuploads.rst index 10aa15f2..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' @@ -40,14 +40,14 @@ your users to be able to upload everything there if the server is directly sending out the data to the client. That way you can make sure that users are not able to upload HTML files that would cause XSS problems (see :ref:`xss`). Also make sure to disallow ``.php`` files if the server -executes them, but who has PHP installed on his server, right? :) +executes them, but who has PHP installed on their server, right? :) Next the functions that check if an extension is valid and that uploads the file and redirects the user to the URL for the uploaded file:: def allowed_file(filename): return '.' in filename and \ - filename.rsplit('.', 1)[1] in ALLOWED_EXTENSIONS + filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS @app.route('/', methods=['GET', 'POST']) def upload_file(): @@ -58,7 +58,7 @@ the file and redirects the user to the URL for the uploaded file:: return redirect(request.url) file = request.files['file'] # if user does not select file, browser also - # submit a empty part without filename + # submit an empty part without filename if file.filename == '': flash('No selected file') return redirect(request.url) @@ -71,9 +71,9 @@ the file and redirects the user to the URL for the uploaded file:: Upload new File

Upload new File

-
-

- + + +

''' @@ -104,9 +104,9 @@ before storing it directly on the filesystem. >>> secure_filename('../../../../home/username/.bashrc') 'home_username_.bashrc' -Now one last thing is missing: the serving of the uploaded files. In the -:func:`upload_file()` we redirect the user to -``url_for('uploaded_file', filename=filename)``, that is, ``/uploads/filename``. +Now one last thing is missing: the serving of the uploaded files. In the +:func:`upload_file()` we redirect the user to +``url_for('uploaded_file', filename=filename)``, that is, ``/uploads/filename``. So we write the :func:`uploaded_file` function to return the file of that name. As of Flask 0.5 we can use a function that does that for us:: @@ -181,4 +181,4 @@ applications dealing with uploads, there is also a Flask extension called blacklisting of extensions and more. .. _jQuery: https://jquery.com/ -.. _Flask-Uploads: http://pythonhosted.org/Flask-Uploads/ +.. _Flask-Uploads: https://pythonhosted.org/Flask-Uploads/ diff --git a/docs/patterns/flashing.rst b/docs/patterns/flashing.rst index b2de07ce..7efd1446 100644 --- a/docs/patterns/flashing.rst +++ b/docs/patterns/flashing.rst @@ -9,7 +9,9 @@ application. Flask provides a really simple way to give feedback to a user with the flashing system. The flashing system basically makes it possible to record a message at the end of a request and access it next request and only next request. This is usually combined with a layout -template that does this. +template that does this. Note that browsers and sometimes web servers enforce +a limit on cookie sizes. This means that flashing messages that are too +large for session cookies causes message flashing to fail silently. Simple Flashing --------------- @@ -76,7 +78,7 @@ And here is the :file:`login.html` template which also inherits from {% if error %}

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

+
Username:
`_. -.. _Flask-SQLAlchemy: http://pythonhosted.org/Flask-SQLAlchemy/ +.. _Flask-SQLAlchemy: http://flask-sqlalchemy.pocoo.org/ Declarative @@ -108,9 +108,9 @@ Querying is simple as well: >>> User.query.filter(User.name == 'admin').first() -.. _SQLAlchemy: http://www.sqlalchemy.org/ +.. _SQLAlchemy: https://www.sqlalchemy.org/ .. _declarative: - http://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/ + https://docs.sqlalchemy.org/en/latest/orm/extensions/declarative/ Manual Object Relational Mapping -------------------------------- @@ -135,7 +135,7 @@ Here is an example :file:`database.py` module for your application:: def init_db(): metadata.create_all(bind=engine) -As for the declarative approach you need to close the session after +As in the declarative approach, you need to close the session after each request or application context shutdown. Put this into your application module:: @@ -215,4 +215,4 @@ You can also pass strings of SQL statements to the (1, u'admin', u'admin@localhost') For more information about SQLAlchemy, head over to the -`website `_. +`website `_. diff --git a/docs/patterns/sqlite3.rst b/docs/patterns/sqlite3.rst index 1f9e3671..15f38ea7 100644 --- a/docs/patterns/sqlite3.rst +++ b/docs/patterns/sqlite3.rst @@ -3,8 +3,8 @@ Using SQLite 3 with Flask ========================= -In Flask you can easily implement the opening of database connections on -demand and closing them when the context dies (usually at the end of the +In Flask you can easily implement the opening of database connections on +demand and closing them when the context dies (usually at the end of the request). Here is a simple example of how you can use SQLite 3 with Flask:: @@ -71,7 +71,8 @@ Now in each request handling function you can access `g.db` to get the current open database connection. To simplify working with SQLite, a row factory function is useful. It is executed for every result returned from the database to convert the result. For instance, in order to get -dictionaries instead of tuples, this could be inserted into ``get_db``:: +dictionaries instead of tuples, this could be inserted into the ``get_db`` +function we created above:: def make_dicts(cursor, row): return dict((cursor.description[idx][0], value) @@ -79,21 +80,37 @@ dictionaries instead of tuples, this could be inserted into ``get_db``:: db.row_factory = make_dicts -Or even simpler:: +This will make the sqlite3 module return dicts for this database connection, which are much nicer to deal with. Even more simply, we could place this in ``get_db`` instead:: db.row_factory = sqlite3.Row +This would use Row objects rather than dicts to return the results of queries. These are ``namedtuple`` s, so we can access them either by index or by key. For example, assuming we have a ``sqlite3.Row`` called ``r`` for the rows ``id``, ``FirstName``, ``LastName``, and ``MiddleInitial``:: + + >>> # You can get values based on the row's name + >>> r['FirstName'] + John + >>> # Or, you can get them based on index + >>> r[1] + John + # Row objects are also iterable: + >>> for value in r: + ... print(value) + 1 + John + Doe + M + Additionally, it is a good idea to provide a query function that combines getting the cursor, executing and fetching the results:: - + def query_db(query, args=(), one=False): cur = get_db().execute(query, args) rv = cur.fetchall() cur.close() return (rv[0] if rv else None) if one else rv -This handy little function, in combination with a row factory, makes -working with the database much more pleasant than it is by just using the +This handy little function, in combination with a row factory, makes +working with the database much more pleasant than it is by just using the raw cursor and connection objects. Here is how you can use it:: @@ -114,7 +131,7 @@ To pass variable parts to the SQL statement, use a question mark in the statement and pass in the arguments as a list. Never directly add them to the SQL statement with string formatting because this makes it possible to attack the application using `SQL Injections -`_. +`_. Initial Schemas --------------- diff --git a/docs/patterns/subclassing.rst b/docs/patterns/subclassing.rst new file mode 100644 index 00000000..d8de2335 --- /dev/null +++ b/docs/patterns/subclassing.rst @@ -0,0 +1,17 @@ +Subclassing Flask +================= + +The :class:`~flask.Flask` class is designed for subclassing. + +For example, you may want to override how request parameters are handled to preserve their order:: + + from flask import Flask, Request + from werkzeug.datastructures import ImmutableOrderedMultiDict + class MyRequest(Request): + """Request subclass to override request parameter storage""" + parameter_storage_class = ImmutableOrderedMultiDict + class MyFlask(Flask): + """Flask subclass using the custom request class""" + request_class = MyRequest + +This is the recommended approach for overriding or augmenting Flask's internal functionality. diff --git a/docs/patterns/viewdecorators.rst b/docs/patterns/viewdecorators.rst index 97e6871d..7fd97dca 100644 --- a/docs/patterns/viewdecorators.rst +++ b/docs/patterns/viewdecorators.rst @@ -16,15 +16,13 @@ Login Required Decorator ------------------------ So let's implement such a decorator. A decorator is a function that -returns a function. Pretty simple actually. The only thing you have to -keep in mind when implementing something like this is to update the -`__name__`, `__module__` and some other attributes of a function. This is -often forgotten, but you don't have to do that by hand, there is a -function for that that is used like a decorator (:func:`functools.wraps`). +wraps and replaces another function. Since the original function is +replaced, you need to remember to copy the original function's information +to the new function. Use :func:`functools.wraps` to handle this for you. This example assumes that the login page is called ``'login'`` and that -the current user is stored as `g.user` and ``None`` if there is no-one -logged in:: +the current user is stored in ``g.user`` and is ``None`` if there is no-one +logged in. :: from functools import wraps from flask import g, request, redirect, url_for @@ -37,15 +35,24 @@ logged in:: return f(*args, **kwargs) return decorated_function -So how would you use that decorator now? Apply it as innermost decorator -to a view function. When applying further decorators, always remember -that the :meth:`~flask.Flask.route` decorator is the outermost:: +To use the decorator, apply it as innermost decorator to a view function. +When applying further decorators, always remember +that the :meth:`~flask.Flask.route` decorator is the outermost. :: @app.route('/secret_page') @login_required def secret_page(): pass +.. note:: + The ``next`` value will exist in ``request.args`` after a ``GET`` request for + the login page. You'll have to pass it along when sending the ``POST`` request + from the login form. You can do this with a hidden input tag, then retrieve it + from ``request.form`` when logging the user in. :: + + + + Caching Decorator ----------------- diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 88602b6c..0e53de17 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -19,7 +19,7 @@ forms. fun. You can get it from `PyPI `_. -.. _Flask-WTF: http://pythonhosted.org/Flask-WTF/ +.. _Flask-WTF: https://flask-wtf.readthedocs.io/en/stable/ The Forms --------- @@ -32,11 +32,11 @@ This is an example form for a typical registration page:: username = StringField('Username', [validators.Length(min=4, max=25)]) email = StringField('Email Address', [validators.Length(min=6, max=35)]) password = PasswordField('New Password', [ - validators.Required(), + validators.DataRequired(), validators.EqualTo('confirm', message='Passwords must match') ]) confirm = PasswordField('Repeat Password') - accept_tos = BooleanField('I accept the TOS', [validators.Required()]) + accept_tos = BooleanField('I accept the TOS', [validators.DataRequired()]) In the View ----------- @@ -108,7 +108,7 @@ takes advantage of the :file:`_formhelpers.html` template: .. sourcecode:: html+jinja {% from "_formhelpers.html" import render_field %} - +
{{ render_field(form.username) }} {{ render_field(form.email) }} @@ -122,5 +122,5 @@ takes advantage of the :file:`_formhelpers.html` template: For more information about WTForms, head over to the `WTForms website`_. -.. _WTForms: http://wtforms.readthedocs.org/ -.. _WTForms website: http://wtforms.readthedocs.org/ +.. _WTForms: https://wtforms.readthedocs.io/ +.. _WTForms website: https://wtforms.readthedocs.io/ diff --git a/docs/python3.rst b/docs/python3.rst index 4d488f16..a7a4f165 100644 --- a/docs/python3.rst +++ b/docs/python3.rst @@ -3,32 +3,21 @@ Python 3 Support ================ -Flask and all of its dependencies support Python 3 so you can in theory -start working on it already. There are however a few things you should be -aware of before you start using Python 3 for your next project. +Flask, its dependencies, and most Flask extensions support Python 3. +You should start using Python 3 for your next project, +but there are a few things to be aware of. -If you want to use Flask with Python 3 you will need to use Python 3.3 or -higher. 3.2 and older are *not* supported. +You need to use Python 3.3 or higher. 3.2 and older are *not* supported. -In addition to that you need to use the latest and greatest versions of -`itsdangerous`, `Jinja2` and `Werkzeug`. Flask 0.10 and Werkzeug 0.9 were -the first versions to introduce Python 3 support. +You should use the latest versions of all Flask-related packages. +Flask 0.10 and Werkzeug 0.9 were the first versions to introduce Python 3 support. -Some of the decisions made in regards to unicode and byte utilization on -Python 3 make it hard to write low level code. This mainly affects WSGI -middlewares and interacting with the WSGI provided information. Werkzeug -wraps all that information in high-level helpers but some of those were -specifically added for the Python 3 support and are quite new. +Python 3 changed how unicode and bytes are handled, which complicates how low +level code handles HTTP data. This mainly affects WSGI middleware interacting +with the WSGI ``environ`` data. Werkzeug wraps that information in high-level +helpers, so encoding issues should not affect you. -Unless you require absolute compatibility, you should be fine with Python 3 -nowadays. Most libraries and Flask extensions have been ported by now and -using Flask with Python 3 is generally a smooth ride. However, keep in mind -that most libraries (including Werkzeug and Flask) might not quite as stable -on Python 3 yet. You might therefore sometimes run into bugs that are -usually encoding-related. - -The majority of the upgrade pain is in the lower-level libraries like -Flask and Werkzeug and not in the actual high-level application code. For -instance all of the Flask examples that are in the Flask repository work -out of the box on both 2.x and 3.x and did not require a single line of -code changed. +The majority of the upgrade work is in the lower-level libraries like +Flask and Werkzeug, not the high-level application code. +For example, all of the examples in the Flask repository work on both Python 2 and 3 +and did not require a single line of code changed. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 38c14035..09365496 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -26,7 +26,7 @@ So what did that code do? class will be our WSGI application. 2. Next we create an instance of this class. The first argument is the name of the application's module or package. If you are using a single module (as - in this example), you should use `__name__` because depending on if it's + in this example), you should use ``__name__`` because depending on if it's started as application or imported as module the name will be different (``'__main__'`` versus the actual import name). This is needed so that Flask knows where to look for templates, static files, and so on. For more @@ -37,19 +37,25 @@ So what did that code do? particular function, and returns the message we want to display in the user's browser. -Just save it as :file:`hello.py` (or something similar) and run it with your Python -interpreter. Make sure to not call your application :file:`flask.py` because this -would conflict with Flask itself. +Just save it as :file:`hello.py` or something similar. Make sure to not call +your application :file:`flask.py` because this would conflict with Flask +itself. To run the application you can either use the :command:`flask` command or -python's :option:`-m` switch with Flask:: +python's ``-m`` switch with Flask. Before you can do that you need +to tell your terminal the application to work with by exporting the +``FLASK_APP`` environment variable:: - $ flask -a hello run + $ export FLASK_APP=hello.py + $ flask run * Running on http://127.0.0.1:5000/ -or alternatively:: +If you are on Windows you need to use ``set`` instead of ``export``. - $ python -m flask -a hello run +Alternatively you can use :command:`python -m flask`:: + + $ export FLASK_APP=hello.py + $ python -m flask run * Running on http://127.0.0.1:5000/ This launches a very simple builtin server, which is good enough for testing @@ -72,7 +78,7 @@ should see your hello world greeting. you can make the server publicly available simply by adding ``--host=0.0.0.0`` to the command line:: - flask -a hello run --host=0.0.0.0 + flask run --host=0.0.0.0 This tells your operating system to listen on all public IPs. @@ -80,35 +86,26 @@ should see your hello world greeting. What to do if the Server does not Start --------------------------------------- -In case the ``python -m flask`` fails or :command:`flask` does not exist, +In case the :command:`python -m flask` fails or :command:`flask` does not exist, there are multiple reasons this might be the case. First of all you need to look at the error message. Old Version of Flask ```````````````````` -Versions of Flask older than 1.0 use to have different ways to start the +Versions of Flask older than 0.11 use to have different ways to start the application. In short, the :command:`flask` command did not exist, and -neither did ``python -m flask``. In that case you have two options: +neither did :command:`python -m flask`. In that case you have two options: either upgrade to newer Flask versions or have a look at the :ref:`server` docs to see the alternative method for running a server. -Python older 2.7 -```````````````` - -In case you have a version of Python older than 2.7 ``python -m flask`` -does not work. You can either use :command:`flask` or ``python -m -flask.cli`` as an alternative. This is because Python before 2.7 does no -permit packages to act as executable modules. For more information see -:ref:`cli`. - Invalid Import Name ``````````````````` -The :option:`-a` argument to :command:`flask` is the name of the module to import. In -case that module is incorrectly named you will get an import error upon -start (or if debug is enabled when you navigate to the application). It -will tell you what it tried to import and why it failed. +The ``FLASK_APP`` environment variable is the name of the module to import at +:command:`flask run`. In case that module is incorrectly named you will get an +import error upon start (or if debug is enabled when you navigate to the +application). It will tell you what it tried to import and why it failed. The most common reason is a typo or because you did not actually create an ``app`` object. @@ -126,10 +123,13 @@ That is not very nice and Flask can do better. If you enable debug support the server will reload itself on code changes, and it will also provide you with a helpful debugger if things go wrong. -There are different ways to enable the debug mode. The most obvious one -is the :option:`--debug` parameter to the :command:`flask` command:: +To enable debug mode you can export the ``FLASK_DEBUG`` environment variable +before running the server:: - flask --debug -a hello run + $ export FLASK_DEBUG=1 + $ flask run + +(On Windows you need to use ``set`` instead of ``export``). This does the following things: @@ -202,8 +202,10 @@ The following converters exist: =========== =============================================== `string` accepts any text without a slash (the default) `int` accepts integers -`float` like `int` but for floating point values +`float` like ``int`` but for floating point values `path` like the default but also accepts slashes +`any` matches one of the items provided +`uuid` accepts UUID strings =========== =============================================== .. admonition:: Unique URLs / Redirection Behavior @@ -224,7 +226,7 @@ The following converters exist: 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 + ``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. @@ -248,29 +250,29 @@ 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: - ->>> 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') -... -/ -/login -/login?next=/ -/user/John%20Doe +some examples:: + + >>> 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')) + ... + / + /login + /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 @@ -286,8 +288,8 @@ There are three good reasons for this: 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 +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. @@ -296,7 +298,7 @@ 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 +can be changed by providing the ``methods`` argument to the :meth:`~flask.Flask.route` decorator. Here are some examples:: from flask import request @@ -304,9 +306,9 @@ can be changed by providing the `methods` argument to the @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': - do_the_login() + return do_the_login() else: - show_the_login_form() + 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 @@ -319,7 +321,7 @@ 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 -clients wants to *do* with the requested page. The following methods are +client wants to *do* with the requested page. The following methods are very common: ``GET`` @@ -365,7 +367,7 @@ 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. -.. _HTTP RFC: http://www.ietf.org/rfc/rfc2068.txt +.. _HTTP RFC: https://www.ietf.org/rfc/rfc2068.txt Static Files ------------ @@ -444,22 +446,22 @@ know how that works, head over to the :ref:`template-inheritance` pattern documentation. Basically template inheritance makes it possible to keep certain elements on each page (like header, navigation and footer). -Automatic escaping is enabled, so if `name` contains HTML it will be escaped +Automatic escaping is enabled, so if ``name`` contains HTML it will be escaped automatically. If you can trust a variable and you know that it will be safe HTML (for example because it came from a module that converts wiki markup to HTML) you can mark it as safe by using the :class:`~jinja2.Markup` class or by using the ``|safe`` filter in the template. Head over to the Jinja 2 documentation for more examples. -Here is a basic introduction to how the :class:`~jinja2.Markup` class works: +Here is a basic introduction to how the :class:`~jinja2.Markup` class works:: ->>> from flask import Markup ->>> Markup('Hello %s!') % 'hacker' -Markup(u'Hello <blink>hacker</blink>!') ->>> Markup.escape('hacker') -Markup(u'<blink>hacker</blink>') ->>> Markup('Marked up » HTML').striptags() -u'Marked up \xbb HTML' + >>> from flask import Markup + >>> Markup('Hello %s!') % 'hacker' + Markup(u'Hello <blink>hacker</blink>!') + >>> Markup.escape('hacker') + Markup(u'<blink>hacker</blink>') + >>> Markup('Marked up » HTML').striptags() + u'Marked up \xbb HTML' .. versionchanged:: 0.5 @@ -536,16 +538,16 @@ The Request Object `````````````````` The request object is documented in the API section and we will not cover -it here in detail (see :class:`~flask.request`). Here is a broad overview of +it here in detail (see :class:`~flask.Request`). Here is a broad overview of some of the most common operations. First of all you have to import it from -the `flask` module:: +the ``flask`` module:: from flask import request The current request method is available by using the -:attr:`~flask.request.method` attribute. To access form data (data +:attr:`~flask.Request.method` attribute. To access form data (data transmitted in a ``POST`` or ``PUT`` request) you can use the -:attr:`~flask.request.form` attribute. Here is a full example of the two +:attr:`~flask.Request.form` attribute. Here is a full example of the two attributes mentioned above:: @app.route('/login', methods=['POST', 'GET']) @@ -561,14 +563,14 @@ attributes mentioned above:: # was GET or the credentials were invalid return render_template('login.html', error=error) -What happens if the key does not exist in the `form` attribute? In that +What happens if the key does not exist in the ``form`` attribute? In that case a special :exc:`KeyError` is raised. You can catch it like a standard :exc:`KeyError` but if you don't do that, a HTTP 400 Bad Request error page is shown instead. So for many situations you don't have to deal with that problem. To access parameters submitted in the URL (``?key=value``) you can use the -:attr:`~flask.request.args` attribute:: +:attr:`~flask.Request.args` attribute:: searchword = request.args.get('key', '') @@ -577,7 +579,7 @@ We recommend accessing URL parameters with `get` or by catching the bad request page in that case is not user friendly. For a full list of methods and attributes of the request object, head over -to the :class:`~flask.request` documentation. +to the :class:`~flask.Request` documentation. File Uploads @@ -723,17 +725,15 @@ converting return values into response objects is as follows: 3. If a tuple is returned the items in the tuple can provide extra information. Such tuples have to be in the form ``(response, status, headers)`` or ``(response, headers)`` where at least one item has - to be in the tuple. The `status` value will override the status code - and `headers` can be a list or dictionary of additional header values. + to be in the tuple. The ``status`` value will override the status code + and ``headers`` can be a list or dictionary of additional header values. 4. If none of that works, Flask will assume the return value is a valid WSGI application and convert that into a response object. If you want to get hold of the resulting response object inside the view you can use the :func:`~flask.make_response` function. -Imagine you have a view like this: - -.. sourcecode:: python +Imagine you have a view like this:: @app.errorhandler(404) def not_found(error): @@ -741,9 +741,7 @@ Imagine you have a view like this: You just need to wrap the return expression with :func:`~flask.make_response` and get the response object to modify it, then -return it: - -.. sourcecode:: python +return it:: @app.errorhandler(404) def not_found(error): @@ -782,7 +780,7 @@ sessions work:: session['username'] = request.form['username'] return redirect(url_for('index')) return ''' - +

@@ -805,13 +803,13 @@ not using the template engine (as in this example). The problem with random is that it's hard to judge what is truly random. And a secret key should be as random as possible. Your operating system has ways to generate pretty random stuff based on a cryptographic - random generator which can be used to get such a key: + random generator which can be used to get such a key:: - >>> import os - >>> os.urandom(24) - '\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O>> import os + >>> os.urandom(24) + '\xfd{H\xe5<\x95\xf9\xe3\x96.5\xd1\x01O`_. +`_. Flask configures Jinja2 to automatically escape all values unless explicitly told otherwise. This should rule out all XSS problems caused @@ -73,7 +73,7 @@ them knowing. Say you have a specific URL that, when you sent ``POST`` requests to will delete a user's profile (say ``http://example.com/user/delete``). If an attacker now creates a page that sends a post request to that page with -some JavaScript they just has to trick some users to load that page and +some JavaScript they just have to trick some users to load that page and their profiles will end up being deleted. Imagine you were to run Facebook with millions of concurrent users and @@ -95,81 +95,12 @@ the form validation framework, which does not exist in Flask. JSON Security ------------- -.. admonition:: ECMAScript 5 Changes +In Flask 0.10 and lower, :func:`~flask.jsonify` did not serialize top-level +arrays to JSON. This was because of a security vulnerability in ECMAScript 4. - Starting with ECMAScript 5 the behavior of literals changed. Now they - are not constructed with the constructor of ``Array`` and others, but - with the builtin constructor of ``Array`` which closes this particular - attack vector. - -JSON itself is a high-level serialization format, so there is barely -anything that could cause security problems, right? You can't declare -recursive structures that could cause problems and the only thing that -could possibly break are very large responses that can cause some kind of -denial of service at the receiver's side. - -However there is a catch. Due to how browsers work the CSRF issue comes -up with JSON unfortunately. Fortunately there is also a weird part of the -JavaScript specification that can be used to solve that problem easily and -Flask is kinda doing that for you by preventing you from doing dangerous -stuff. Unfortunately that protection is only there for -:func:`~flask.jsonify` so you are still at risk when using other ways to -generate JSON. - -So what is the issue and how to avoid it? The problem are arrays at -top-level in JSON. Imagine you send the following data out in a JSON -request. Say that's exporting the names and email addresses of all your -friends for a part of the user interface that is written in JavaScript. -Not very uncommon: - -.. sourcecode:: javascript - - [ - {"username": "admin", - "email": "admin@localhost"} - ] - -And it is doing that of course only as long as you are logged in and only -for you. And it is doing that for all ``GET`` requests to a certain URL, -say the URL for that request is -``http://example.com/api/get_friends.json``. - -So now what happens if a clever hacker is embedding this to his website -and social engineers a victim to visiting his site: - -.. sourcecode:: html - - - - - -If you know a bit of JavaScript internals you might know that it's -possible to patch constructors and register callbacks for setters. An -attacker can use this (like above) to get all the data you exported in -your JSON file. The browser will totally ignore the :mimetype:`application/json` -mimetype if :mimetype:`text/javascript` is defined as content type in the script -tag and evaluate that as JavaScript. Because top-level array elements are -allowed (albeit useless) and we hooked in our own constructor, after that -page loaded the data from the JSON response is in the `captured` array. - -Because it is a syntax error in JavaScript to have an object literal -(``{...}``) toplevel an attacker could not just do a request to an -external URL with the script tag to load up the data. So what Flask does -is to only allow objects as toplevel elements when using -:func:`~flask.jsonify`. Make sure to do the same when using an ordinary -JSON generate function. +ECMAScript 5 closed this vulnerability, so only extremely old browsers are +still vulnerable. All of these browsers have `other more serious +vulnerabilities +`_, so +this behavior was changed and :func:`~flask.jsonify` now supports serializing +arrays. diff --git a/docs/server.rst b/docs/server.rst index c7a7e641..f8332ebf 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -5,7 +5,7 @@ Development Server .. currentmodule:: flask -Starting with Flask 1.0 there are multiple built-in ways to run a +Starting with Flask 0.11 there are multiple built-in ways to run a development server. The best one is the :command:`flask` command line utility but you can also continue using the :meth:`Flask.run` method. @@ -16,7 +16,9 @@ The :command:`flask` command line script (:ref:`cli`) is strongly recommended fo development because it provides a superior reload experience due to how it loads the application. The basic usage is like this:: - $ flask -a my_application --debug run + $ export FLASK_APP=my_application + $ export FLASK_DEBUG=1 + $ flask run This will enable the debugger, the reloader and then start the server on *http://localhost:5000/*. @@ -25,7 +27,7 @@ The individual features of the server can be controlled by passing more arguments to the ``run`` option. For instance the reloader can be disabled:: - $ flask -a my_application --debug run --no-reload + $ flask run --no-reload In Code ------- @@ -40,11 +42,11 @@ Example:: app.run() This works well for the common case but it does not work well for -development which is why from Flask 1.0 onwards the :command:`flask` method is -recommended. The reason for this is that due to how the reload mechanism -works there are some bizarre side-effects (like executing certain code -twice, sometimes crashing without message or dying when a syntax or -import error happens). +development which is why from Flask 0.11 onwards the :command:`flask` +method is recommended. The reason for this is that due to how the reload +mechanism works there are some bizarre side-effects (like executing +certain code twice, sometimes crashing without message or dying when a +syntax or import error happens). It is however still a perfectly valid method for invoking a non automatic reloading application. diff --git a/docs/shell.rst b/docs/shell.rst index 0caf9139..9d9bb5f9 100644 --- a/docs/shell.rst +++ b/docs/shell.rst @@ -29,7 +29,7 @@ chapter of the documentation first. Command Line Interface ---------------------- -Starting with Flask 1.0 the recommended way to work with the shell is the +Starting with Flask 0.11 the recommended way to work with the shell is the ``flask shell`` command which does a lot of this automatically for you. For instance the shell is automatically initialized with a loaded application context. diff --git a/docs/signals.rst b/docs/signals.rst index b368194c..2426e920 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -19,9 +19,9 @@ more. Also keep in mind that signals are intended to notify subscribers and should not encourage subscribers to modify data. You will notice that there are signals that appear to do the same thing like some of the builtin decorators do (eg: :data:`~flask.request_started` is very similar -to :meth:`~flask.Flask.before_request`). There are however difference in -how they work. The core :meth:`~flask.Flask.before_request` handler for -example is executed in a specific order and is able to abort the request +to :meth:`~flask.Flask.before_request`). However, there are differences in +how they work. The core :meth:`~flask.Flask.before_request` handler, for +example, is executed in a specific order and is able to abort the request early by returning a response. In contrast all signal handlers are executed in undefined order and do not modify any data. diff --git a/docs/styleguide.rst b/docs/styleguide.rst index e03e4ef5..390d5668 100644 --- a/docs/styleguide.rst +++ b/docs/styleguide.rst @@ -167,7 +167,7 @@ Docstring conventions: """ Module header: - The module header consists of an utf-8 encoding declaration (if non + The module header consists of a utf-8 encoding declaration (if non ASCII letters are used, but it is recommended all the time) and a standard docstring:: diff --git a/docs/testing.rst b/docs/testing.rst index 6387202b..bd1c8467 100644 --- a/docs/testing.rst +++ b/docs/testing.rst @@ -24,7 +24,7 @@ the :ref:`tutorial`. If you don't have that application yet, get the sources from `the examples`_. .. _the examples: - https://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ + https://github.com/pallets/flask/tree/master/examples/flaskr/ The Testing Skeleton -------------------- @@ -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 @@ -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) @@ -152,13 +154,13 @@ invalid credentials. Add this new test to the class:: def test_login_logout(self): rv = self.login('admin', 'default') - assert 'You were logged in' in rv.data + assert b'You were logged in' in rv.data rv = self.logout() - assert 'You were logged out' in rv.data + assert b'You were logged out' in rv.data rv = self.login('adminx', 'default') - assert 'Invalid username' in rv.data + assert b'Invalid username' in rv.data rv = self.login('admin', 'defaultx') - assert 'Invalid password' in rv.data + assert b'Invalid password' in rv.data Test Adding Messages -------------------- @@ -172,9 +174,9 @@ like this:: title='', text='HTML allowed here' ), follow_redirects=True) - assert 'No entries here so far' not in rv.data - assert '<Hello>' in rv.data - assert 'HTML allowed here' in rv.data + assert b'No entries here so far' not in rv.data + assert b'<Hello>' in rv.data + assert b'HTML allowed here' in rv.data Here we check that HTML is allowed in the text but not in the title, which is the intended behavior. @@ -194,7 +196,7 @@ suite. .. _MiniTwit Example: - https://github.com/mitsuhiko/flask/tree/master/examples/minitwit/ + https://github.com/pallets/flask/tree/master/examples/minitwit/ Other Testing Tricks @@ -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'): @@ -223,8 +225,8 @@ there does not seem to be a good way to do that, consider switching to application factories (see :ref:`app-factories`). Note however that if you are using a test request context, the -:meth:`~flask.Flask.before_request` functions are not automatically called -same for :meth:`~flask.Flask.after_request` functions. However +:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.after_request` +functions are not called automatically. However :meth:`~flask.Flask.teardown_request` functions are indeed executed when the test request context leaves the ``with`` block. If you do want the :meth:`~flask.Flask.before_request` functions to be called as well, you diff --git a/docs/tutorial/css.rst b/docs/tutorial/css.rst index 3f0e932a..56414657 100644 --- a/docs/tutorial/css.rst +++ b/docs/tutorial/css.rst @@ -1,11 +1,11 @@ .. _tutorial-css: -Step 7: Adding Style +Step 8: Adding Style ==================== Now that everything else works, it's time to add some style to the application. Just create a stylesheet called :file:`style.css` in the -:file:`static` folder we created before: +:file:`static` folder: .. sourcecode:: css diff --git a/docs/tutorial/dbcon.rst b/docs/tutorial/dbcon.rst index 8757114c..2dd3d7be 100644 --- a/docs/tutorial/dbcon.rst +++ b/docs/tutorial/dbcon.rst @@ -1,25 +1,23 @@ .. _tutorial-dbcon: -Step 3: Database Connections +Step 4: Database Connections ---------------------------- -We have created a function for establishing a database connection with -`connect_db`, but by itself, that's not particularly useful. Creating and -closing database connections all the time is very inefficient, so we want -to keep it around for longer. Because database connections encapsulate a -transaction, we also need to make sure that only one request at the time -uses the connection. How can we elegantly do that with Flask? +You currently have a function for establishing a database connection with +`connect_db`, but by itself, it is not particularly useful. Creating and +closing database connections all the time is very inefficient, so you will +need to keep it around for longer. Because database connections +encapsulate a transaction, you will need to make sure that only one +request at a time uses the connection. An elegant way to do this is by +utilizing the *application context*. -This is where the application context comes into play, so let's start -there. - -Flask provides us with two contexts: the application context and the -request context. For the time being, all you have to know is that there +Flask provides two contexts: the *application context* and the +*request context*. For the time being, all you have to know is that there are special variables that use these. For instance, the :data:`~flask.request` variable is the request object associated with the current request, whereas :data:`~flask.g` is a general purpose -variable associated with the current application context. We will go into -the details of this a bit later. +variable associated with the current application context. The tutorial +will cover some more details of this later on. For the time being, all you have to know is that you can store information safely on the :data:`~flask.g` object. @@ -37,8 +35,7 @@ already established connection:: g.sqlite_db = connect_db() return g.sqlite_db - -So now we know how to connect, but how do we properly disconnect? For +Now you know how to connect, but how can you properly disconnect? For that, Flask provides us with the :meth:`~flask.Flask.teardown_appcontext` decorator. It's executed every time the application context tears down:: @@ -75,4 +72,4 @@ Continue to :ref:`tutorial-dbinit`. larger `, it's a good idea not to. .. _example source: - https://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ + https://github.com/pallets/flask/tree/master/examples/flaskr/ diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index b3cf39dc..fbbcde00 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -1,6 +1,6 @@ .. _tutorial-dbinit: -Step 4: Creating The Database +Step 5: Creating The Database ============================= As outlined earlier, Flaskr is a database powered application, and more @@ -16,13 +16,14 @@ Such a schema can be created by piping the ``schema.sql`` file into the The downside of this is that it requires the ``sqlite3`` command to be installed, which is not necessarily the case on every system. This also -requires that we provide the path to the database, which can introduce +requires that you provide the path to the database, which can introduce errors. It's a good idea to add a function that initializes the database -for you to the application. +for you, to the application. -To do this, we can create a function and hook it into the :command:`flask` -command that initializes the database. Let me show you the code first. Just -add this function below the `connect_db` function in :file:`flaskr.py`:: +To do this, you can create a function and hook it into a :command:`flask` +command that initializes the database. For now just take a look at the +code segment below. A good place to add this function, and command, is +just below the `connect_db` function in :file:`flaskr.py`:: def init_db(): db = get_db() @@ -34,33 +35,33 @@ add this function below the `connect_db` function in :file:`flaskr.py`:: def initdb_command(): """Initializes the database.""" init_db() - print 'Initialized the database.' + print('Initialized the database.') The ``app.cli.command()`` decorator registers a new command with the :command:`flask` script. When the command executes, Flask will automatically -create a application context for us bound to the right application. -Within the function, we can then access :attr:`flask.g` and other things as -we would expect. When the script ends, the application context tears down +create an application context which is bound to the right application. +Within the function, you can then access :attr:`flask.g` and other things as +you might expect. When the script ends, the application context tears down and the database connection is released. -We want to keep an actual functions around that initializes the database, +You will want to keep an actual function around that initializes the database, though, so that we can easily create databases in unit tests later on. (For more information see :ref:`testing`.) The :func:`~flask.Flask.open_resource` method of the application object is a convenient helper function that will open a resource that the application provides. This function opens a file from the resource -location (your ``flaskr`` folder) and allows you to read from it. We are -using this here to execute a script on the database connection. +location (the :file:`flaskr/flaskr` folder) and allows you to read from it. +It is used in this example to execute a script on the database connection. -The connection object provided by SQLite can give us a cursor object. -On that cursor, there is a method to execute a complete script. Finally, we +The connection object provided by SQLite can give you a cursor object. +On that cursor, there is a method to execute a complete script. Finally, you only have to commit the changes. SQLite3 and other transactional databases will not commit unless you explicitly tell it to. Now, it is possible to create a database with the :command:`flask` script:: - flask --app=flaskr initdb + flask initdb Initialized the database. .. admonition:: Troubleshooting diff --git a/docs/tutorial/folders.rst b/docs/tutorial/folders.rst index fba19d72..ba62b3b7 100644 --- a/docs/tutorial/folders.rst +++ b/docs/tutorial/folders.rst @@ -3,21 +3,25 @@ Step 0: Creating The Folders ============================ -Before we get started, let's create the folders needed for this +Before getting started, you will need to create the folders needed for this application:: /flaskr - /static - /templates + /flaskr + /static + /templates -The ``flaskr`` folder is not a Python package, but just something where we -drop our files. Later on, we will put our database schema as well as main -module into this folder. It is done in the following way. The files inside -the :file:`static` folder are available to users of the application via HTTP. -This is the place where CSS and Javascript files go. Inside the -:file:`templates` folder, Flask will look for `Jinja2`_ templates. The -templates you create later on in the tutorial will go in this directory. +The application will be installed and run as Python package. This is the +recommended way to install and run Flask applications. You will see exactly +how to run ``flaskr`` later on in this tutorial. For now go ahead and create +the applications directory structure. In the next few steps you will be +creating the database schema as well as the main module. -Continue with :ref:`tutorial-schema`. +As a quick side note, the files inside of the :file:`static` folder are +available to users of the application via HTTP. This is the place where CSS and +JavaScript files go. Inside the :file:`templates` folder, Flask will look for +`Jinja2`_ templates. You will see examples of this later on. + +For now you should continue with :ref:`tutorial-schema`. .. _Jinja2: http://jinja.pocoo.org/ diff --git a/docs/tutorial/index.rst b/docs/tutorial/index.rst index 4b680b9b..f0a583e0 100644 --- a/docs/tutorial/index.rst +++ b/docs/tutorial/index.rst @@ -15,7 +15,7 @@ If you want the full source code in advance or for comparison, check out the `example source`_. .. _example source: - https://github.com/mitsuhiko/flask/tree/master/examples/flaskr/ + https://github.com/pallets/flask/tree/master/examples/flaskr/ .. toctree:: :maxdepth: 2 @@ -24,6 +24,7 @@ the `example source`_. folders schema setup + packaging dbcon dbinit views diff --git a/docs/tutorial/introduction.rst b/docs/tutorial/introduction.rst index d30bd5cf..1abe597f 100644 --- a/docs/tutorial/introduction.rst +++ b/docs/tutorial/introduction.rst @@ -3,8 +3,9 @@ Introducing Flaskr ================== -We will call our blogging application flaskr, but feel free to choose your own -less Web-2.0-ish name ;) Essentially, we want it to do the following things: +This tutorial will demonstrate a blogging application named Flaskr, but feel +free to choose your own less Web-2.0-ish name ;) Essentially, it will do the +following things: 1. Let the user sign in and out with credentials specified in the configuration. Only one user is supported. @@ -14,8 +15,8 @@ less Web-2.0-ish name ;) Essentially, we want it to do the following things: 3. The index page shows all entries so far in reverse chronological order (newest on top) and the user can add new ones from there if logged in. -We will be using SQLite3 directly for this application because it's good -enough for an application of this size. For larger applications, however, +SQLite3 will be used directly for this application because it's good enough +for an application of this size. For larger applications, however, it makes a lot of sense to use `SQLAlchemy`_, as it handles database connections in a more intelligent way, allowing you to target different relational databases at once and more. You might also want to consider @@ -30,4 +31,4 @@ Here a screenshot of the final application: Continue with :ref:`tutorial-folders`. -.. _SQLAlchemy: http://www.sqlalchemy.org/ +.. _SQLAlchemy: https://www.sqlalchemy.org/ diff --git a/docs/tutorial/packaging.rst b/docs/tutorial/packaging.rst new file mode 100644 index 00000000..18f5c9b3 --- /dev/null +++ b/docs/tutorial/packaging.rst @@ -0,0 +1,108 @@ +.. _tutorial-packaging: + +Step 3: Installing flaskr as a Package +====================================== + +Flask is now shipped with built-in support for `Click`_. Click provides +Flask with enhanced and extensible command line utilities. Later in this +tutorial you will see exactly how to extend the ``flask`` command line +interface (CLI). + +A useful pattern to manage a Flask application is to install your app +following the `Python Packaging Guide`_. Presently this involves +creating two new files; :file:`setup.py` and :file:`MANIFEST.in` in the +projects root directory. You also need to add an :file:`__init__.py` +file to make the :file:`flaskr/flaskr` directory a package. After these +changes, your code structure should be:: + + /flaskr + /flaskr + __init__.py + /static + /templates + flaskr.py + schema.sql + setup.py + MANIFEST.in + +The content of the ``setup.py`` file for ``flaskr`` is: + +.. sourcecode:: python + + from setuptools import setup + + setup( + name='flaskr', + packages=['flaskr'], + include_package_data=True, + install_requires=[ + 'flask', + ], + ) + +When using setuptools, it is also necessary to specify any special files +that should be included in your package (in the :file:`MANIFEST.in`). +In this case, the static and templates directories need to be included, +as well as the schema. Create the :file:`MANIFEST.in` and add the +following lines:: + + graft flaskr/templates + graft flaskr/static + include flaskr/schema.sql + +To simplify locating the application, add the following import statement +into this file, :file:`flaskr/__init__.py`: + +.. sourcecode:: python + + from .flaskr import app + +This import statement brings the application instance into the top-level +of the application package. When it is time to run the application, the +Flask development server needs the location of the app instance. This +import statement simplifies the location process. Without it the export +statement a few steps below would need to be +``export FLASK_APP=flaskr.flaskr``. + +At this point you should be able to install the application. As usual, it +is recommended to install your Flask application within a `virtualenv`_. +With that said, go ahead and install the application with:: + + pip install --editable . + +The above installation command assumes that it is run within the projects +root directory, `flaskr/`. The `editable` flag allows editing +source code without having to reinstall the Flask app each time you make +changes. The flaskr app is now installed in your virtualenv (see output +of ``pip freeze``). + +With that out of the way, you should be able to start up the application. +Do this with the following commands:: + + export FLASK_APP=flaskr + export FLASK_DEBUG=true + flask run + +(In case you are on Windows you need to use `set` instead of `export`). +The :envvar:`FLASK_DEBUG` flag enables or disables the interactive debugger. +*Never leave debug mode activated in a production system*, because it will +allow users to execute code on the server! + +You will see a message telling you that server has started along with +the address at which you can access it. + +When you head over to the server in your browser, you will get a 404 error +because we don't have any views yet. That will be addressed a little later, +but first, you should get the database working. + +.. admonition:: Externally Visible Server + + Want your server to be publicly available? Check out the + :ref:`externally visible server ` section for more + information. + +Continue with :ref:`tutorial-dbcon`. + +.. _Click: http://click.pocoo.org +.. _Python Packaging Guide: https://packaging.python.org +.. _virtualenv: https://virtualenv.pypa.io diff --git a/docs/tutorial/schema.rst b/docs/tutorial/schema.rst index b164b94c..00f56f09 100644 --- a/docs/tutorial/schema.rst +++ b/docs/tutorial/schema.rst @@ -3,10 +3,10 @@ Step 1: Database Schema ======================= -First, we want to create the database schema. Only a single table is needed -for this application and we only want to support SQLite, so creating the -database schema is quite easy. Just put the following contents into a file -named `schema.sql` in the just created `flaskr` folder: +In this step, you will create the database schema. Only a single table is +needed for this application and it will only support SQLite. All you need to do +is put the following contents into a file named :file:`schema.sql` in the +:file:`flaskr/flaskr` folder: .. sourcecode:: sql @@ -17,7 +17,7 @@ named `schema.sql` in the just created `flaskr` folder: 'text' text not null ); -This schema consists of a single table called ``entries``. Each row in +This schema consists of a single table called ``entries``. Each row in this table has an ``id``, a ``title``, and a ``text``. The ``id`` is an automatically incrementing integer and a primary key, the other two are strings that must not be null. diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index 703d5504..4bedb54c 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -3,15 +3,16 @@ Step 2: Application Setup Code ============================== -Now that we have the schema in place, we can create the application module. -Let's call it ``flaskr.py``. We will place this file inside the ``flaskr`` -folder. We will begin by adding the imports we need and by adding the config -section. For small applications, it is possible to drop the configuration -directly into the module, and this is what we will be doing here. However, -a cleaner solution would be to create a separate ``.ini`` or ``.py`` file, -load that, and import the values from there. +Now that the schema is in place, you can create the application module, +:file:`flaskr.py`. This file should be placed inside of the +:file:`flaskr/flaskr` folder. The first several lines of code in the +application module are the needed import statements. After that there will be a +few lines of configuration code. For small applications like ``flaskr``, it is +possible to drop the configuration directly into the module. However, a cleaner +solution is to create a separate ``.ini`` or ``.py`` file, load that, and +import the values from there. -First, we add the imports in :file:`flaskr.py`:: +Here are the import statements (in :file:`flaskr.py`):: # all the imports import os @@ -19,12 +20,13 @@ First, we add the imports in :file:`flaskr.py`:: from flask import Flask, request, session, g, redirect, url_for, abort, \ render_template, flash -Next, we can create our actual application and initialize it with the -config from the same file in :file:`flaskr.py`:: +The next couple lines will create the actual application instance and +initialize it with the config from the same file in :file:`flaskr.py`: - # create our little application :) - app = Flask(__name__) - app.config.from_object(__name__) +.. sourcecode:: python + + app = Flask(__name__) # create the application instance :) + app.config.from_object(__name__) # load config from this file , flaskr.py # Load default config and override config from an environment variable app.config.update(dict( @@ -35,8 +37,8 @@ config from the same file in :file:`flaskr.py`:: )) app.config.from_envvar('FLASKR_SETTINGS', silent=True) -The :class:`~flask.Config` object works similarly to a dictionary so we -can update it with new values. +The :class:`~flask.Config` object works similarly to a dictionary, so it can be +updated with new values. .. admonition:: Database Path @@ -55,10 +57,10 @@ can update it with new values. Usually, it is a good idea to load a separate, environment-specific configuration file. Flask allows you to import multiple configurations and it -will use the setting defined in the last import. This enables robust +will use the setting defined in the last import. This enables robust configuration setups. :meth:`~flask.Config.from_envvar` can help achieve this. -.. code-block:: python +.. sourcecode:: python app.config.from_envvar('FLASKR_SETTINGS', silent=True) @@ -74,15 +76,15 @@ that in all cases, only variable names that are uppercase are considered. The ``SECRET_KEY`` is needed to keep the client-side sessions secure. Choose that key wisely and as hard to guess and complex as possible. -We will also add a method that allows for easy connections to the -specified database. This can be used to open a connection on request and +Lastly, you will add a method that allows for easy connections to the +specified database. This can be used to open a connection on request and also from the interactive Python shell or a script. This will come in -handy later. We create a simple database connection through SQLite and +handy later. You can create a simple database connection through SQLite and then tell it to use the :class:`sqlite3.Row` object to represent rows. -This allows us to treat the rows as if they were dictionaries instead of +This allows the rows to be treated as if they were dictionaries instead of tuples. -:: +.. sourcecode:: python def connect_db(): """Connects to the specific database.""" @@ -90,26 +92,6 @@ tuples. rv.row_factory = sqlite3.Row return rv -With that out of the way, you should be able to start up the application -without problems. Do this with the following command:: - - flask --app=flaskr --debug run - -The :option:`--debug` flag enables or disables the interactive debugger. *Never -leave debug mode activated in a production system*, because it will allow -users to execute code on the server! - -You will see a message telling you that server has started along with -the address at which you can access it. - -When you head over to the server in your browser, you will get a 404 error -because we don't have any views yet. We will focus on that a little later, -but first, we should get the database working. - -.. admonition:: Externally Visible Server - - Want your server to be publicly available? Check out the - :ref:`externally visible server ` section for more - information. +In the next section you will see how to run the application. -Continue with :ref:`tutorial-dbcon`. +Continue with :ref:`tutorial-packaging`. diff --git a/docs/tutorial/templates.rst b/docs/tutorial/templates.rst index 77991893..4f7977e8 100644 --- a/docs/tutorial/templates.rst +++ b/docs/tutorial/templates.rst @@ -1,11 +1,12 @@ .. _tutorial-templates: -Step 6: The Templates +Step 7: The Templates ===================== -Now we should start working on the templates. If we were to request the URLs -now, we would only get an exception that Flask cannot find the templates. The -templates are using `Jinja2`_ syntax and have autoescaping enabled by +Now it is time to start working on the templates. As you may have +noticed, if you make requests with the app running, you will get +an exception that Flask cannot find the templates. The templates +are using `Jinja2`_ syntax and have autoescaping enabled by default. This means that unless you mark a value in the code with :class:`~flask.Markup` or with the ``|safe`` filter in the template, Jinja2 will ensure that special characters such as ``<`` or ``>`` are @@ -57,9 +58,9 @@ show_entries.html This template extends the :file:`layout.html` template from above to display the messages. Note that the ``for`` loop iterates over the messages we passed -in with the :func:`~flask.render_template` function. We also tell the -form to submit to your `add_entry` function and use ``POST`` as HTTP -method: +in with the :func:`~flask.render_template` function. Notice that the form is +configured to submit to the `add_entry` view function and use ``POST`` as +HTTP method: .. sourcecode:: html+jinja @@ -78,9 +79,9 @@ method: {% endif %}
    {% for entry in entries %} -
  • {{ entry.title }}

    {{ entry.text|safe }} +
  • {{ entry.title }}

    {{ entry.text|safe }}
  • {% else %} -
  • Unbelievable. No entries here so far +
  • Unbelievable. No entries here so far
  • {% endfor %}
{% endblock %} diff --git a/docs/tutorial/testing.rst b/docs/tutorial/testing.rst index d70b4abe..26099375 100644 --- a/docs/tutorial/testing.rst +++ b/docs/tutorial/testing.rst @@ -8,3 +8,89 @@ expected, it's probably not a bad idea to add automated tests to simplify modifications in the future. The application above is used as a basic example of how to perform unit testing in the :ref:`testing` section of the documentation. Go there to see how easy it is to test Flask applications. + +Adding tests to flaskr +---------------------- + +Assuming you have seen the :ref:`testing` section and have either written +your own tests for ``flaskr`` or have followed along with the examples +provided, you might be wondering about ways to organize the project. + +One possible and recommended project structure is:: + + flaskr/ + flaskr/ + __init__.py + static/ + templates/ + tests/ + test_flaskr.py + setup.py + MANIFEST.in + +For now go ahead a create the :file:`tests/` directory as well as the +:file:`test_flaskr.py` file. + +Running the tests +----------------- + +At this point you can run the tests. Here ``pytest`` will be used. + +.. note:: Make sure that ``pytest`` is installed in the same virtualenv + as flaskr. Otherwise ``pytest`` test will not be able to import the + required components to test the application:: + + pip install -e . + pip install pytest + +Run and watch the tests pass, within the top-level :file:`flaskr/` +directory as:: + + pytest + +Testing + setuptools +-------------------- + +One way to handle testing is to integrate it with ``setuptools``. Here +that requires adding a couple of lines to the :file:`setup.py` file and +creating a new file :file:`setup.cfg`. One benefit of running the tests +this way is that you do not have to install ``pytest``. Go ahead and +update the :file:`setup.py` file to contain:: + + from setuptools import setup + + setup( + name='flaskr', + packages=['flaskr'], + include_package_data=True, + install_requires=[ + 'flask', + ], + setup_requires=[ + 'pytest-runner', + ], + tests_require=[ + 'pytest', + ], + ) + +Now create :file:`setup.cfg` in the project root (alongside +:file:`setup.py`):: + + [aliases] + test=pytest + +Now you can run:: + + python setup.py test + +This calls on the alias created in :file:`setup.cfg` which in turn runs +``pytest`` via ``pytest-runner``, as the :file:`setup.py` script has +been called. (Recall the `setup_requires` argument in :file:`setup.py`) +Following the standard rules of test-discovery your tests will be +found, run, and hopefully pass. + +This is one possible way to run and manage testing. Here ``pytest`` is +used, but there are other options such as ``nose``. Integrating testing +with ``setuptools`` is convenient because it is not necessary to actually +download ``pytest`` or any other testing framework one might use. diff --git a/docs/tutorial/views.rst b/docs/tutorial/views.rst index 10b578a7..4364d973 100644 --- a/docs/tutorial/views.rst +++ b/docs/tutorial/views.rst @@ -1,10 +1,10 @@ .. _tutorial-views: -Step 5: The View Functions +Step 6: The View Functions ========================== -Now that the database connections are working, we can start writing the -view functions. We will need four of them: +Now that the database connections are working, you can start writing the +view functions. You will need four of them: Show Entries ------------ @@ -12,11 +12,11 @@ Show Entries This view shows all the entries stored in the database. It listens on the root of the application and will select title and text from the database. The one with the highest id (the newest entry) will be on top. The rows -returned from the cursor look a bit like tuples because we are using +returned from the cursor look a bit like dictionaries because we are using the :class:`sqlite3.Row` row factory. -The view function will pass the entries as dictionaries to the -:file:`show_entries.html` template and return the rendered one:: +The view function will pass the entries to the :file:`show_entries.html` +template and return the rendered one:: @app.route('/') def show_entries(): @@ -30,7 +30,7 @@ Add New Entry This view lets the user add new entries if they are logged in. This only responds to ``POST`` requests; the actual form is shown on the -`show_entries` page. If everything worked out well, we will +`show_entries` page. If everything worked out well, it will :func:`~flask.flash` an information message to the next request and redirect back to the `show_entries` page:: @@ -45,8 +45,8 @@ redirect back to the `show_entries` page:: flash('New entry was successfully posted') return redirect(url_for('show_entries')) -Note that we check that the user is logged in here (the `logged_in` key is -present in the session and ``True``). +Note that this view checks that the user is logged in (that is, if the +`logged_in` key is present in the session and ``True``). .. admonition:: Security Note @@ -81,11 +81,11 @@ notified about that, and the user is asked again:: return render_template('login.html', error=error) The `logout` function, on the other hand, removes that key from the session -again. We use a neat trick here: if you use the :meth:`~dict.pop` method +again. There is a neat trick here: if you use the :meth:`~dict.pop` method of the dict and pass a second parameter to it (the default), the method will delete the key from the dictionary if present or do nothing when that -key is not in there. This is helpful because now we don't have to check -if the user was logged in. +key is not in there. This is helpful because now it is not necessary to +check if the user was logged in. :: @@ -95,4 +95,23 @@ if the user was logged in. flash('You were logged out') return redirect(url_for('show_entries')) +.. admonition:: Security Note + + Passwords should never be stored in plain text in a production + system. This tutorial uses plain text passwords for simplicity. If you + plan to release a project based off this tutorial out into the world, + passwords should be both `hashed and salted`_ before being stored in a + database or file. + + Fortunately, there are Flask extensions for the purpose of + hashing passwords and verifying passwords against hashes, so adding + this functionality is fairly straight forward. There are also + many general python libraries that can be used for hashing. + + You can find a list of recommended Flask extensions + `here `_ + + Continue with :ref:`tutorial-templates`. + +.. _hashed and salted: https://blog.codinghorror.com/youre-probably-storing-passwords-incorrectly/ diff --git a/docs/upgrading.rst b/docs/upgrading.rst index fca4d75b..af2383c0 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -14,21 +14,75 @@ This section of the documentation enumerates all the changes in Flask from release to release and how you can change your code to have a painless updating experience. -If you want to use the :command:`easy_install` command to upgrade your Flask -installation, make sure to pass it the :option:`-U` parameter:: +Use the :command:`pip` command to upgrade your existing Flask installation by +providing the ``--upgrade`` parameter:: - $ easy_install -U Flask + $ pip install --upgrade Flask -.. _upgrading-to-10: +.. _upgrading-to-012: -Version 1.0 ------------ +Version 0.12 +------------ + +Changes to send_file +```````````````````` + +The ``filename`` is no longer automatically inferred from file-like objects. +This means that the following code will no longer automatically have +``X-Sendfile`` support, etag generation or MIME-type guessing:: + + response = send_file(open('/path/to/file.txt')) + +Any of the following is functionally equivalent:: + + fname = '/path/to/file.txt' + + # Just pass the filepath directly + response = send_file(fname) + + # Set the MIME-type and ETag explicitly + response = send_file(open(fname), mimetype='text/plain') + response.set_etag(...) + + # Set `attachment_filename` for MIME-type guessing + # ETag still needs to be manually set + response = send_file(open(fname), attachment_filename=fname) + response.set_etag(...) + +The reason for this is that some file-like objects have an invalid or even +misleading ``name`` attribute. Silently swallowing errors in such cases was not +a satisfying solution. + +Additionally the default of falling back to ``application/octet-stream`` has +been restricted. If Flask can't guess one or the user didn't provide one, the +function fails if no filename information was provided. + +.. _upgrading-to-011: + +Version 0.11 +------------ + +0.11 is an odd release in the Flask release cycle because it was supposed +to be the 1.0 release. However because there was such a long lead time up +to the release we decided to push out a 0.11 release first with some +changes removed to make the transition easier. If you have been tracking +the master branch which was 1.0 you might see some unexpected changes. -Flask 1.0 removed the ``debug_log_format`` attribute from Flask +In case you did track the master branch you will notice that :command:`flask --app` +is removed now. You need to use the environment variable to specify an +application. + +Debugging +````````` + +Flask 0.11 removed the ``debug_log_format`` attribute from Flask applications. Instead the new ``LOGGER_HANDLER_POLICY`` configuration can be used to disable the default log handlers and custom log handlers can be set up. +Error handling +`````````````` + The behavior of error handlers was changed. The precedence of handlers used to be based on the decoration/call order of :meth:`~flask.Flask.errorhandler` and @@ -37,9 +91,7 @@ Now the inheritance hierarchy takes precedence and handlers for more specific exception classes are executed instead of more general ones. See :ref:`error-handlers` for specifics. -The :func:`~flask.templating.render_template_string` function has changed to -autoescape template variables by default. This better matches the behavior -of :func:`~flask.templating.render_template`. +Trying to register a handler on an instance now raises :exc:`ValueError`. .. note:: @@ -47,8 +99,25 @@ of :func:`~flask.templating.render_template`. only for exception *instances*. This was unintended and plain wrong, and therefore was replaced with the intended behavior of registering handlers only using exception classes and HTTP error codes. - - Trying to register a handler on an instance now raises :exc:`ValueError`. + +Templating +`````````` + +The :func:`~flask.templating.render_template_string` function has changed to +autoescape template variables by default. This better matches the behavior +of :func:`~flask.templating.render_template`. + +Extension imports +````````````````` + +Extension imports of the form ``flask.ext.foo`` are deprecated, you should use +``flask_foo``. + +The old form still works, but Flask will issue a +``flask.exthook.ExtDeprecationWarning`` for each extension you import the old +way. We also provide a migration utility called `flask-ext-migrate +`_ that is supposed to +automatically rewrite your imports for this. .. _upgrading-to-010: @@ -74,7 +143,7 @@ when there is no request context yet but an application context. The old ``flask.Flask.request_globals_class`` attribute was renamed to :attr:`flask.Flask.app_ctx_globals_class`. -.. _Flask-OldSessions: http://pythonhosted.org/Flask-OldSessions/ +.. _Flask-OldSessions: https://pythonhosted.org/Flask-OldSessions/ Version 0.9 ----------- @@ -102,7 +171,7 @@ Version 0.8 ----------- Flask introduced a new session interface system. We also noticed that -there was a naming collision between `flask.session` the module that +there was a naming collision between ``flask.session`` the module that implements sessions and :data:`flask.session` which is the global session object. With that introduction we moved the implementation details for the session system into a new module called :mod:`flask.sessions`. If you @@ -129,7 +198,7 @@ applications with Flask. Because we want to make upgrading as easy as possible we tried to counter the problems arising from these changes by providing a script that can ease the transition. -The script scans your whole application and generates an unified diff with +The script scans your whole application and generates a unified diff with changes it assumes are safe to apply. However as this is an automated tool it won't be able to find all use cases and it might miss some. We internally spread a lot of deprecation warnings all over the place to make @@ -146,7 +215,7 @@ good. To apply the upgrade script do the following: 1. Download the script: `flask-07-upgrade.py - `_ + `_ 2. Run it in the directory of your application:: python flask-07-upgrade.py > patchfile.diff @@ -168,7 +237,7 @@ Please note that deprecation warnings are disabled by default starting with Python 2.7. In order to see the deprecation warnings that might be emitted you have to enabled them with the :mod:`warnings` module. -If you are working with windows and you lack the `patch` command line +If you are working with windows and you lack the ``patch`` command line utility you can get it as part of various Unix runtime environments for windows including cygwin, msysgit or ming32. Also source control systems like svn, hg or git have builtin support for applying unified diffs as @@ -185,7 +254,7 @@ before, you should catch them with :exc:`RuntimeError` now. Additionally the :func:`~flask.send_file` function is now issuing deprecation warnings if you depend on functionality that will be removed -in Flask 1.0. Previously it was possible to use etags and mimetypes +in Flask 0.11. Previously it was possible to use etags and mimetypes when file objects were passed. This was unreliable and caused issues for a few setups. If you get a deprecation warning, make sure to update your application to work with either filenames there or disable @@ -285,7 +354,7 @@ to upgrade. What changed? runtime. - Blueprints have an inverse behavior for :meth:`url_for`. Previously ``.foo`` told :meth:`url_for` that it should look for the endpoint - `foo` on the application. Now it means “relative to current module”. + ``foo`` on the application. Now it means “relative to current module”. The script will inverse all calls to :meth:`url_for` automatically for you. It will do this in a very eager way so you might end up with some unnecessary leading dots in your code if you're not using @@ -303,7 +372,7 @@ to upgrade. What changed? name into that folder if you want :file:`blueprintname/template.html` as the template name. -If you continue to use the `Module` object which is deprecated, Flask will +If you continue to use the ``Module`` object which is deprecated, Flask will restore the previous behavior as good as possible. However we strongly recommend upgrading to the new blueprints as they provide a lot of useful improvement such as the ability to attach a blueprint multiple times, @@ -323,7 +392,7 @@ change the order. Another change that breaks backwards compatibility is that context processors will no longer override values passed directly to the template -rendering function. If for example `request` is as variable passed +rendering function. If for example ``request`` is as variable passed directly to the template, the default context processor will not override it with the current request object. This makes it easier to extend context processors later to inject additional variables without breaking @@ -349,7 +418,7 @@ The following changes may be relevant to your application: for this feature. Removing support for this makes the Flask internal code easier to understand and fixes a couple of small issues that make debugging harder than necessary. -- The `create_jinja_loader` function is gone. If you want to customize +- The ``create_jinja_loader`` function is gone. If you want to customize the Jinja loader now, use the :meth:`~flask.Flask.create_jinja_environment` method instead. diff --git a/examples/flaskr/.gitignore b/examples/flaskr/.gitignore index fb46a3af..8d567f84 100644 --- a/examples/flaskr/.gitignore +++ b/examples/flaskr/.gitignore @@ -1 +1,2 @@ flaskr.db +.eggs/ diff --git a/examples/flaskr/MANIFEST.in b/examples/flaskr/MANIFEST.in new file mode 100644 index 00000000..efbd93df --- /dev/null +++ b/examples/flaskr/MANIFEST.in @@ -0,0 +1,3 @@ +graft flaskr/templates +graft flaskr/static +include flaskr/schema.sql diff --git a/examples/flaskr/README b/examples/flaskr/README index 6259082a..90860ff2 100644 --- a/examples/flaskr/README +++ b/examples/flaskr/README @@ -13,18 +13,26 @@ export an FLASKR_SETTINGS environment variable pointing to a configuration file. - 2. initialize the database with this command: + 2. install the app from the root of the project directory - flask --app=flaskr initdb + pip install --editable . - 3. now you can run flaskr: + 3. Instruct flask to use the right application - flask --app=flaskr run + export FLASK_APP=flaskr + + 4. initialize the database with this command: + + flask initdb + + 5. now you can run flaskr: + + flask run the application will greet you on http://localhost:5000/ ~ Is it tested? - You betcha. Run the `test_flaskr.py` file to see + You betcha. Run `python setup.py test` to see the tests pass. diff --git a/examples/flaskr/flaskr/__init__.py b/examples/flaskr/flaskr/__init__.py new file mode 100644 index 00000000..d096b1e7 --- /dev/null +++ b/examples/flaskr/flaskr/__init__.py @@ -0,0 +1 @@ +from .flaskr import app diff --git a/examples/flaskr/flaskr.py b/examples/flaskr/flaskr/flaskr.py similarity index 100% rename from examples/flaskr/flaskr.py rename to examples/flaskr/flaskr/flaskr.py diff --git a/examples/flaskr/schema.sql b/examples/flaskr/flaskr/schema.sql similarity index 100% rename from examples/flaskr/schema.sql rename to examples/flaskr/flaskr/schema.sql diff --git a/examples/flaskr/static/style.css b/examples/flaskr/flaskr/static/style.css similarity index 100% rename from examples/flaskr/static/style.css rename to examples/flaskr/flaskr/static/style.css diff --git a/examples/flaskr/templates/layout.html b/examples/flaskr/flaskr/templates/layout.html similarity index 100% rename from examples/flaskr/templates/layout.html rename to examples/flaskr/flaskr/templates/layout.html diff --git a/examples/flaskr/templates/login.html b/examples/flaskr/flaskr/templates/login.html similarity index 100% rename from examples/flaskr/templates/login.html rename to examples/flaskr/flaskr/templates/login.html diff --git a/examples/flaskr/templates/show_entries.html b/examples/flaskr/flaskr/templates/show_entries.html similarity index 80% rename from examples/flaskr/templates/show_entries.html rename to examples/flaskr/flaskr/templates/show_entries.html index 9cbd3229..2f68b9d3 100644 --- a/examples/flaskr/templates/show_entries.html +++ b/examples/flaskr/flaskr/templates/show_entries.html @@ -13,9 +13,9 @@ {% endif %}
    {% for entry in entries %} -
  • {{ entry.title }}

    {{ entry.text|safe }} +
  • {{ entry.title }}

    {{ entry.text|safe }}
  • {% else %} -
  • Unbelievable. No entries here so far +
  • Unbelievable. No entries here so far
  • {% endfor %}
{% endblock %} diff --git a/examples/flaskr/setup.cfg b/examples/flaskr/setup.cfg new file mode 100644 index 00000000..db50667a --- /dev/null +++ b/examples/flaskr/setup.cfg @@ -0,0 +1,2 @@ +[tool:pytest] +test=pytest diff --git a/examples/flaskr/setup.py b/examples/flaskr/setup.py new file mode 100644 index 00000000..910f23ac --- /dev/null +++ b/examples/flaskr/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup( + name='flaskr', + packages=['flaskr'], + include_package_data=True, + install_requires=[ + 'flask', + ], + setup_requires=[ + 'pytest-runner', + ], + tests_require=[ + 'pytest', + ], +) diff --git a/examples/flaskr/test_flaskr.py b/examples/flaskr/tests/test_flaskr.py similarity index 98% rename from examples/flaskr/test_flaskr.py rename to examples/flaskr/tests/test_flaskr.py index 084f13f4..663e92e0 100644 --- a/examples/flaskr/test_flaskr.py +++ b/examples/flaskr/tests/test_flaskr.py @@ -9,11 +9,10 @@ :license: BSD, see LICENSE for more details. """ -import pytest - import os -import flaskr import tempfile +import pytest +from flaskr import flaskr @pytest.fixture diff --git a/examples/minitwit/.gitignore b/examples/minitwit/.gitignore new file mode 100644 index 00000000..c3accd82 --- /dev/null +++ b/examples/minitwit/.gitignore @@ -0,0 +1,2 @@ +minitwit.db +.eggs/ diff --git a/examples/minitwit/MANIFEST.in b/examples/minitwit/MANIFEST.in new file mode 100644 index 00000000..973d6586 --- /dev/null +++ b/examples/minitwit/MANIFEST.in @@ -0,0 +1,3 @@ +graft minitwit/templates +graft minitwit/static +include minitwit/schema.sql \ No newline at end of file diff --git a/examples/minitwit/README b/examples/minitwit/README index 92fae233..b9bc5ea2 100644 --- a/examples/minitwit/README +++ b/examples/minitwit/README @@ -14,18 +14,26 @@ export an MINITWIT_SETTINGS environment variable pointing to a configuration file. - 2. fire up a shell and run this: + 2. install the app from the root of the project directory - flask --app=minitwit initdb + pip install --editable . - 3. now you can run minitwit: + 3. tell flask about the right application: - flask --app=minitwit run + export FLASK_APP=minitwit + + 4. fire up a shell and run this: + + flask initdb + + 5. now you can run minitwit: + + flask run the application will greet you on http://localhost:5000/ ~ Is it tested? - You betcha. Run the `test_minitwit.py` file to + You betcha. Run the `python setup.py test` file to see the tests pass. diff --git a/examples/minitwit/minitwit/__init__.py b/examples/minitwit/minitwit/__init__.py new file mode 100644 index 00000000..96c81aec --- /dev/null +++ b/examples/minitwit/minitwit/__init__.py @@ -0,0 +1 @@ +from .minitwit import app diff --git a/examples/minitwit/minitwit.py b/examples/minitwit/minitwit/minitwit.py similarity index 99% rename from examples/minitwit/minitwit.py rename to examples/minitwit/minitwit/minitwit.py index bbc3b483..69840267 100644 --- a/examples/minitwit/minitwit.py +++ b/examples/minitwit/minitwit/minitwit.py @@ -85,7 +85,7 @@ def format_datetime(timestamp): def gravatar_url(email, size=80): """Return the gravatar image for the given email address.""" - return 'http://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ + return 'https://www.gravatar.com/avatar/%s?d=identicon&s=%d' % \ (md5(email.strip().lower().encode('utf-8')).hexdigest(), size) diff --git a/examples/minitwit/schema.sql b/examples/minitwit/minitwit/schema.sql similarity index 100% rename from examples/minitwit/schema.sql rename to examples/minitwit/minitwit/schema.sql diff --git a/examples/minitwit/static/style.css b/examples/minitwit/minitwit/static/style.css similarity index 100% rename from examples/minitwit/static/style.css rename to examples/minitwit/minitwit/static/style.css diff --git a/examples/minitwit/templates/layout.html b/examples/minitwit/minitwit/templates/layout.html similarity index 100% rename from examples/minitwit/templates/layout.html rename to examples/minitwit/minitwit/templates/layout.html diff --git a/examples/minitwit/templates/login.html b/examples/minitwit/minitwit/templates/login.html similarity index 100% rename from examples/minitwit/templates/login.html rename to examples/minitwit/minitwit/templates/login.html diff --git a/examples/minitwit/templates/register.html b/examples/minitwit/minitwit/templates/register.html similarity index 100% rename from examples/minitwit/templates/register.html rename to examples/minitwit/minitwit/templates/register.html diff --git a/examples/minitwit/templates/timeline.html b/examples/minitwit/minitwit/templates/timeline.html similarity index 100% rename from examples/minitwit/templates/timeline.html rename to examples/minitwit/minitwit/templates/timeline.html diff --git a/examples/minitwit/setup.cfg b/examples/minitwit/setup.cfg new file mode 100644 index 00000000..b7e47898 --- /dev/null +++ b/examples/minitwit/setup.cfg @@ -0,0 +1,2 @@ +[aliases] +test=pytest diff --git a/examples/minitwit/setup.py b/examples/minitwit/setup.py new file mode 100644 index 00000000..1e580216 --- /dev/null +++ b/examples/minitwit/setup.py @@ -0,0 +1,16 @@ +from setuptools import setup + +setup( + name='minitwit', + packages=['minitwit'], + include_package_data=True, + install_requires=[ + 'flask', + ], + setup_requires=[ + 'pytest-runner', + ], + tests_require=[ + 'pytest', + ], +) \ No newline at end of file diff --git a/examples/minitwit/test_minitwit.py b/examples/minitwit/tests/test_minitwit.py similarity index 99% rename from examples/minitwit/test_minitwit.py rename to examples/minitwit/tests/test_minitwit.py index bd58d4dc..50ca26d9 100644 --- a/examples/minitwit/test_minitwit.py +++ b/examples/minitwit/tests/test_minitwit.py @@ -9,9 +9,9 @@ :license: BSD, see LICENSE for more details. """ import os -import minitwit import tempfile import pytest +from minitwit import minitwit @pytest.fixture diff --git a/examples/patterns/largerapp/setup.py b/examples/patterns/largerapp/setup.py new file mode 100644 index 00000000..eaf00f07 --- /dev/null +++ b/examples/patterns/largerapp/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup + +setup( + name='yourapplication', + packages=['yourapplication'], + include_package_data=True, + install_requires=[ + 'flask', + ], +) diff --git a/examples/patterns/largerapp/tests/test_largerapp.py b/examples/patterns/largerapp/tests/test_largerapp.py new file mode 100644 index 00000000..6bc0531e --- /dev/null +++ b/examples/patterns/largerapp/tests/test_largerapp.py @@ -0,0 +1,12 @@ +from yourapplication import app +import pytest + +@pytest.fixture +def client(): + app.config['TESTING'] = True + client = app.test_client() + return client + +def test_index(client): + rv = client.get('/') + assert b"Hello World!" in rv.data \ No newline at end of file diff --git a/examples/patterns/largerapp/yourapplication/__init__.py b/examples/patterns/largerapp/yourapplication/__init__.py new file mode 100644 index 00000000..089d2937 --- /dev/null +++ b/examples/patterns/largerapp/yourapplication/__init__.py @@ -0,0 +1,4 @@ +from flask import Flask +app = Flask(__name__) + +import yourapplication.views \ No newline at end of file diff --git a/examples/patterns/largerapp/yourapplication/static/style.css b/examples/patterns/largerapp/yourapplication/static/style.css new file mode 100644 index 00000000..e69de29b diff --git a/examples/patterns/largerapp/yourapplication/templates/index.html b/examples/patterns/largerapp/yourapplication/templates/index.html new file mode 100644 index 00000000..e69de29b diff --git a/examples/patterns/largerapp/yourapplication/templates/layout.html b/examples/patterns/largerapp/yourapplication/templates/layout.html new file mode 100644 index 00000000..e69de29b diff --git a/examples/patterns/largerapp/yourapplication/templates/login.html b/examples/patterns/largerapp/yourapplication/templates/login.html new file mode 100644 index 00000000..e69de29b diff --git a/examples/patterns/largerapp/yourapplication/views.py b/examples/patterns/largerapp/yourapplication/views.py new file mode 100644 index 00000000..b112328e --- /dev/null +++ b/examples/patterns/largerapp/yourapplication/views.py @@ -0,0 +1,5 @@ +from yourapplication import app + +@app.route('/') +def index(): + return 'Hello World!' \ No newline at end of file diff --git a/examples/persona/README.md b/examples/persona/README.md deleted file mode 100644 index d9482d36..00000000 --- a/examples/persona/README.md +++ /dev/null @@ -1,3 +0,0 @@ -A simple example for integrating [Persona](https://login.persona.org/) into a -Flask application. In addition to Flask, it requires the -[requests](www.python-requests.org/) library. diff --git a/examples/persona/persona.py b/examples/persona/persona.py deleted file mode 100644 index 7374c3af..00000000 --- a/examples/persona/persona.py +++ /dev/null @@ -1,55 +0,0 @@ -from flask import Flask, render_template, session, request, abort, g - -import requests - - -app = Flask(__name__) -app.config.update( - DEBUG=True, - SECRET_KEY='my development key', - PERSONA_JS='https://login.persona.org/include.js', - PERSONA_VERIFIER='https://verifier.login.persona.org/verify', -) -app.config.from_envvar('PERSONA_SETTINGS', silent=True) - - -@app.before_request -def get_current_user(): - g.user = None - email = session.get('email') - if email is not None: - g.user = email - - -@app.route('/') -def index(): - """Just a generic index page to show.""" - return render_template('index.html') - - -@app.route('/_auth/login', methods=['GET', 'POST']) -def login_handler(): - """This is used by the persona.js file to kick off the - verification securely from the server side. If all is okay - the email address is remembered on the server. - """ - resp = requests.post(app.config['PERSONA_VERIFIER'], data={ - 'assertion': request.form['assertion'], - 'audience': request.host_url, - }, verify=True) - if resp.ok: - verification_data = resp.json() - if verification_data['status'] == 'okay': - session['email'] = verification_data['email'] - return 'OK' - - abort(400) - - -@app.route('/_auth/logout', methods=['POST']) -def logout_handler(): - """This is what persona.js will call to sign the user - out again. - """ - session.clear() - return 'OK' diff --git a/examples/persona/static/persona.js b/examples/persona/static/persona.js deleted file mode 100644 index c1fcdb79..00000000 --- a/examples/persona/static/persona.js +++ /dev/null @@ -1,52 +0,0 @@ -$(function() { - /* convert the links into clickable buttons that go to the - persona service */ - $('a.signin').on('click', function() { - navigator.id.request({ - siteName: 'Flask Persona Example' - }); - return false; - }); - - $('a.signout').on('click', function() { - navigator.id.logout(); - return false; - }); - - /* watch persona state changes */ - navigator.id.watch({ - loggedInUser: $CURRENT_USER, - onlogin: function(assertion) { - /* because the login needs to verify the provided assertion - with the persona service which requires an HTTP request, - this could take a bit. To not confuse the user we show - a progress box */ - var box = $('
') - .hide() - .text('Please wait ...') - .appendTo('body') - .fadeIn('fast'); - $.ajax({ - type: 'POST', - url: $URL_ROOT + '_auth/login', - data: {assertion: assertion}, - success: function(res, status, xhr) { window.location.reload(); }, - error: function(xhr, status, err) { - box.remove(); - navigator.id.logout(); - alert('Login failure: ' + err); - } - }); - }, - onlogout: function() { - $.ajax({ - type: 'POST', - url: $URL_ROOT + '_auth/logout', - success: function(res, status, xhr) { window.location.reload(); }, - error: function(xhr, status, err) { - alert('Logout failure: ' + err); - } - }); - } - }); -}); diff --git a/examples/persona/static/spinner.png b/examples/persona/static/spinner.png deleted file mode 100644 index c9cd8358..00000000 Binary files a/examples/persona/static/spinner.png and /dev/null differ diff --git a/examples/persona/static/style.css b/examples/persona/static/style.css deleted file mode 100644 index 6f0c5f15..00000000 --- a/examples/persona/static/style.css +++ /dev/null @@ -1,39 +0,0 @@ -html { - background: #eee; -} - -body { - font-family: 'Verdana', sans-serif; - font-size: 15px; - margin: 30px auto; - width: 720px; - background: white; - padding: 30px; -} - -h1 { - margin: 0; -} - -h1, h2, a { - color: #d00; -} - -div.authbar { - background: #eee; - padding: 0 15px; - margin: 10px -15px; - line-height: 25px; - height: 25px; - vertical-align: middle; -} - -div.signinprogress { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(255, 255, 255, 0.8) url(spinner.png) center center no-repeat; - font-size: 0; -} diff --git a/examples/persona/templates/index.html b/examples/persona/templates/index.html deleted file mode 100644 index bcccdfc1..00000000 --- a/examples/persona/templates/index.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "layout.html" %} -{% block title %}Welcome{% endblock %} -{% block body %} -

Welcome

-

- This is a small example application that shows how to integrate - Mozilla's persona signin service into a Flask application. -

- The advantage of persona over your own login system is that the - password is managed outside of your application and you get - a verified mail address as primary identifier for your user. -

- In this example nothing is actually stored on the server, it - just takes over the email address from the persona verifier - and stores it in a session. - {% if g.user %} -

- You are now logged in as {{ g.user }} - {% else %} -

- To sign in click the sign in button above. - {% endif %} -{% endblock %} diff --git a/examples/persona/templates/layout.html b/examples/persona/templates/layout.html deleted file mode 100644 index 3fe46620..00000000 --- a/examples/persona/templates/layout.html +++ /dev/null @@ -1,27 +0,0 @@ - -{% block title %}{% endblock %} | Flask Persona Example - - - - - - -

-

Mozilla Persona Example

-
- {% if g.user %} - Signed in as {{ g.user }} - (Sign out) - {% else %} - Not signed in. - {% endif %} -
-
-{% block body %}{% endblock %} diff --git a/flask/__init__.py b/flask/__init__.py index 7fd7a253..bb6c4c18 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -10,7 +10,7 @@ :license: BSD, see LICENSE for more details. """ -__version__ = '0.11.dev0' +__version__ = '0.13-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. @@ -40,7 +40,7 @@ from .signals import signals_available, template_rendered, request_started, \ # it. from . import json -# This was the only thing that flask used to export at one point and it had +# This was the only thing that Flask used to export at one point and it had # a more generic name. jsonify = json.jsonify diff --git a/flask/_compat.py b/flask/_compat.py index bfe607d6..071628fc 100644 --- a/flask/_compat.py +++ b/flask/_compat.py @@ -65,17 +65,25 @@ def with_metaclass(meta, *bases): # Certain versions of pypy have a bug where clearing the exception stack -# breaks the __exit__ function in a very peculiar way. This is currently -# true for pypy 2.2.1 for instance. The second level of exception blocks -# is necessary because pypy seems to forget to check if an exception -# happened until the next bytecode instruction? +# breaks the __exit__ function in a very peculiar way. The second level of +# exception blocks is necessary because pypy seems to forget to check if an +# exception happened until the next bytecode instruction? +# +# Relevant PyPy bugfix commit: +# https://bitbucket.org/pypy/pypy/commits/77ecf91c635a287e88e60d8ddb0f4e9df4003301 +# According to ronan on #pypy IRC, it is released in PyPy2 2.3 and later +# versions. +# +# Ubuntu 14.04 has PyPy 2.2.1, which does exhibit this bug. BROKEN_PYPY_CTXMGR_EXIT = False if hasattr(sys, 'pypy_version_info'): class _Mgr(object): def __enter__(self): return self def __exit__(self, *args): - sys.exc_clear() + if hasattr(sys, 'exc_clear'): + # Python 3 (PyPy3) doesn't have exc_clear + sys.exc_clear() try: try: with _Mgr(): diff --git a/flask/app.py b/flask/app.py index 33b543dd..84f55d2c 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 collections import deque - -from werkzeug.datastructures import ImmutableDict -from werkzeug.routing import Map, Rule, RequestRedirect, BuildError -from werkzeug.exceptions import HTTPException, InternalServerError, \ - MethodNotAllowed, BadRequest, default_exceptions +from itertools import chain +from threading import Lock -from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - locked_cached_property, _endpoint_from_view_func, find_package -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() @@ -120,9 +120,12 @@ class Flask(_PackageBoundObject): The `instance_path` and `instance_relative_config` parameters were added. - .. versionadded:: 1.0 + .. 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 @@ -159,7 +169,7 @@ class Flask(_PackageBoundObject): #: The class that is used for the Jinja environment. #: - #: .. versionadded:: 1.0 + #: .. versionadded:: 0.11 jinja_environment = Environment #: The class that is used for the :data:`~flask.g` instance. @@ -198,7 +208,7 @@ class Flask(_PackageBoundObject): #: 1. Default values for certain config options. #: 2. Access to config values through attributes in addition to keys. #: - #: .. versionadded:: 1.0 + #: .. versionadded:: 0.11 config_class = Config #: The debug flag. Set this to ``True`` to enable debugging of the @@ -289,7 +299,7 @@ class Flask(_PackageBoundObject): #: Default configuration parameters. default_config = ImmutableDict({ - 'DEBUG': False, + 'DEBUG': get_debug_flag(default=False), 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, 'PRESERVE_CONTEXT_ON_EXCEPTION': None, @@ -314,7 +324,8 @@ class Flask(_PackageBoundObject): 'PREFERRED_URL_SCHEME': 'http', 'JSON_AS_ASCII': True, 'JSON_SORT_KEYS': True, - 'JSONIFY_PRETTYPRINT_REGULAR': True, + 'JSONIFY_PRETTYPRINT_REGULAR': False, + 'JSONIFY_MIMETYPE': 'application/json', 'TEMPLATES_AUTO_RELOAD': None, }) @@ -336,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, @@ -390,7 +402,7 @@ class Flask(_PackageBoundObject): #: is the class for the instance check and the second the error handler #: function. #: - #: To register a error handler, use the :meth:`errorhandler` + #: To register an error handler, use the :meth:`errorhandler` #: decorator. self.error_handler_spec = {None: self._error_handlers} @@ -421,9 +433,8 @@ class Flask(_PackageBoundObject): #: A dictionary with lists of functions that should be called after #: each 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:`after_request` decorator. + #: example be used to close database connections. To register a function + #: here, use the :meth:`after_request` decorator. self.after_request_funcs = {} #: A dictionary with lists of functions that are called after @@ -481,7 +492,7 @@ class Flask(_PackageBoundObject): #: A list of shell context processor functions that should be run #: when a shell context is created. #: - #: .. versionadded:: 1.0 + #: .. versionadded:: 0.11 self.shell_context_processors = [] #: all the attached blueprints in a dictionary by name. Blueprints @@ -518,26 +529,29 @@ class Flask(_PackageBoundObject): #: def to_python(self, value): #: return value.split(',') #: def to_url(self, values): - #: return ','.join(BaseConverter.to_url(value) + #: return ','.join(super(ListConverter, self).to_url(value) #: for value in values) #: #: app = Flask(__name__) #: 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 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 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 @@ -546,7 +560,7 @@ class Flask(_PackageBoundObject): #: provided by Flask itself and can be overridden. #: #: This is an instance of a :class:`click.Group` object. - self.cli = cli.AppGroup(self) + self.cli = cli.AppGroup(self.name) def _get_error_handlers(self): from warnings import warn @@ -683,7 +697,7 @@ class Flask(_PackageBoundObject): this function to customize the behavior. .. versionadded:: 0.5 - .. versionchanged:: 1.0 + .. versionchanged:: 0.11 ``Environment.auto_reload`` set in accordance with ``TEMPLATES_AUTO_RELOAD`` configuration option. """ @@ -772,7 +786,7 @@ class Flask(_PackageBoundObject): application. This runs all the registered shell context processors. - .. versionadded:: 1.0 + .. versionadded:: 0.11 """ rv = {'app': self, 'g': g} for processor in self.shell_context_processors: @@ -780,8 +794,13 @@ class Flask(_PackageBoundObject): return rv def run(self, host=None, port=None, debug=None, **options): - """Runs the application on a local development server. If the - :attr:`debug` flag is set the server will automatically reload + """Runs the application on a local development server. + + Do not use ``run()`` in a production setting. It is not intended to + meet security and performance requirements for a production server. + Instead, see :ref:`deployment` for WSGI server recommendations. + + If the :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. If you want to run the application in debug mode, but disable the @@ -791,8 +810,7 @@ class Flask(_PackageBoundObject): It is not recommended to use this function for development with automatic reloading as this is badly supported. Instead you should - be using the :command:`flask` command line script's ``runserver`` - support. + be using the :command:`flask` command line script's ``run`` support. .. admonition:: Keep in Mind @@ -809,7 +827,8 @@ class Flask(_PackageBoundObject): :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to have the server available externally as well. Defaults to - ``'127.0.0.1'``. + ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config + variable if present. :param port: the port of the webserver. Defaults to ``5000`` or the port defined in the ``SERVER_NAME`` config variable if present. @@ -820,15 +839,22 @@ class Flask(_PackageBoundObject): :func:`werkzeug.serving.run_simple` for more information. """ + # Change this into a no-op if the server is invoked from the + # command line. Have a look at cli.py for more information. + if os.environ.get('FLASK_RUN_FROM_CLI_SERVER') == '1': + from .debughelpers import explain_ignored_app_run + explain_ignored_app_run() + return + from werkzeug.serving import run_simple - if host is None: - host = '127.0.0.1' - if port is None: - server_name = self.config['SERVER_NAME'] - if server_name and ':' in server_name: - port = int(server_name.rsplit(':', 1)[1]) - else: - port = 5000 + _host = '127.0.0.1' + _port = 5000 + server_name = self.config.get("SERVER_NAME") + sn_host, sn_port = None, None + if server_name: + sn_host, _, sn_port = server_name.partition(':') + host = host or sn_host or _host + port = int(port or sn_port or _port) if debug is not None: self.debug = bool(debug) options.setdefault('use_reloader', self.debug) @@ -837,7 +863,7 @@ class Flask(_PackageBoundObject): run_simple(host, port, self, **options) finally: # reset the first request information if the development server - # resetted normally. This makes it possible to restart the server + # reset normally. This makes it possible to restart the server # without reloader and that stuff from an interactive shell. self._got_first_request = False @@ -871,9 +897,9 @@ class Flask(_PackageBoundObject): from flask.testing import FlaskClient class CustomClient(FlaskClient): - def __init__(self, authentication=None, *args, **kwargs): - FlaskClient.__init__(*args, **kwargs) - self._authentication = authentication + def __init__(self, *args, **kwargs): + self._authentication = kwargs.pop("authentication") + super(CustomClient,self).__init__( *args, **kwargs) app.test_client_class = CustomClient client = app.test_client(authentication='Basic ....') @@ -888,7 +914,7 @@ class Flask(_PackageBoundObject): to override the client to be used by setting the :attr:`test_client_class` attribute. - .. versionchanged:: 1.0 + .. versionchanged:: 0.11 Added `**kwargs` to support passing additional keyword arguments to the constructor of :attr:`test_client_class`. """ @@ -949,12 +975,12 @@ class Flask(_PackageBoundObject): def iter_blueprints(self): """Iterates over all blueprints by the order they were registered. - .. versionadded:: 1.0 + .. versionadded:: 0.11 """ 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. @@ -994,6 +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 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 @@ -1023,8 +1053,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 = 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: @@ -1110,7 +1141,7 @@ class Flask(_PackageBoundObject): @setupmethod def errorhandler(self, code_or_exception): - """A decorator that is used to register a function give a given + """A decorator that is used to register a function given an error code. Example:: @app.errorhandler(404) @@ -1148,7 +1179,8 @@ class Flask(_PackageBoundObject): that do not necessarily have to be a subclass of the :class:`~werkzeug.exceptions.HTTPException` class. - :param code: the code as integer for the handler + :param code_or_exception: the code as integer for the handler, or + an arbitrary exception """ def decorator(f): self._register_error_handler(None, code_or_exception, f) @@ -1342,7 +1374,7 @@ class Flask(_PackageBoundObject): will have to surround the execution of these code by try/except statements and log occurring errors. - When a teardown function was called because of a exception it will + When a teardown function was called because of an exception it will be passed an error object. The return values of teardown functions are ignored. @@ -1398,7 +1430,7 @@ class Flask(_PackageBoundObject): def shell_context_processor(self, f): """Registers a shell context processor function. - .. versionadded:: 1.0 + .. versionadded:: 0.11 """ self.shell_context_processors.append(f) return f @@ -1431,24 +1463,13 @@ class Flask(_PackageBoundObject): def find_handler(handler_map): if not handler_map: return - queue = deque(exc_class.__mro__) - # Protect from geniuses who might create circular references in - # __mro__ - done = set() - - while queue: - cls = queue.popleft() - if cls in done: - continue - done.add(cls) + for cls in exc_class.__mro__: handler = handler_map.get(cls) if handler is not None: # cache for next time exc_class is raised handler_map[exc_class] = handler return handler - queue.extend(cls.__mro__) - # try blueprint handlers handler = find_handler(self.error_handler_spec .get(request.blueprint, {}) @@ -1550,7 +1571,7 @@ class Flask(_PackageBoundObject): self.log_exception((exc_type, exc_value, tb)) if handler is None: return InternalServerError() - return handler(e) + return self.finalize_request(handler(e), from_error_handler=True) def log_exception(self, exc_info): """Logs an exception. This is called by :meth:`handle_exception` @@ -1618,9 +1639,30 @@ class Flask(_PackageBoundObject): rv = self.dispatch_request() except Exception as e: rv = self.handle_user_exception(e) + return self.finalize_request(rv) + + def finalize_request(self, rv, from_error_handler=False): + """Given the return value from a view function this finalizes + the request by converting it into a response and invoking the + postprocessing functions. This is invoked for both normal + request dispatching as well as error handlers. + + Because this means that it might be called as a result of a + failure a special safe mode is available which can be enabled + with the `from_error_handler` flag. If enabled, failures in + response processing will be logged and otherwise ignored. + + :internal: + """ response = self.make_response(rv) - response = self.process_response(response) - request_finished.send(self, response=response) + try: + response = self.process_response(response) + request_finished.send(self, response=response) + except Exception: + if not from_error_handler: + raise + self.logger.exception('Request finalizing failed with an ' + 'error while handling an error') return response def try_trigger_before_first_request_functions(self): @@ -1673,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) @@ -1962,14 +2048,17 @@ 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 - response = self.make_response(self.handle_exception(e)) + response = self.handle_exception(e) + except: + error = sys.exc_info()[1] + raise return response(environ, start_response) finally: if self.should_ignore_error(error): diff --git a/flask/blueprints.py b/flask/blueprints.py index c0d47476..57d77512 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 app's :class:`~flask.app.Flask.json_encoder`. + json_encoder = None + #: Blueprint local JSON decoder class to use. + #: 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, static_url_path=None, template_folder=None, url_prefix=None, subdomain=None, url_defaults=None, @@ -407,7 +414,7 @@ class Blueprint(_PackageBoundObject): application-wide function of the :class:`~flask.Flask` object but for error handlers limited to this blueprint. - .. versionadded:: 1.0 + .. versionadded:: 0.11 """ self.record_once(lambda s: s.app._register_error_handler( self.name, code_or_exception, f)) diff --git a/flask/cli.py b/flask/cli.py index 360ce12e..0cc240a2 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -11,13 +11,15 @@ import os import sys +import traceback from threading import Lock, Thread from functools import update_wrapper import click from ._compat import iteritems, reraise - +from .helpers import get_debug_flag +from . import __version__ class NoAppException(click.UsageError): """Raised if an application cannot be found or loaded.""" @@ -54,10 +56,10 @@ def prepare_exec_for_file(filename): module = [] # Chop off file extensions or package markers - if filename.endswith('.py'): - filename = filename[:-3] - elif os.path.split(filename)[1] == '__init__.py': + if os.path.split(filename)[1] == '__init__.py': filename = os.path.dirname(filename) + elif filename.endswith('.py'): + filename = filename[:-3] else: raise NoAppException('The file provided (%s) does exist but is not a ' 'valid Python file. This means that it cannot ' @@ -85,7 +87,21 @@ def locate_app(app_id): module = app_id app_obj = None - __import__(module) + try: + __import__(module) + except ImportError: + # Reraise the ImportError if it occurred within the imported module. + # Determine this by checking whether the trace has a depth > 1. + if sys.exc_info()[-1].tb_next: + stack_trace = traceback.format_exc() + raise NoAppException('There was an error trying to import' + ' the app (%s):\n%s' % (module, stack_trace)) + else: + raise NoAppException('The file/path provided (%s) does not appear' + ' to exist. Please verify the path is ' + 'correct. If app is not on PYTHONPATH, ' + 'ensure the extension is .py' % module) + mod = sys.modules[module] if app_obj is None: app = find_best_app(mod) @@ -98,10 +114,35 @@ def locate_app(app_id): return app +def find_default_import_path(): + app = os.environ.get('FLASK_APP') + if app is None: + return + if os.path.isfile(app): + return prepare_exec_for_file(app) + return app + + +def get_version(ctx, param, value): + if not value or ctx.resilient_parsing: + return + message = 'Flask %(version)s\nPython %(python_version)s' + click.echo(message % { + 'version': __version__, + 'python_version': sys.version, + }, color=ctx.color) + ctx.exit() + +version_option = click.Option(['--version'], + help='Show the flask version', + expose_value=False, + callback=get_version, + is_flag=True, is_eager=True) + class DispatchingApp(object): - """Special application that dispatches to a flask application which + """Special application that dispatches to a Flask application which is imported by name in a background thread. If an error happens - it is is recorded and shows as part of the WSGI handling which in case + it is recorded and shown as part of the WSGI handling which in case of the Werkzeug debugger means that it shows up in the browser. """ @@ -155,15 +196,22 @@ class DispatchingApp(object): class ScriptInfo(object): """Help object to deal with Flask applications. This is usually not necessary to interface with as it's used internally in the dispatching - to click. + to click. In future versions of Flask this object will most likely play + a bigger role. Typically it's created automatically by the + :class:`FlaskGroup` but you can also manually create it and pass it + onwards as click object. """ - def __init__(self, app_import_path=None, debug=None, create_app=None): - #: The application import path + def __init__(self, app_import_path=None, create_app=None): + if create_app is None: + if app_import_path is None: + app_import_path = find_default_import_path() + self.app_import_path = app_import_path + else: + app_import_path = None + + #: Optionally the import path for the Flask application. self.app_import_path = app_import_path - #: The debug flag. If this is not None, the application will - #: automatically have it's debug flag overridden with this value. - self.debug = debug #: Optionally a function that is passed the script info to create #: the instance of the application. self.create_app = create_app @@ -183,13 +231,16 @@ class ScriptInfo(object): if self.create_app is not None: rv = self.create_app(self) else: - if self.app_import_path is None: - raise NoAppException('Could not locate Flask application. ' - 'You did not provide FLASK_APP or the ' - '--app parameter.') + if not self.app_import_path: + raise NoAppException( + 'Could not locate Flask application. You did not provide ' + 'the FLASK_APP environment variable.\n\nFor more ' + 'information see ' + 'http://flask.pocoo.org/docs/latest/quickstart/') rv = locate_app(self.app_import_path) - if self.debug is not None: - rv.debug = self.debug + debug = get_debug_flag() + if debug is not None: + rv.debug = debug self._loaded_app = rv return rv @@ -210,29 +261,6 @@ def with_appcontext(f): return update_wrapper(decorator, f) -def set_debug_value(ctx, param, value): - ctx.ensure_object(ScriptInfo).debug = value - - -def set_app_value(ctx, param, value): - if value is not None: - if os.path.isfile(value): - value = prepare_exec_for_file(value) - elif '.' not in sys.path: - sys.path.insert(0, '.') - ctx.ensure_object(ScriptInfo).app_import_path = value - - -debug_option = click.Option(['--debug/--no-debug'], - help='Enable or disable debug mode.', - default=None, callback=set_debug_value) - - -app_option = click.Option(['-a', '--app'], - help='The application to run', - callback=set_app_value, is_eager=True) - - class AppGroup(click.Group): """This works similar to a regular click :class:`~click.Group` but it changes the behavior of the :meth:`command` decorator so that it @@ -273,23 +301,17 @@ class FlaskGroup(AppGroup): :param add_default_commands: if this is True then the default run and shell commands wil be added. - :param add_app_option: adds the default :option:`--app` option. This gets - automatically disabled if a `create_app` - callback is defined. - :param add_debug_option: adds the default :option:`--debug` option. + :param add_version_option: adds the ``--version`` option. :param create_app: an optional callback that is passed the script info and returns the loaded app. """ - def __init__(self, add_default_commands=True, add_app_option=None, - add_debug_option=True, create_app=None, **extra): + def __init__(self, add_default_commands=True, create_app=None, + add_version_option=True, **extra): params = list(extra.pop('params', None) or ()) - if add_app_option is None: - add_app_option = create_app is None - if add_app_option: - params.append(app_option) - if add_debug_option: - params.append(debug_option) + + if add_version_option: + params.append(version_option) AppGroup.__init__(self, params=params, **extra) self.create_app = create_app @@ -298,7 +320,24 @@ class FlaskGroup(AppGroup): self.add_command(run_command) self.add_command(shell_command) + self._loaded_plugin_commands = False + + def _load_plugin_commands(self): + if self._loaded_plugin_commands: + return + try: + import pkg_resources + except ImportError: + self._loaded_plugin_commands = True + return + + for ep in pkg_resources.iter_entry_points('flask.commands'): + self.add_command(ep.load(), ep.name) + self._loaded_plugin_commands = True + def get_command(self, ctx, name): + self._load_plugin_commands() + # We load built-in commands first as these should always be the # same no matter what the app does. If the app does want to # override this it needs to make a custom instance of this group @@ -319,6 +358,8 @@ class FlaskGroup(AppGroup): pass def list_commands(self, ctx): + self._load_plugin_commands() + # The commands available is the list of both the application (if # available) plus the builtin commands. rv = set(click.Group.list_commands(self, ctx)) @@ -330,7 +371,9 @@ class FlaskGroup(AppGroup): # want the help page to break if the app does not exist. # If someone attempts to use the command we try to create # the app again and this will give us the error. - pass + # However, we will not do so silently because that would confuse + # users. + traceback.print_exc() return sorted(rv) def main(self, *args, **kwargs): @@ -342,33 +385,6 @@ class FlaskGroup(AppGroup): return AppGroup.main(self, *args, **kwargs) -def script_info_option(*args, **kwargs): - """This decorator works exactly like :func:`click.option` but is eager - by default and stores the value in the :attr:`ScriptInfo.data`. This - is useful to further customize an application factory in very complex - situations. - - :param script_info_key: this is a mandatory keyword argument which - defines under which data key the value should - be stored. - """ - try: - key = kwargs.pop('script_info_key') - except LookupError: - raise TypeError('script_info_key not provided.') - - real_callback = kwargs.get('callback') - def callback(ctx, param, value): - if real_callback is not None: - value = real_callback(ctx, value) - ctx.ensure_object(ScriptInfo).data[key] = value - return value - - kwargs['callback'] = callback - kwargs.setdefault('is_eager', True) - return click.option(*args, **kwargs) - - @click.command('run', short_help='Runs a development server.') @click.option('--host', '-h', default='127.0.0.1', help='The interface to bind to.') @@ -400,16 +416,25 @@ def run_command(info, host, port, reload, debugger, eager_loading, Flask is enabled and disabled otherwise. """ from werkzeug.serving import run_simple + + # Set a global flag that indicates that we were invoked from the + # command line interface provided server command. This is detected + # by Flask.run to make the call into a no-op. This is necessary to + # avoid ugly errors when the script that is loaded here also attempts + # to start a server. + os.environ['FLASK_RUN_FROM_CLI_SERVER'] = '1' + + debug = get_debug_flag() if reload is None: - reload = info.debug + reload = bool(debug) if debugger is None: - debugger = info.debug + debugger = bool(debug) if eager_loading is None: eager_loading = not reload app = DispatchingApp(info.load_app, use_eager_loading=eager_loading) - # Extra startup messages. This depends a but on Werkzeug internals to + # Extra startup messages. This depends a bit on Werkzeug internals to # not double execute when the reloader kicks in. if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': # If we have an import path we can print it out now which can help @@ -418,8 +443,8 @@ def run_command(info, host, port, reload, debugger, eager_loading, # we won't print anything. if info.app_import_path is not None: print(' * Serving Flask app "%s"' % info.app_import_path) - if info.debug is not None: - print(' * Forcing debug %s' % (info.debug and 'on' or 'off')) + if debug is not None: + print(' * Forcing debug mode %s' % (debug and 'on' or 'off')) run_simple(host, port, app, use_reloader=reload, use_debugger=debugger, threaded=with_threads) @@ -462,16 +487,22 @@ def shell_command(): cli = FlaskGroup(help="""\ This shell command acts as general utility script for Flask applications. -It loads the application configured (either through the FLASK_APP environment -variable or the --app parameter) and then provides commands either provided -by the application or Flask itself. +It loads the application configured (through the FLASK_APP environment +variable) and then provides commands either provided by the application or +Flask itself. The most useful commands are the "run" and "shell" command. Example usage: - flask --app=hello --debug run -""") +\b + %(prefix)s%(cmd)s FLASK_APP=hello.py + %(prefix)s%(cmd)s FLASK_DEBUG=1 + %(prefix)sflask run +""" % { + 'cmd': os.name == 'posix' and 'export' or 'set', + 'prefix': os.name == 'posix' and '$ ' or '', +}) def main(as_module=False): diff --git a/flask/config.py b/flask/config.py index d2eeb837..697add71 100644 --- a/flask/config.py +++ b/flask/config.py @@ -126,7 +126,7 @@ class Config(dict): d = types.ModuleType('config') d.__file__ = filename try: - with open(filename) as config_file: + with open(filename, mode='rb') as config_file: exec(compile(config_file.read(), filename, 'exec'), d.__dict__) except IOError as e: if silent and e.errno in (errno.ENOENT, errno.EISDIR): @@ -143,10 +143,12 @@ class Config(dict): - a string: in this case the object with that name will be imported - an actual object reference: that object is used directly - Objects are usually either modules or classes. + Objects are usually either modules or classes. :meth:`from_object` + loads only the uppercase attributes of the module/class. A ``dict`` + object will not work with :meth:`from_object` because the keys of a + ``dict`` are not attributes of the ``dict`` class. - Just the uppercase variables in that object are stored in the config. - Example usage:: + Example of module-based configuration:: app.config.from_object('yourapplication.default_config') from yourapplication import default_config @@ -157,6 +159,9 @@ class Config(dict): with :meth:`from_pyfile` and ideally from a location not within the package because the package might be installed system wide. + See :ref:`config-dev-prod` for an example of class-based configuration + using :meth:`from_object`. + :param obj: an import name or object """ if isinstance(obj, string_types): @@ -176,7 +181,7 @@ class Config(dict): :param silent: set to ``True`` if you want silent failure for missing files. - .. versionadded:: 1.0 + .. versionadded:: 0.11 """ filename = os.path.join(self.root_path, filename) @@ -194,7 +199,7 @@ class Config(dict): """Updates the config like :meth:`update` ignoring items with non-upper keys. - .. versionadded:: 1.0 + .. versionadded:: 0.11 """ mappings = [] if len(mapping) == 1: @@ -222,7 +227,7 @@ class Config(dict): app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' image_store_config = app.config.get_namespace('IMAGE_STORE_') - The resulting dictionary `image_store` would look like:: + The resulting dictionary `image_store_config` would look like:: { 'type': 'fs', @@ -239,7 +244,7 @@ class Config(dict): :param trim_namespace: a flag indicating if the keys of the resulting dictionary should not include the namespace - .. versionadded:: 1.0 + .. versionadded:: 0.11 """ rv = {} for k, v in iteritems(self): diff --git a/flask/ctx.py b/flask/ctx.py index 1e01df78..480d9c5c 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -36,7 +36,7 @@ class _AppCtxGlobals(object): return self.__dict__.pop(name, default) def setdefault(self, name, default=None): - self.__dict__.setdefault(name, default) + return self.__dict__.setdefault(name, default) def __contains__(self, item): return item in self.__dict__ @@ -181,12 +181,14 @@ class AppContext(object): def pop(self, exc=_sentinel): """Pops the app context.""" - self._refcnt -= 1 - if self._refcnt <= 0: - if exc is _sentinel: - exc = sys.exc_info()[1] - self.app.do_teardown_appcontext(exc) - rv = _app_ctx_stack.pop() + try: + self._refcnt -= 1 + if self._refcnt <= 0: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) + finally: + rv = _app_ctx_stack.pop() assert rv is self, 'Popped wrong app context. (%r instead of %r)' \ % (rv, self) appcontext_popped.send(self.app) @@ -341,38 +343,40 @@ class RequestContext(object): """ app_ctx = self._implicit_app_ctx_stack.pop() - clear_request = False - if not self._implicit_app_ctx_stack: - self.preserved = False - self._preserved_exc = None - if exc is _sentinel: - exc = sys.exc_info()[1] - self.app.do_teardown_request(exc) - - # If this interpreter supports clearing the exception information - # we do that now. This will only go into effect on Python 2.x, - # on 3.x it disappears automatically at the end of the exception - # stack. - if hasattr(sys, 'exc_clear'): - sys.exc_clear() - - request_close = getattr(self.request, 'close', None) - if request_close is not None: - request_close() - clear_request = True - - rv = _request_ctx_stack.pop() - assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ - % (rv, self) - - # get rid of circular dependencies at the end of the request - # so that we don't require the GC to be active. - if clear_request: - rv.request.environ['werkzeug.request'] = None - - # Get rid of the app as well if necessary. - if app_ctx is not None: - app_ctx.pop(exc) + try: + clear_request = False + if not self._implicit_app_ctx_stack: + self.preserved = False + self._preserved_exc = None + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) + + # If this interpreter supports clearing the exception information + # we do that now. This will only go into effect on Python 2.x, + # on 3.x it disappears automatically at the end of the exception + # stack. + if hasattr(sys, 'exc_clear'): + sys.exc_clear() + + request_close = getattr(self.request, 'close', None) + if request_close is not None: + request_close() + clear_request = True + finally: + rv = _request_ctx_stack.pop() + + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + if clear_request: + rv.request.environ['werkzeug.request'] = None + + # Get rid of the app as well if necessary. + if app_ctx is not None: + app_ctx.pop(exc) + + assert rv is self, 'Popped wrong request context. ' \ + '(%r instead of %r)' % (rv, self) def auto_pop(self, exc): if self.request.environ.get('flask._preserve_context') or \ diff --git a/flask/debughelpers.py b/flask/debughelpers.py index 90710dd3..9e44fe69 100644 --- a/flask/debughelpers.py +++ b/flask/debughelpers.py @@ -8,6 +8,9 @@ :copyright: (c) 2015 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +import os +from warnings import warn + from ._compat import implements_to_string, text_type from .app import Flask from .blueprints import Blueprint @@ -153,3 +156,12 @@ def explain_template_loading_attempts(app, template, attempts): info.append(' See http://flask.pocoo.org/docs/blueprints/#templates') app.logger.info('\n'.join(info)) + + +def explain_ignored_app_run(): + if os.environ.get('WERKZEUG_RUN_MAIN') != 'true': + warn(Warning('Silently ignoring app.run() because the ' + 'application is run from the flask command line ' + 'executable. Consider putting app.run() behind an ' + 'if __name__ == "__main__" guard to silence this ' + 'warning.'), stacklevel=3) diff --git a/flask/exthook.py b/flask/exthook.py index 05ac4e35..d8842802 100644 --- a/flask/exthook.py +++ b/flask/exthook.py @@ -21,9 +21,16 @@ """ import sys import os +import warnings from ._compat import reraise +class ExtDeprecationWarning(DeprecationWarning): + pass + +warnings.simplefilter('always', ExtDeprecationWarning) + + class ExtensionImporter(object): """This importer redirects imports from this submodule to other locations. This makes it possible to transition from the old flaskext.name to the @@ -49,13 +56,21 @@ class ExtensionImporter(object): sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] def find_module(self, fullname, path=None): - if fullname.startswith(self.prefix): + if fullname.startswith(self.prefix) and \ + fullname != 'flask.ext.ExtDeprecationWarning': return self def load_module(self, fullname): if fullname in sys.modules: return sys.modules[fullname] + modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] + + warnings.warn( + "Importing flask.ext.{x} is deprecated, use flask_{x} instead." + .format(x=modname), ExtDeprecationWarning, stacklevel=2 + ) + for path in self.module_choices: realname = path % modname try: @@ -83,6 +98,14 @@ class ExtensionImporter(object): module = sys.modules[fullname] = sys.modules[realname] if '.' not in modname: setattr(sys.modules[self.wrapper_module], modname, module) + + if realname.startswith('flaskext.'): + warnings.warn( + "Detected extension named flaskext.{x}, please rename it " + "to flask_{x}. The old form is deprecated." + .format(x=modname), ExtDeprecationWarning + ) + return module raise ImportError('No module named %s' % fullname) diff --git a/flask/helpers.py b/flask/helpers.py index 479ca4bb..828f5840 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -14,10 +14,10 @@ import sys import pkgutil import posixpath import mimetypes -from datetime import timedelta from time import time from zlib import adler32 from threading import RLock +import unicodedata from werkzeug.routing import BuildError from functools import update_wrapper @@ -26,8 +26,9 @@ try: except ImportError: from urlparse import quote as url_quote -from werkzeug.datastructures import Headers -from werkzeug.exceptions import NotFound +from werkzeug.datastructures import Headers, Range +from werkzeug.exceptions import BadRequest, NotFound, \ + RequestedRangeNotSatisfiable # this was moved in 0.7 try: @@ -54,6 +55,13 @@ _os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] if sep not in (None, '/')) +def get_debug_flag(default=None): + val = os.environ.get('FLASK_DEBUG') + if not val: + return default + return val.lower() not in ('0', 'false', 'no') + + def _endpoint_from_view_func(view_func): """Internal helper that returns the default endpoint for a given function. This always is the function name. @@ -101,7 +109,7 @@ def stream_with_context(generator_or_function): gen = iter(generator_or_function) except TypeError: def decorator(*args, **kwargs): - gen = generator_or_function() + gen = generator_or_function(*args, **kwargs) return stream_with_context(gen) return update_wrapper(decorator, generator_or_function) @@ -300,20 +308,30 @@ def url_for(endpoint, **values): scheme = values.pop('_scheme', None) appctx.app.inject_url_defaults(endpoint, values) + # This is not the best way to deal with this but currently the + # underlying Werkzeug router does not support overriding the scheme on + # a per build call basis. + old_scheme = None if scheme is not None: if not external: raise ValueError('When specifying _scheme, _external must be True') + old_scheme = url_adapter.url_scheme url_adapter.url_scheme = scheme try: - rv = url_adapter.build(endpoint, values, method=method, - force_external=external) + try: + rv = url_adapter.build(endpoint, values, method=method, + force_external=external) + finally: + if old_scheme is not None: + url_adapter.url_scheme = old_scheme except BuildError as error: # We need to inject the values again so that the app callback can # deal with that sort of stuff. 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: @@ -414,7 +432,7 @@ def get_flashed_messages(with_categories=False, category_filter=[]): def send_file(filename_or_fp, mimetype=None, as_attachment=False, attachment_filename=None, add_etags=True, - cache_timeout=None, conditional=False): + cache_timeout=None, conditional=False, last_modified=None): """Sends the contents of a file to the client. This will use the most efficient method available and configured. By default it will try to use the WSGI server's file_wrapper support. Alternatively @@ -428,6 +446,13 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, guessing requires a `filename` or an `attachment_filename` to be provided. + ETags will also be attached automatically if a `filename` is provided. You + can turn this off by setting `add_etags=False`. + + If `conditional=True` and `filename` is provided, this method will try to + upgrade the response stream to support range requests. This will allow + the request to be answered with partial content response. + Please never pass filenames to this function from user sources; you should use :func:`send_from_directory` instead. @@ -446,7 +471,21 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. versionchanged:: 0.9 cache_timeout pulls its default from application config, when None. - :param filename_or_fp: the filename of the file to send in `latin-1`. + .. versionchanged:: 0.12 + The filename is no longer automatically inferred from file objects. If + you want to use automatic mimetype and etag support, pass a filepath via + `filename_or_fp` or `attachment_filename`. + + .. 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. This is relative to the :attr:`~Flask.root_path` if a relative path is specified. Alternatively a file object might be provided in @@ -454,8 +493,9 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, back to the traditional method. Make sure that the file pointer is positioned at the start of data to send before calling :func:`send_file`. - :param mimetype: the mimetype of the file if provided, otherwise - auto detection happens. + :param mimetype: the mimetype of the file if provided. If a file path is + given, auto detection happens as fallback, otherwise an + error will be raised. :param as_attachment: set to ``True`` if you want to send this file with a ``Content-Disposition: attachment`` header. :param attachment_filename: the filename for the attachment if it @@ -467,69 +507,76 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, (default), this value is set by :meth:`~Flask.get_send_file_max_age` of :data:`~flask.current_app`. + :param last_modified: set the ``Last-Modified`` header to this value, + a :class:`~datetime.datetime` or timestamp. + If a file was passed, this overrides its mtime. """ mtime = None + fsize = None if isinstance(filename_or_fp, string_types): filename = filename_or_fp + if not os.path.isabs(filename): + filename = os.path.join(current_app.root_path, filename) file = None + if attachment_filename is None: + attachment_filename = os.path.basename(filename) else: - from warnings import warn file = filename_or_fp - filename = getattr(file, 'name', None) - - # XXX: this behavior is now deprecated because it was unreliable. - # removed in Flask 1.0 - if not attachment_filename and not mimetype \ - and isinstance(filename, string_types): - warn(DeprecationWarning('The filename support for file objects ' - 'passed to send_file is now deprecated. Pass an ' - 'attach_filename if you want mimetypes to be guessed.'), - stacklevel=2) - if add_etags: - warn(DeprecationWarning('In future flask releases etags will no ' - 'longer be generated for file objects passed to the send_file ' - 'function because this behavior was unreliable. Pass ' - 'filenames instead if possible, otherwise attach an etag ' - 'yourself based on another value'), stacklevel=2) - - if filename is not None: - if not os.path.isabs(filename): - filename = os.path.join(current_app.root_path, filename) - if mimetype is None and (filename or attachment_filename): - mimetype = mimetypes.guess_type(filename or attachment_filename)[0] + filename = None + if mimetype is None: - mimetype = 'application/octet-stream' + if attachment_filename is not None: + mimetype = mimetypes.guess_type(attachment_filename)[0] \ + or 'application/octet-stream' + + if mimetype is None: + raise ValueError( + 'Unable to infer MIME-type because no filename is available. ' + 'Please set either `attachment_filename`, pass a filepath to ' + '`filename_or_fp` or set your own MIME-type via `mimetype`.' + ) headers = Headers() if as_attachment: if attachment_filename is None: - if filename is None: - raise TypeError('filename unavailable, required for ' - 'sending as attachment') - attachment_filename = os.path.basename(filename) - headers.add('Content-Disposition', 'attachment', - filename=attachment_filename) + raise TypeError('filename unavailable, required for ' + 'sending as attachment') + + try: + attachment_filename = attachment_filename.encode('latin-1') + except UnicodeEncodeError: + filenames = { + 'filename': unicodedata.normalize( + 'NFKD', attachment_filename).encode('latin-1', 'ignore'), + 'filename*': "UTF-8''%s" % url_quote(attachment_filename), + } + else: + filenames = {'filename': attachment_filename} + + headers.add('Content-Disposition', 'attachment', **filenames) if current_app.use_x_sendfile and filename: if file is not None: file.close() headers['X-Sendfile'] = filename - headers['Content-Length'] = os.path.getsize(filename) + fsize = os.path.getsize(filename) + headers['Content-Length'] = fsize data = None else: if file is None: file = open(filename, 'rb') mtime = os.path.getmtime(filename) - headers['Content-Length'] = os.path.getsize(filename) + fsize = os.path.getsize(filename) + headers['Content-Length'] = fsize data = wrap_file(request.environ, file) rv = current_app.response_class(data, mimetype=mimetype, headers=headers, direct_passthrough=True) - # if we know the file modification date, we can store it as - # the time of the last modification. - if mtime is not None: - rv.last_modified = int(mtime) + if last_modified is not None: + rv.last_modified = last_modified + elif mtime is not None: + rv.last_modified = mtime rv.cache_control.public = True if cache_timeout is None: @@ -539,8 +586,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, rv.expires = int(time() + cache_timeout) if add_etags and filename is not None: + from warnings import warn + try: - rv.set_etag('flask-%s-%s-%s' % ( + rv.set_etag('%s-%s-%s' % ( os.path.getmtime(filename), os.path.getsize(filename), adler32( @@ -552,17 +601,28 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, warn('Access %s failed, maybe it does not exist, so ignore etags in ' 'headers' % filename, stacklevel=2) - if conditional: + if conditional: + if callable(getattr(Range, 'to_content_range_header', None)): + # Werkzeug supports Range Requests + # Remove this test when support for Werkzeug <0.12 is dropped + try: + rv = rv.make_conditional(request, accept_ranges=True, + complete_length=fsize) + except RequestedRangeNotSatisfiable: + file.close() + raise + else: rv = rv.make_conditional(request) - # make sure we don't send x-sendfile for servers that - # ignore the 304 status code for x-sendfile. - if rv.status_code == 304: - rv.headers.pop('x-sendfile', None) + # make sure we don't send x-sendfile for servers that + # ignore the 304 status code for x-sendfile. + if rv.status_code == 304: + rv.headers.pop('x-sendfile', None) return rv -def safe_join(directory, filename): - """Safely join `directory` and `filename`. +def safe_join(directory, *pathnames): + """Safely join `directory` and zero or more untrusted `pathnames` + components. Example usage:: @@ -572,20 +632,23 @@ def safe_join(directory, filename): with open(filename, 'rb') as fd: content = fd.read() # Read and process the file content... - :param directory: the base directory. - :param filename: the untrusted filename relative to that directory. - :raises: :class:`~werkzeug.exceptions.NotFound` if the resulting path - would fall out of `directory`. + :param directory: the trusted base directory. + :param pathnames: the untrusted pathnames relative to that directory. + :raises: :class:`~werkzeug.exceptions.NotFound` if one or more passed + paths fall out of its boundaries. """ - filename = posixpath.normpath(filename) - for sep in _os_alt_seps: - if sep in filename: + 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('../'): raise NotFound() - if os.path.isabs(filename) or \ - filename == '..' or \ - filename.startswith('../'): - raise NotFound() - return os.path.join(directory, filename) + directory = os.path.join(directory, filename) + return directory def send_from_directory(directory, filename, **options): @@ -618,8 +681,11 @@ def send_from_directory(directory, filename, **options): filename = safe_join(directory, filename) if not os.path.isabs(filename): filename = os.path.join(current_app.root_path, filename) - if not os.path.isfile(filename): - raise NotFound() + try: + if not os.path.isfile(filename): + raise NotFound() + except (TypeError, ValueError): + raise BadRequest() options.setdefault('conditional', True) return send_file(filename, **options) diff --git a/flask/json.py b/flask/json.py index 885214d3..bf8a8843 100644 --- a/flask/json.py +++ b/flask/json.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """ - flask.jsonimpl - ~~~~~~~~~~~~~~ + flask.json + ~~~~~~~~~~ Implementation helpers for the JSON support in Flask. @@ -13,16 +13,14 @@ 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 # Use the same json implementation as itsdangerous on which we # depend anyways. -try: - from itsdangerous import simplejson as _json -except ImportError: - from itsdangerous import json as _json +from itsdangerous import json as _json # Figure out if simplejson escapes slashes. This behavior was changed @@ -94,9 +92,16 @@ 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) 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) @@ -106,7 +111,12 @@ 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) 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) @@ -195,14 +205,26 @@ def htmlsafe_dumps(obj, **kwargs): def htmlsafe_dump(obj, fp, **kwargs): """Like :func:`htmlsafe_dumps` but writes into a file object.""" - fp.write(unicode(htmlsafe_dumps(obj, **kwargs))) + fp.write(text_type(htmlsafe_dumps(obj, **kwargs))) def jsonify(*args, **kwargs): - """Creates a :class:`~flask.Response` with the JSON representation of - the given arguments with an :mimetype:`application/json` mimetype. The - arguments to this function are the same as to the :class:`dict` - constructor. + """This function wraps :func:`dumps` to add a few enhancements that make + life easier. It turns the JSON output into a :class:`~flask.Response` + object with the :mimetype:`application/json` mimetype. For convenience, it + also converts multiple arguments into an array or multiple keyword arguments + into a dict. This means that both ``jsonify(1,2,3)`` and + ``jsonify([1,2,3])`` serialize to ``[1,2,3]``. + + For clarity, the JSON serialization behavior has the following differences + from :func:`dumps`: + + 1. Single argument: Passed straight through to :func:`dumps`. + 2. Multiple arguments: Converted to an array before being passed to + :func:`dumps`. + 3. Multiple keyword arguments: Converted to a dict before being passed to + :func:`dumps`. + 4. Both args and kwargs: Behavior undefined and will throw an exception. Example usage:: @@ -222,14 +244,15 @@ def jsonify(*args, **kwargs): "id": 42 } - For security reasons only objects are supported toplevel. For more - information about this, have a look at :ref:`json-security`. - 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. + .. versionchanged:: 0.11 + 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 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 """ @@ -237,18 +260,21 @@ def jsonify(*args, **kwargs): indent = None separators = (',', ':') - if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] \ - and not request.is_xhr: + if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] or current_app.debug: indent = 2 separators = (', ', ': ') - # Note that we add '\n' to end of response - # (see https://github.com/mitsuhiko/flask/pull/1262) - rv = current_app.response_class( - (dumps(dict(*args, **kwargs), indent=indent, separators=separators), - '\n'), - mimetype='application/json') - return rv + if args and kwargs: + raise TypeError('jsonify() behavior undefined when passed both args and kwargs') + elif len(args) == 1: # single args are passed directly to dumps() + data = args[0] + else: + data = args or kwargs + + return current_app.response_class( + (dumps(data, indent=indent, separators=separators), '\n'), + mimetype=current_app.config['JSONIFY_MIMETYPE'] + ) def tojson_filter(obj, **kwargs): diff --git a/flask/logging.py b/flask/logging.py index 5a1f149c..3f888a75 100644 --- a/flask/logging.py +++ b/flask/logging.py @@ -87,4 +87,8 @@ def create_logger(app): logger.__class__ = DebugLogger logger.addHandler(debug_handler) logger.addHandler(prod_handler) + + # Disable propagation by default + logger.propagate = False + return logger diff --git a/flask/sessions.py b/flask/sessions.py index 48fd08fa..525ff246 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -84,21 +84,25 @@ class TaggedJSONSerializer(object): def dumps(self, value): return json.dumps(_tag(value), separators=(',', ':')) + LOADS_MAP = { + ' t': tuple, + ' u': uuid.UUID, + ' b': b64decode, + ' m': Markup, + ' d': parse_date, + } + def loads(self, value): def object_hook(obj): if len(obj) != 1: return obj the_key, the_value = next(iteritems(obj)) - if the_key == ' t': - return tuple(the_value) - elif the_key == ' u': - return uuid.UUID(the_value) - elif the_key == ' b': - return b64decode(the_value) - elif the_key == ' m': - return Markup(the_value) - elif the_key == ' d': - return parse_date(the_value) + # Check the key for a corresponding function + return_function = self.LOADS_MAP.get(the_key) + if return_function: + # Pass the value to the function + return return_function(the_value) + # Didn't find a function for this object return obj return json.loads(value, object_hook=object_hook) @@ -168,7 +172,7 @@ class SessionInterface(object): null_session_class = NullSession #: A flag that indicates if the session interface is pickle based. - #: This can be used by flask extensions to make a decision in regards + #: This can be used by Flask extensions to make a decision in regards #: to how to deal with the session object. #: #: .. versionadded:: 0.10 @@ -263,7 +267,7 @@ class SessionInterface(object): This check is usually skipped if sessions get deleted. - .. versionadded:: 1.0 + .. versionadded:: 0.11 """ if session.modified: return True diff --git a/flask/signals.py b/flask/signals.py index c9b8a210..dd52cdb5 100644 --- a/flask/signals.py +++ b/flask/signals.py @@ -37,7 +37,7 @@ except ImportError: temporarily_connected_to = connected_to = _fail del _fail -# The namespace for code signals. If you are not flask code, do +# The namespace for code signals. If you are not Flask code, do # not put signals in here. Create your own namespace instead. _signals = Namespace() diff --git a/flask/templating.py b/flask/templating.py index 8c95a6a7..2da4926d 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -52,27 +52,36 @@ class DispatchingJinjaLoader(BaseLoader): self.app = app def get_source(self, environment, template): - explain = self.app.config['EXPLAIN_TEMPLATE_LOADING'] + if self.app.config['EXPLAIN_TEMPLATE_LOADING']: + return self._get_source_explained(environment, template) + return self._get_source_fast(environment, template) + + def _get_source_explained(self, environment, template): attempts = [] - tmplrv = None + trv = None for srcobj, loader in self._iter_loaders(template): try: rv = loader.get_source(environment, template) - if tmplrv is None: - tmplrv = rv - if not explain: - break + if trv is None: + trv = rv except TemplateNotFound: rv = None attempts.append((loader, srcobj, rv)) - if explain: - from .debughelpers import explain_template_loading_attempts - explain_template_loading_attempts(self.app, template, attempts) + from .debughelpers import explain_template_loading_attempts + explain_template_loading_attempts(self.app, template, attempts) + + if trv is not None: + return trv + raise TemplateNotFound(template) - if tmplrv is not None: - return tmplrv + def _get_source_fast(self, environment, template): + for srcobj, loader in self._iter_loaders(template): + try: + return loader.get_source(environment, template) + except TemplateNotFound: + continue raise TemplateNotFound(template) def _iter_loaders(self, template): diff --git a/flask/testing.py b/flask/testing.py index 54ce20ef..31600245 100644 --- a/flask/testing.py +++ b/flask/testing.py @@ -10,6 +10,7 @@ :license: BSD, see LICENSE for more details. """ +import werkzeug from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack @@ -39,19 +40,32 @@ def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): class FlaskClient(Client): """Works like a regular Werkzeug test client but has some knowledge about how Flask works to defer the cleanup of the request context stack to the - end of a with body when used in a with statement. For general information - about how to use this class refer to :class:`werkzeug.test.Client`. + end of a ``with`` body when used in a ``with`` statement. For general + information about how to use this class refer to + :class:`werkzeug.test.Client`. + + .. versionchanged:: 0.12 + `app.test_client()` includes preset default environment, which can be + set after instantiation of the `app.test_client()` object in + `client.environ_base`. Basic usage is outlined in the :ref:`testing` chapter. """ preserve_context = False + def __init__(self, *args, **kwargs): + super(FlaskClient, self).__init__(*args, **kwargs) + self.environ_base = { + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": "werkzeug/" + werkzeug.__version__ + } + @contextmanager def session_transaction(self, *args, **kwargs): - """When used in combination with a with statement this opens a + """When used in combination with a ``with`` statement this opens a session transaction. This can be used to modify the session that - the test client uses. Once the with block is left the session is + the test client uses. Once the ``with`` block is left the session is stored back. :: @@ -100,6 +114,7 @@ class FlaskClient(Client): def open(self, *args, **kwargs): kwargs.setdefault('environ_overrides', {}) \ ['flask._preserve_context'] = self.preserve_context + kwargs.setdefault('environ_base', self.environ_base) as_tuple = kwargs.pop('as_tuple', False) buffered = kwargs.pop('buffered', False) diff --git a/flask/views.py b/flask/views.py index 9bd6b615..848ccb0b 100644 --- a/flask/views.py +++ b/flask/views.py @@ -48,7 +48,7 @@ class View(object): generated view function! """ - #: A for which methods this pluggable view can handle. + #: A list of methods this view can handle. methods = None #: The canonical way to decorate class-based views is to decorate the @@ -103,33 +103,34 @@ class View(object): 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(rv.methods or []) - 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 you will response 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) @@ -139,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) diff --git a/flask/wrappers.py b/flask/wrappers.py index 8b1ca251..04bdcb5d 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -113,7 +113,7 @@ class Request(RequestBase): is considered to include JSON data if the mimetype is :mimetype:`application/json` or :mimetype:`application/*+json`. - .. versionadded:: 1.0 + .. versionadded:: 0.11 """ mt = self.mimetype if mt == 'application/json': @@ -123,11 +123,12 @@ class Request(RequestBase): return False def get_json(self, force=False, silent=False, cache=True): - """Parses the incoming JSON request data and returns it. If - parsing fails the :meth:`on_json_loading_failed` method on the - request object will be invoked. By default this function will - only load the json data if the mimetype is :mimetype:`application/json` - but this can be overridden by the `force` parameter. + """Parses the incoming JSON request data and returns it. By default + this function will return ``None`` if the mimetype is not + :mimetype:`application/json` but this can be overridden by the + ``force`` parameter. If parsing fails the + :meth:`on_json_loading_failed` method on the request object will be + invoked. :param force: if set to ``True`` the mimetype is ignored. :param silent: if set to ``True`` this method will fail silently @@ -136,7 +137,8 @@ class Request(RequestBase): on the request. """ rv = getattr(self, '_cached_json', _missing) - if rv is not _missing: + # We return cached JSON only when the cache is enabled. + if cache and rv is not _missing: return rv if not (force or self.is_json): diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 7cab94e1..18e1a14b 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -5,7 +5,7 @@ ~~~~~~~~~~~~~~~~ This command line script scans a whole application tree and attempts to - output an unified diff with all the changes that are necessary to easily + output a unified diff with all the changes that are necessary to easily upgrade the application to 0.7 and to not yield deprecation warnings. This will also attempt to find `after_request` functions that don't modify @@ -19,6 +19,7 @@ :copyright: (c) Copyright 2015 by Armin Ronacher. :license: see LICENSE for more details. """ +from __future__ import print_function import re import os import inspect @@ -59,7 +60,7 @@ def make_diff(filename, old, new): posixpath.normpath(posixpath.join('a', filename)), posixpath.normpath(posixpath.join('b', filename)), lineterm=''): - print line + print(line) def looks_like_teardown_function(node): diff --git a/scripts/flaskext_migrate.py b/scripts/flaskext_migrate.py deleted file mode 100644 index 4d937007..00000000 --- a/scripts/flaskext_migrate.py +++ /dev/null @@ -1,162 +0,0 @@ -# Script which modifies source code away from the deprecated "flask.ext" -# format. -# -# Run in the terminal by typing: `python flaskext_migrate.py ` -# -# Author: Keyan Pishdadian 2015 - -from redbaron import RedBaron -import sys - - -def read_source(input_file): - """Parses the input_file into a RedBaron FST.""" - with open(input_file, "r") as source_code: - red = RedBaron(source_code.read()) - return red - - -def write_source(red, input_file): - """Overwrites the input_file once the FST has been modified.""" - with open(input_file, "w") as source_code: - source_code.write(red.dumps()) - - -def fix_imports(red): - """Wrapper which fixes "from" style imports and then "import" style.""" - red = fix_standard_imports(red) - red = fix_from_imports(red) - return red - - -def fix_from_imports(red): - """ - Converts "from" style imports to not use "flask.ext". - - Handles: - Case 1: from flask.ext.foo import bam --> from flask_foo import bam - Case 2: from flask.ext import foo --> import flask_foo as foo - """ - from_imports = red.find_all("FromImport") - for x, node in enumerate(from_imports): - values = node.value - if len(values) < 2: - continue - if (values[0].value == 'flask') and (values[1].value == 'ext'): - # Case 1 - if len(node.value) == 3: - package = values[2].value - modules = node.modules() - module_string = _get_modules(modules) - if len(modules) > 1: - node.replace("from flask_%s import %s" - % (package, module_string)) - else: - name = node.names()[0] - node.replace("from flask_%s import %s as %s" - % (package, module_string, name)) - # Case 2 - else: - module = node.modules()[0] - node.replace("import flask_%s as %s" - % (module, module)) - return red - - -def fix_standard_imports(red): - """ - Handles import modification in the form: - import flask.ext.foo" --> import flask_foo - """ - imports = red.find_all("ImportNode") - for x, node in enumerate(imports): - try: - if (node.value[0].value[0].value == 'flask' and - node.value[0].value[1].value == 'ext'): - package = node.value[0].value[2].value - name = node.names()[0].split('.')[-1] - if name == package: - node.replace("import flask_%s" % (package)) - else: - node.replace("import flask_%s as %s" % (package, name)) - except IndexError: - pass - - return red - - -def _get_modules(module): - """ - Takes a list of modules and converts into a string. - - The module list can include parens, this function checks each element in - the list, if there is a paren then it does not add a comma before the next - element. Otherwise a comma and space is added. This is to preserve module - imports which are multi-line and/or occur within parens. While also not - affecting imports which are not enclosed. - """ - modules_string = [cur + ', ' if cur.isalnum() and next.isalnum() - else cur - for (cur, next) in zip(module, module[1:]+[''])] - - return ''.join(modules_string) - - -def fix_function_calls(red): - """ - Modifies function calls in the source to reflect import changes. - - Searches the AST for AtomtrailerNodes and replaces them. - """ - atoms = red.find_all("Atomtrailers") - for x, node in enumerate(atoms): - try: - if (node.value[0].value == 'flask' and - node.value[1].value == 'ext'): - params = _form_function_call(node) - node.replace("flask_%s%s" % (node.value[2], params)) - except IndexError: - pass - - return red - - -def _form_function_call(node): - """ - Reconstructs function call strings when making attribute access calls. - """ - node_vals = node.value - output = "." - for x, param in enumerate(node_vals[3::]): - if param.dumps()[0] == "(": - output = output[0:-1] + param.dumps() - return output - else: - output += param.dumps() + "." - - -def check_user_input(): - """Exits and gives error message if no argument is passed in the shell.""" - if len(sys.argv) < 2: - sys.exit("No filename was included, please try again.") - - -def fix_tester(ast): - """Wrapper which allows for testing when not running from shell.""" - ast = fix_imports(ast) - ast = fix_function_calls(ast) - return ast.dumps() - - -def fix(): - """Wrapper for user argument checking and import fixing.""" - check_user_input() - input_file = sys.argv[1] - ast = read_source(input_file) - ast = fix_imports(ast) - ast = fix_function_calls(ast) - write_source(ast, input_file) - - -if __name__ == "__main__": - fix() diff --git a/scripts/flaskext_tester.py b/scripts/flaskext_tester.py index 45adb0a4..93ab0ad7 100644 --- a/scripts/flaskext_tester.py +++ b/scripts/flaskext_tester.py @@ -112,7 +112,7 @@ RESULT_TEMPATE = u'''\ def log(msg, *args): - print '[EXTTEST]', msg % args + print('[EXTTEST] ' + (msg % args)) class TestResult(object): @@ -302,7 +302,7 @@ def main(): if args.browse: import webbrowser webbrowser.open('file:///' + filename.lstrip('/')) - print 'Results written to', filename + print('Results written to {}'.format(filename)) if __name__ == '__main__': diff --git a/scripts/make-release.py b/scripts/make-release.py index 4a74cb59..fc6421ab 100644 --- a/scripts/make-release.py +++ b/scripts/make-release.py @@ -10,6 +10,7 @@ :copyright: (c) 2015 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from __future__ import print_function import sys import os import re @@ -80,22 +81,17 @@ def set_init_version(version): set_filename_version('flask/__init__.py', version, '__version__') -def set_setup_version(version): - info('Setting setup.py version to %s', version) - set_filename_version('setup.py', version, 'version') - - def build_and_upload(): - Popen([sys.executable, 'setup.py', 'release', 'sdist', 'upload']).wait() + Popen([sys.executable, 'setup.py', 'release', 'sdist', 'bdist_wheel', 'upload']).wait() def fail(message, *args): - print >> sys.stderr, 'Error:', message % args + print('Error:', message % args, file=sys.stderr) sys.exit(1) def info(message, *args): - print >> sys.stderr, message % args + print(message % args, file=sys.stderr) def get_git_tags(): @@ -140,12 +136,10 @@ def main(): fail('You have uncommitted changes in git') set_init_version(version) - set_setup_version(version) make_git_commit('Bump version number to %s', version) make_git_tag(version) build_and_upload() set_init_version(dev_version) - set_setup_version(dev_version) if __name__ == '__main__': diff --git a/scripts/test_import_migration.py b/scripts/test_import_migration.py deleted file mode 100644 index f000f2b1..00000000 --- a/scripts/test_import_migration.py +++ /dev/null @@ -1,76 +0,0 @@ -# Tester for the flaskext_migrate.py module located in flask/scripts/ -# -# Author: Keyan Pishdadian -import pytest -redbaron = pytest.importorskip("redbaron") -import flaskext_migrate as migrate - -def test_simple_from_import(): - red = redbaron.RedBaron("from flask.ext import foo") - output = migrate.fix_tester(red) - assert output == "import flask_foo as foo" - - -def test_from_to_from_import(): - red = redbaron.RedBaron("from flask.ext.foo import bar") - output = migrate.fix_tester(red) - assert output == "from flask_foo import bar as bar" - - -def test_multiple_import(): - red = redbaron.RedBaron("from flask.ext.foo import bar, foobar, something") - output = migrate.fix_tester(red) - assert output == "from flask_foo import bar, foobar, something" - - -def test_multiline_import(): - red = redbaron.RedBaron("from flask.ext.foo import \ - bar,\ - foobar,\ - something") - output = migrate.fix_tester(red) - assert output == "from flask_foo import bar, foobar, something" - - -def test_module_import(): - red = redbaron.RedBaron("import flask.ext.foo") - output = migrate.fix_tester(red) - assert output == "import flask_foo" - - -def test_named_module_import(): - red = redbaron.RedBaron("import flask.ext.foo as foobar") - output = migrate.fix_tester(red) - assert output == "import flask_foo as foobar" - - -def test_named_from_import(): - red = redbaron.RedBaron("from flask.ext.foo import bar as baz") - output = migrate.fix_tester(red) - assert output == "from flask_foo import bar as baz" - - -def test_parens_import(): - red = redbaron.RedBaron("from flask.ext.foo import (bar, foo, foobar)") - output = migrate.fix_tester(red) - assert output == "from flask_foo import (bar, foo, foobar)" - - -def test_function_call_migration(): - red = redbaron.RedBaron("flask.ext.foo(var)") - output = migrate.fix_tester(red) - assert output == "flask_foo(var)" - - -def test_nested_function_call_migration(): - red = redbaron.RedBaron("import flask.ext.foo\n\n" - "flask.ext.foo.bar(var)") - output = migrate.fix_tester(red) - assert output == ("import flask_foo\n\n" - "flask_foo.bar(var)") - - -def test_no_change_to_import(): - red = redbaron.RedBaron("from flask import Flask") - output = migrate.fix_tester(red) - assert output == "from flask import Flask" diff --git a/setup.cfg b/setup.cfg index 69feced4..781de592 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,8 +1,11 @@ [aliases] release = egg_info -RDb '' -[wheel] +[bdist_wheel] universal = 1 -[pytest] +[metadata] +license_file = LICENSE + +[tool:pytest] norecursedirs = .* *.egg *.egg-info env* artwork docs diff --git a/setup.py b/setup.py index 490ac02e..08995073 100644 --- a/setup.py +++ b/setup.py @@ -33,13 +33,15 @@ And run it: $ python hello.py * Running on http://localhost:5000/ + Ready for production? `Read this first `. + Links ````` * `website `_ * `documentation `_ * `development version - `_ + `_ """ import re @@ -57,7 +59,7 @@ with open('flask/__init__.py', 'rb') as f: setup( name='Flask', version=version, - url='http://github.com/mitsuhiko/flask/', + url='https://github.com/pallets/flask/', license='BSD', author='Armin Ronacher', author_email='armin.ronacher@active-4.com', diff --git a/test-requirements.txt b/test-requirements.txt new file mode 100644 index 00000000..053148f8 --- /dev/null +++ b/test-requirements.txt @@ -0,0 +1 @@ +tox diff --git a/tests/conftest.py b/tests/conftest.py index 7a209c22..8c9541de 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,6 +7,7 @@ :license: BSD, see LICENSE for more details. """ import flask +import gc import os import sys import pkgutil @@ -126,8 +127,8 @@ def purge_module(request): return inner -@pytest.fixture -def catch_deprecation_warnings(): - import warnings - warnings.simplefilter('default', category=DeprecationWarning) - return lambda: warnings.catch_warnings(record=True) +@pytest.yield_fixture(autouse=True) +def catch_deprecation_warnings(recwarn): + yield + gc.collect() + assert not recwarn.list diff --git a/tests/test_appctx.py b/tests/test_appctx.py index f24704ef..13b61eee 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -146,3 +146,25 @@ def test_context_refcounts(): assert res.status_code == 200 assert res.data == b'' assert called == ['request', 'app'] + + +def test_clean_pop(): + called = [] + app = flask.Flask(__name__) + + @app.teardown_request + def teardown_req(error=None): + 1 / 0 + + @app.teardown_appcontext + def teardown_app(error=None): + called.append('TEARDOWN') + + try: + with app.test_request_context(): + called.append(flask.current_app.name) + except ZeroDivisionError: + pass + + assert called == ['test_appctx', 'TEARDOWN'] + assert not flask.current_app diff --git a/tests/test_apps/cliapp/__init__.py b/tests/test_apps/cliapp/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/test_apps/cliapp/app.py b/tests/test_apps/cliapp/app.py new file mode 100644 index 00000000..f142bc47 --- /dev/null +++ b/tests/test_apps/cliapp/app.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import, print_function + +from flask import Flask + +testapp = Flask('testapp') diff --git a/tests/test_apps/cliapp/importerrorapp.py b/tests/test_apps/cliapp/importerrorapp.py new file mode 100644 index 00000000..fb87c9b1 --- /dev/null +++ b/tests/test_apps/cliapp/importerrorapp.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import, print_function + +from flask import Flask + +raise ImportError() + +testapp = Flask('testapp') diff --git a/tests/test_apps/cliapp/multiapp.py b/tests/test_apps/cliapp/multiapp.py new file mode 100644 index 00000000..67ed6fba --- /dev/null +++ b/tests/test_apps/cliapp/multiapp.py @@ -0,0 +1,6 @@ +from __future__ import absolute_import, print_function + +from flask import Flask + +app1 = Flask('app1') +app2 = Flask('app2') diff --git a/tests/test_basic.py b/tests/test_basic.py index d22e9e5b..163b83cf 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__) @@ -307,12 +355,8 @@ def test_missing_session(): app = flask.Flask(__name__) def expect_exception(f, *args, **kwargs): - try: - f(*args, **kwargs) - except RuntimeError as e: - assert e.args and 'session is unavailable' in e.args[0] - else: - assert False, 'expected exception' + 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) @@ -337,7 +381,7 @@ def test_session_expiration(): client = app.test_client() rv = client.get('/') assert 'set-cookie' in rv.headers - match = re.search(r'\bexpires=([^;]+)(?i)', rv.headers['set-cookie']) + match = re.search(r'(?i)\bexpires=([^;]+)', rv.headers['set-cookie']) expires = parse_date(match.group()) expected = datetime.utcnow() + app.permanent_session_lifetime assert expires.year == expected.year @@ -772,6 +816,46 @@ def test_error_handling(): assert b'forbidden' == rv.data +def test_error_handling_processing(): + app = flask.Flask(__name__) + app.config['LOGGER_HANDLER_POLICY'] = 'never' + + @app.errorhandler(500) + def internal_server_error(e): + return 'internal server error', 500 + + @app.route('/') + def broken_func(): + 1 // 0 + + @app.after_request + def after_request(resp): + 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' + + +def test_baseexception_error_handling(): + app = flask.Flask(__name__) + app.config['LOGGER_HANDLER_POLICY'] = 'never' + + @app.route('/') + def broken_func(): + raise KeyboardInterrupt() + + with app.test_client() as c: + with pytest.raises(KeyboardInterrupt): + c.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__) @@ -853,12 +937,9 @@ def test_trapping_of_bad_request_key_errors(): app.config['TRAP_BAD_REQUEST_ERRORS'] = True c = app.test_client() - try: - c.get('/fail') - except KeyError as e: - assert isinstance(e, BadRequest) - else: - assert False, 'Expected exception' + with pytest.raises(KeyError) as e: + c.get("/fail") + assert e.errisinstance(BadRequest) def test_trapping_of_all_http_exceptions(): @@ -888,73 +969,135 @@ def test_enctype_debug_helper(): # stack otherwise and we want to ensure that this is not the case # to not negatively affect other tests. with app.test_client() as c: - try: + with pytest.raises(DebugFilesKeyError) as e: c.post('/fail', data={'foo': 'index.txt'}) - except DebugFilesKeyError as e: - assert 'no file contents were transmitted' in str(e) - assert 'This was submitted: "index.txt"' in str(e) - else: - assert False, 'Expected exception' + assert 'no file contents were transmitted' in str(e.value) + 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(): @@ -982,7 +1125,7 @@ def test_make_response_with_response_instance(): rv = flask.make_response( flask.jsonify({'msg': 'W00t'}), 400) assert rv.status_code == 400 - assert rv.data == b'{\n "msg": "W00t"\n}\n' + assert rv.data == b'{"msg":"W00t"}\n' assert rv.mimetype == 'application/json' rv = flask.make_response( @@ -1029,6 +1172,26 @@ def test_jsonify_prettyprint(): assert rv.data == pretty_response +def test_jsonify_mimetype(): + app = flask.Flask(__name__) + app.config.update({"JSONIFY_MIMETYPE": 'application/vnd.api+json'}) + with app.test_request_context(): + msg = { + "msg": {"submsg": "W00t"}, + } + rv = flask.make_response( + flask.jsonify(msg), 200) + assert rv.mimetype == 'application/vnd.api+json' + + +def test_jsonify_args_and_kwargs_check(): + app = flask.Flask(__name__) + with app.test_request_context(): + with pytest.raises(TypeError) as e: + flask.jsonify('fake args', kwargs='fake') + assert 'behavior undefined' in str(e.value) + + def test_url_generation(): app = flask.Flask(__name__) @@ -1081,6 +1244,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 @@ -1114,20 +1294,47 @@ def test_static_files(): rv.close() -def test_none_response(): - app = flask.Flask(__name__) +def test_static_path_deprecated(recwarn): + app = flask.Flask(__name__, static_path='/foo') + recwarn.pop(DeprecationWarning) + app.testing = True + rv = app.test_client().get('/foo/index.html') + assert rv.status_code == 200 + rv.close() - @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" + with app.test_request_context(): + assert flask.url_for('static', filename='index.html') == '/foo/index.html' + + +def test_static_url_path(): + app = flask.Flask(__name__, static_url_path='/foo') + app.testing = True + rv = app.test_client().get('/foo/index.html') + assert rv.status_code == 200 + rv.close() + + with app.test_request_context(): + 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_request_locals(): @@ -1203,12 +1410,8 @@ def test_exception_propagation(): c = app.test_client() if config_key is not None: app.config[config_key] = True - try: + with pytest.raises(Exception): c.get('/') - except Exception: - pass - else: - assert False, 'expected exception' else: assert c.get('/').status_code == 500 @@ -1222,6 +1425,24 @@ def test_exception_propagation(): t.join() +@pytest.mark.parametrize('debug', [True, False]) +@pytest.mark.parametrize('use_debugger', [True, False]) +@pytest.mark.parametrize('use_reloader', [True, False]) +@pytest.mark.parametrize('propagate_exceptions', [None, True, False]) +def test_werkzeug_passthrough_errors(monkeypatch, debug, use_debugger, + use_reloader, propagate_exceptions): + rv = {} + + # Mocks werkzeug.serving.run_simple method + def run_simple_mock(*args, **kwargs): + rv['passthrough_errors'] = kwargs.get('passthrough_errors') + + app = flask.Flask(__name__) + monkeypatch.setattr(werkzeug.serving, 'run_simple', run_simple_mock) + app.config['PROPAGATE_EXCEPTIONS'] = propagate_exceptions + app.run(debug=debug, use_debugger=use_debugger, use_reloader=use_reloader) + + def test_max_content_length(): app = flask.Flask(__name__) app.config['MAX_CONTENT_LENGTH'] = 64 @@ -1325,14 +1546,11 @@ def test_debug_mode_complains_after_first_request(): return 'Awesome' assert not app.got_first_request assert app.test_client().get('/').data == b'Awesome' - try: + with pytest.raises(AssertionError) as e: @app.route('/foo') def broken(): return 'Meh' - except AssertionError as e: - assert 'A setup function was called' in str(e) - else: - assert False, 'Expected exception' + assert 'A setup function was called' in str(e) app.debug = False @@ -1388,14 +1606,11 @@ def test_routing_redirect_debugging(): def foo(): return 'success' with app.test_client() as c: - try: + with pytest.raises(AssertionError) as e: c.post('/foo', data={}) - except AssertionError as e: - assert 'http://localhost/foo/' in str(e) - assert ('Make sure to directly send ' - 'your POST-request to this URL') in str(e) - else: - assert False, 'Expected exception' + assert 'http://localhost/foo/' in str(e) + assert ('Make sure to directly send ' + 'your POST-request to this URL') in str(e) rv = c.get('/foo', data={}, follow_redirects=True) assert rv.data == b'success' @@ -1616,3 +1831,20 @@ 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) + + +@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), +)) +def test_run_from_config(monkeypatch, host, port, expect_host, expect_port): + 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 7e22bc7f..de293e7f 100644 --- a/tests/test_blueprints.py +++ b/tests/test_blueprints.py @@ -174,12 +174,9 @@ def test_templates_and_static(test_apps): assert flask.url_for('admin.static', filename='test.txt') == '/admin/static/test.txt' with app.test_request_context(): - try: + with pytest.raises(TemplateNotFound) as e: flask.render_template('missing.html') - except TemplateNotFound as e: - assert e.name == 'missing.html' - else: - assert 0, 'expected exception' + assert e.value.name == 'missing.html' with flask.Flask(__name__).test_request_context(): assert flask.render_template('nested/nested.txt') == 'I\'m nested' @@ -358,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() diff --git a/tests/test_cli.py b/tests/test_cli.py new file mode 100644 index 00000000..8b291a63 --- /dev/null +++ b/tests/test_cli.py @@ -0,0 +1,210 @@ +# -*- coding: utf-8 -*- +""" + tests.test_cli + ~~~~~~~~~~~~~~ + + :copyright: (c) 2016 by the Flask Team, see AUTHORS for more details. + :license: BSD, see LICENSE for more details. +""" +# +# This file was part of Flask-CLI and was modified under the terms its license, +# the Revised BSD License. +# Copyright (C) 2015 CERN. +# +from __future__ import absolute_import, print_function +import os +import sys + +import click +import pytest +from click.testing import CliRunner +from flask import Flask, current_app + +from flask.cli import AppGroup, FlaskGroup, NoAppException, ScriptInfo, \ + find_best_app, locate_app, with_appcontext, prepare_exec_for_file, \ + find_default_import_path, get_version + + +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 + assert testapp.cli.name == testapp.name + + +def test_find_best_app(test_apps): + """Test if `find_best_app` behaves as expected with different combinations of input.""" + class Module: + app = Flask('appname') + assert find_best_app(Module) == Module.app + + class Module: + application = Flask('appname') + assert find_best_app(Module) == Module.application + + class Module: + myapp = Flask('appname') + assert find_best_app(Module) == Module.myapp + + class Module: + pass + pytest.raises(NoAppException, find_best_app, Module) + + class Module: + myapp1 = Flask('appname1') + myapp2 = 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. + + :func:`prepare_exec_for_file` has a side effect, where + the parent directory of given file is added to `sys.path`. + """ + realpath = os.path.realpath('/tmp/share/test.py') + dirname = os.path.dirname(realpath) + assert prepare_exec_for_file('/tmp/share/test.py') == 'test' + assert dirname in sys.path + + realpath = os.path.realpath('/tmp/share/__init__.py') + dirname = os.path.dirname(os.path.dirname(realpath)) + assert prepare_exec_for_file('/tmp/share/__init__.py') == 'share' + assert dirname in sys.path + + with pytest.raises(NoAppException): + prepare_exec_for_file('/tmp/share/test.txt') + + +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") + + +def test_find_default_import_path(test_apps, monkeypatch, tmpdir): + """Test of find_default_import_path.""" + monkeypatch.delitem(os.environ, 'FLASK_APP', raising=False) + assert find_default_import_path() == None + monkeypatch.setitem(os.environ, 'FLASK_APP', 'notanapp') + assert find_default_import_path() == 'notanapp' + tmpfile = tmpdir.join('testapp.py') + tmpfile.write('') + monkeypatch.setitem(os.environ, 'FLASK_APP', str(tmpfile)) + expect_rv = prepare_exec_for_file(str(tmpfile)) + assert find_default_import_path() == expect_rv + + +def test_get_version(test_apps, capsys): + """Test of get_version.""" + from flask import __version__ as flask_ver + from sys import version as py_ver + class MockCtx(object): + resilient_parsing = False + color = None + def exit(self): return + ctx = MockCtx() + get_version(ctx, None, "test") + out, err = capsys.readouterr() + assert flask_ver in out + assert py_ver in out + + +def test_scriptinfo(test_apps): + """Test of ScriptInfo.""" + obj = ScriptInfo(app_import_path="cliapp.app:testapp") + assert obj.load_app().name == "testapp" + assert obj.load_app().name == "testapp" + + def create_app(info): + return Flask("createapp") + + obj = ScriptInfo(create_app=create_app) + app = obj.load_app() + assert app.name == "createapp" + assert obj.load_app() == app + + +def test_with_appcontext(): + """Test of with_appcontext.""" + @click.command() + @with_appcontext + def testcmd(): + click.echo(current_app.name) + + 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(): + """Test of with_appcontext.""" + @click.group(cls=AppGroup) + def cli(): + pass + + @cli.command(with_appcontext=True) + def test(): + click.echo(current_app.name) + + @cli.group() + def subgroup(): + pass + + @subgroup.command(with_appcontext=True) + def test2(): + click.echo(current_app.name) + + 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' + + result = runner.invoke(cli, ['subgroup', 'test2'], obj=obj) + assert result.exit_code == 0 + assert result.output == 'testappgroup\n' + + +def test_flaskgroup(): + """Test FlaskGroup.""" + def create_app(info): + return Flask("flaskgroup") + + @click.group(cls=FlaskGroup, create_app=create_app) + def cli(**params): + pass + + @cli.command() + def test(): + click.echo(current_app.name) + + runner = CliRunner() + result = runner.invoke(cli, ['test']) + assert result.exit_code == 0 + assert result.output == 'flaskgroup\n' + + +def test_print_exceptions(): + """Print the stacktrace if the CLI.""" + def create_app(info): + raise Exception("oh no") + return Flask("flaskgroup") + + @click.group(cls=FlaskGroup, create_app=create_app) + def cli(**params): + pass + + runner = CliRunner() + result = runner.invoke(cli, ['--help']) + assert result.exit_code == 0 + assert 'Exception: oh no' in result.output + assert 'Traceback' in result.output diff --git a/tests/test_config.py b/tests/test_config.py index 7a17b607..5c98db98 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,11 +7,14 @@ :license: BSD, see LICENSE for more details. """ -import pytest -import os from datetime import timedelta +import os +import textwrap + import flask +from flask._compat import PY2 +import pytest # config keys used for the TestConfig @@ -89,12 +92,9 @@ def test_config_from_envvar(): try: os.environ = {} app = flask.Flask(__name__) - try: + with pytest.raises(RuntimeError) as e: app.config.from_envvar('FOO_SETTINGS') - except RuntimeError as e: - assert "'FOO_SETTINGS' is not set" in str(e) - else: - assert 0, 'expected exception' + assert "'FOO_SETTINGS' is not set" in str(e.value) assert not app.config.from_envvar('FOO_SETTINGS', silent=True) os.environ = {'FOO_SETTINGS': __file__.rsplit('.', 1)[0] + '.py'} @@ -108,16 +108,13 @@ def test_config_from_envvar_missing(): env = os.environ try: os.environ = {'FOO_SETTINGS': 'missing.cfg'} - try: + with pytest.raises(IOError) as e: app = flask.Flask(__name__) app.config.from_envvar('FOO_SETTINGS') - except IOError as e: - msg = str(e) - assert msg.startswith('[Errno 2] Unable to load configuration ' - 'file (No such file or directory):') - assert msg.endswith("missing.cfg'") - else: - assert False, 'expected IOError' + msg = str(e.value) + assert msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):') + assert msg.endswith("missing.cfg'") assert not app.config.from_envvar('FOO_SETTINGS', silent=True) finally: os.environ = env @@ -125,29 +122,23 @@ def test_config_from_envvar_missing(): def test_config_missing(): app = flask.Flask(__name__) - try: + with pytest.raises(IOError) as e: app.config.from_pyfile('missing.cfg') - except IOError as e: - msg = str(e) - assert msg.startswith('[Errno 2] Unable to load configuration ' - 'file (No such file or directory):') - assert msg.endswith("missing.cfg'") - else: - assert 0, 'expected config' + msg = str(e.value) + assert msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):') + assert msg.endswith("missing.cfg'") assert not app.config.from_pyfile('missing.cfg', silent=True) def test_config_missing_json(): app = flask.Flask(__name__) - try: + with pytest.raises(IOError) as e: app.config.from_json('missing.json') - except IOError as e: - msg = str(e) - assert msg.startswith('[Errno 2] Unable to load configuration ' - 'file (No such file or directory):') - assert msg.endswith("missing.json'") - else: - assert 0, 'expected config' + msg = str(e.value) + assert msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):') + assert msg.endswith("missing.json'") assert not app.config.from_json('missing.json', silent=True) @@ -199,3 +190,18 @@ def test_get_namespace(): assert 2 == len(bar_options) assert 'bar stuff 1' == bar_options['BAR_STUFF_1'] assert 'bar stuff 2' == bar_options['BAR_STUFF_2'] + + +@pytest.mark.parametrize('encoding', ['utf-8', 'iso-8859-15', 'latin-1']) +def test_from_pyfile_weird_encoding(tmpdir, encoding): + f = tmpdir.join('my_config.py') + f.write_binary(textwrap.dedent(u''' + # -*- coding: {0} -*- + TEST_VALUE = "föö" + '''.format(encoding)).encode(encoding)) + app = flask.Flask(__name__) + app.config.from_pyfile(str(f)) + value = app.config['TEST_VALUE'] + if PY2: + value = value.decode(encoding) + assert value == u'föö' diff --git a/tests/test_deprecations.py b/tests/test_deprecations.py index 757aacd0..666f7d56 100644 --- a/tests/test_deprecations.py +++ b/tests/test_deprecations.py @@ -16,7 +16,7 @@ import flask class TestRequestDeprecation(object): - def test_request_json(self, catch_deprecation_warnings): + def test_request_json(self, recwarn): """Request.json is deprecated""" app = flask.Flask(__name__) app.testing = True @@ -27,13 +27,11 @@ class TestRequestDeprecation(object): print(flask.request.json) return 'OK' - with catch_deprecation_warnings() as captured: - c = app.test_client() - c.post('/', data='{"spam": 42}', content_type='application/json') + c = app.test_client() + c.post('/', data='{"spam": 42}', content_type='application/json') + recwarn.pop(DeprecationWarning) - assert len(captured) == 1 - - def test_request_module(self, catch_deprecation_warnings): + def test_request_module(self, recwarn): """Request.module is deprecated""" app = flask.Flask(__name__) app.testing = True @@ -43,8 +41,6 @@ class TestRequestDeprecation(object): assert flask.request.module is None return 'OK' - with catch_deprecation_warnings() as captured: - c = app.test_client() - c.get('/') - - assert len(captured) == 1 + c = app.test_client() + c.get('/') + recwarn.pop(DeprecationWarning) diff --git a/tests/test_ext.py b/tests/test_ext.py index f5728312..ebb5f02d 100644 --- a/tests/test_ext.py +++ b/tests/test_ext.py @@ -20,6 +20,18 @@ except ImportError: from flask._compat import PY2 +@pytest.fixture(autouse=True) +def disable_extwarnings(request, recwarn): + from flask.exthook import ExtDeprecationWarning + + def inner(): + assert set(w.category for w in recwarn.list) \ + <= set([ExtDeprecationWarning]) + recwarn.clear() + + request.addfinalizer(inner) + + @pytest.fixture(autouse=True) def importhook_setup(monkeypatch, request): # we clear this out for various reasons. The most important one is @@ -167,8 +179,8 @@ def test_flaskext_broken_package_no_module_caching(flaskext_broken): def test_no_error_swallowing(flaskext_broken): with pytest.raises(ImportError) as excinfo: import flask.ext.broken - - assert excinfo.type is ImportError + # python3.6 raises a subclass of ImportError: 'ModuleNotFoundError' + assert issubclass(excinfo.type, ImportError) if PY2: message = 'No module named missing_module' else: diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 8d09327f..325713c0 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -12,9 +12,13 @@ import pytest import os +import uuid import datetime + import flask from logging import StreamHandler +from werkzeug.datastructures import Range +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 @@ -31,23 +35,13 @@ def has_encoding(name): class TestJSON(object): - def test_jsonify_date_types(self): - """Test jsonify with datetime.date and datetime.datetime types.""" - - test_dates = ( - datetime.datetime(1973, 3, 11, 6, 30, 45), - datetime.date(1975, 1, 5) - ) - + def test_ignore_cached_json(self): 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) - assert rv.mimetype == 'application/json' - assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple()) + 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__) @@ -103,8 +97,49 @@ class TestJSON(object): content_type='application/json; charset=iso-8859-15') assert resp.data == u'Hällo Wörld'.encode('utf-8') - def test_jsonify(self): - d = dict(a=23, b=42, c=[1, 2, 3]) + def test_json_as_unicode(self): + app = flask.Flask(__name__) + + 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'] = 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__) + 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 + + @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): + """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) + assert rv.mimetype == 'application/json' + assert flask.json.loads(rv.data) == test_value + + def test_jsonify_dicts(self): + """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__) @app.route('/kw') def return_kwargs(): @@ -118,18 +153,57 @@ class TestJSON(object): assert rv.mimetype == 'application/json' assert flask.json.loads(rv.data) == d - def test_json_as_unicode(self): + def test_jsonify_arrays(self): + """Test jsonify of lists and args unpacking.""" + l = [ + 0, 42, 3.14, 't', 'hello', True, False, + ['test list', 2, False], + {'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) + assert rv.mimetype == 'application/json' + assert flask.json.loads(rv.data) == l - app.config['JSON_AS_ASCII'] = True - with app.app_context(): - rv = flask.json.dumps(u'\N{SNOWMAN}') - assert rv == '"\\u2603"' + def test_jsonify_date_types(self): + """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() - app.config['JSON_AS_ASCII'] = False - with app.app_context(): - rv = flask.json.dumps(u'\N{SNOWMAN}') - assert rv == u'"\u2603"' + 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) + assert rv.mimetype == 'application/json' + assert flask.json.loads(rv.data)['x'] == http_date(d.timetuple()) + + def test_jsonify_uuid_types(self): + """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_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__) @@ -193,6 +267,47 @@ 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 + + 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(bp) + + 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' @@ -215,6 +330,8 @@ class TestJSON(object): def test_json_key_sorting(self): app = flask.Flask(__name__) app.testing = True + app.debug = True + assert app.config['JSON_SORT_KEYS'] == True d = dict.fromkeys(range(20), 'foo') @@ -296,7 +413,7 @@ class TestSendfile(object): assert rv.data == f.read() rv.close() - def test_send_file_xsendfile(self): + def test_send_file_xsendfile(self, catch_deprecation_warnings): app = flask.Flask(__name__) app.use_x_sendfile = True with app.test_request_context(): @@ -308,93 +425,167 @@ class TestSendfile(object): assert rv.mimetype == 'text/html' rv.close() - def test_send_file_object(self, catch_deprecation_warnings): + def test_send_file_last_modified(self): + app = flask.Flask(__name__) + last_modified = datetime.datetime(1999, 1, 1) + + @app.route('/') + def index(): + return flask.send_file(StringIO("party like it's"), + last_modified=last_modified, + mimetype='text/plain') + + c = app.test_client() + rv = c.get('/') + assert rv.last_modified == last_modified + + def test_send_file_object_without_mimetype(self): app = flask.Flask(__name__) - with catch_deprecation_warnings() as captured: - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html'), mode='rb') - rv = flask.send_file(f) + + with app.test_request_context(): + with pytest.raises(ValueError) as excinfo: + flask.send_file(StringIO("LOL")) + assert 'Unable to infer MIME-type' in str(excinfo) + assert 'no filename is available' in str(excinfo) + + with app.test_request_context(): + flask.send_file(StringIO("LOL"), attachment_filename='filename') + + def test_send_file_object(self): + app = flask.Flask(__name__) + + with app.test_request_context(): + with open(os.path.join(app.root_path, 'static/index.html'), mode='rb') as f: + rv = flask.send_file(f, mimetype='text/html') rv.direct_passthrough = False with app.open_resource('static/index.html') as f: assert rv.data == f.read() assert rv.mimetype == 'text/html' rv.close() - # mimetypes + etag - assert len(captured) == 2 app.use_x_sendfile = True - with catch_deprecation_warnings() as captured: - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f) + + with app.test_request_context(): + with open(os.path.join(app.root_path, 'static/index.html')) as f: + rv = flask.send_file(f, mimetype='text/html') assert rv.mimetype == 'text/html' - assert 'x-sendfile' in rv.headers - assert rv.headers['x-sendfile'] == \ - os.path.join(app.root_path, 'static/index.html') + assert 'x-sendfile' not in rv.headers rv.close() - # mimetypes + etag - assert len(captured) == 2 app.use_x_sendfile = False with app.test_request_context(): - with catch_deprecation_warnings() as captured: - f = StringIO('Test') - rv = flask.send_file(f) - rv.direct_passthrough = False - assert rv.data == b'Test' - assert rv.mimetype == 'application/octet-stream' - rv.close() - # etags - assert len(captured) == 1 - with catch_deprecation_warnings() as captured: - class PyStringIO(object): - def __init__(self, *args, **kwargs): - self._io = StringIO(*args, **kwargs) - def __getattr__(self, name): - return getattr(self._io, name) - f = PyStringIO('Test') - f.name = 'test.txt' - rv = flask.send_file(f) - rv.direct_passthrough = False - assert rv.data == b'Test' - assert rv.mimetype == 'text/plain' - rv.close() - # attachment_filename and etags - assert len(captured) == 3 - with catch_deprecation_warnings() as captured: - f = StringIO('Test') - rv = flask.send_file(f, mimetype='text/plain') - rv.direct_passthrough = False - assert rv.data == b'Test' - assert rv.mimetype == 'text/plain' - rv.close() - # etags - assert len(captured) == 1 + f = StringIO('Test') + rv = flask.send_file(f, mimetype='application/octet-stream') + rv.direct_passthrough = False + assert rv.data == b'Test' + assert rv.mimetype == 'application/octet-stream' + rv.close() + + class PyStringIO(object): + def __init__(self, *args, **kwargs): + self._io = StringIO(*args, **kwargs) + def __getattr__(self, name): + return getattr(self._io, name) + f = PyStringIO('Test') + f.name = 'test.txt' + rv = flask.send_file(f, attachment_filename=f.name) + rv.direct_passthrough = False + assert rv.data == b'Test' + assert rv.mimetype == 'text/plain' + rv.close() + + f = StringIO('Test') + rv = flask.send_file(f, mimetype='text/plain') + rv.direct_passthrough = False + assert rv.data == b'Test' + assert rv.mimetype == 'text/plain' + rv.close() app.use_x_sendfile = True - with catch_deprecation_warnings() as captured: - with app.test_request_context(): - f = StringIO('Test') - rv = flask.send_file(f) - assert 'x-sendfile' not in rv.headers - rv.close() - # etags - assert len(captured) == 1 - def test_attachment(self, catch_deprecation_warnings): + with app.test_request_context(): + f = StringIO('Test') + rv = flask.send_file(f, mimetype='text/html') + assert 'x-sendfile' not in rv.headers + rv.close() + + @pytest.mark.skipif( + not callable(getattr(Range, 'to_content_range_header', None)), + reason="not implement within werkzeug" + ) + def test_send_file_range_request(self): app = flask.Flask(__name__) - with catch_deprecation_warnings() as captured: - with app.test_request_context(): - f = open(os.path.join(app.root_path, 'static/index.html')) - rv = flask.send_file(f, as_attachment=True) - value, options = parse_options_header(rv.headers['Content-Disposition']) + + @app.route('/') + def index(): + return flask.send_file('static/index.html', conditional=True) + + c = app.test_client() + + rv = c.get('/', headers={'Range': 'bytes=4-15'}) + assert rv.status_code == 206 + with app.open_resource('static/index.html') as f: + assert rv.data == f.read()[4:16] + rv.close() + + rv = c.get('/', headers={'Range': 'bytes=4-'}) + assert rv.status_code == 206 + with app.open_resource('static/index.html') as f: + assert rv.data == f.read()[4:] + rv.close() + + rv = c.get('/', headers={'Range': 'bytes=4-1000'}) + assert rv.status_code == 206 + with app.open_resource('static/index.html') as f: + assert rv.data == f.read()[4:] + rv.close() + + rv = c.get('/', headers={'Range': 'bytes=-10'}) + assert rv.status_code == 206 + with app.open_resource('static/index.html') as f: + assert rv.data == f.read()[-10:] + rv.close() + + rv = c.get('/', headers={'Range': 'bytes=1000-'}) + assert rv.status_code == 416 + rv.close() + + rv = c.get('/', headers={'Range': 'bytes=-'}) + assert rv.status_code == 416 + rv.close() + + rv = c.get('/', headers={'Range': 'somethingsomething'}) + assert rv.status_code == 416 + rv.close() + + last_modified = datetime.datetime.utcfromtimestamp(os.path.getmtime( + os.path.join(app.root_path, 'static/index.html'))).replace( + microsecond=0) + + rv = c.get('/', headers={'Range': 'bytes=4-15', + 'If-Range': http_date(last_modified)}) + assert rv.status_code == 206 + rv.close() + + rv = c.get('/', headers={'Range': 'bytes=4-15', 'If-Range': http_date( + datetime.datetime(1999, 1, 1))}) + assert rv.status_code == 200 + rv.close() + + def test_attachment(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='index.html') + value, options = \ + parse_options_header(rv.headers['Content-Disposition']) assert value == 'attachment' + assert options['filename'] == 'index.html' + assert 'filename*' not in rv.headers['Content-Disposition'] rv.close() - # mimetypes + etag - assert len(captured) == 2 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' @@ -411,6 +602,19 @@ 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(): + 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() + def test_static_file(self): app = flask.Flask(__name__) # default cache timeout is 12 hours @@ -464,6 +668,14 @@ class TestSendfile(object): assert rv.data.strip() == b'Hello Subdomain' rv.close() + def test_send_from_directory_bad_request(self): + app = flask.Flask(__name__) + app.testing = True + app.root_path = os.path.join(os.path.dirname(__file__), + 'test_apps', 'subdomaintestmodule') + with app.test_request_context(): + with pytest.raises(BadRequest): + flask.send_from_directory('static', 'bad\x00') class TestLogging(object): @@ -578,6 +790,16 @@ class TestLogging(object): 'index', _scheme='https') + def test_url_for_with_alternating_schemes(self): + app = flask.Flask(__name__) + @app.route('/') + def index(): + return '42' + with app.test_request_context(): + assert flask.url_for('index', _external=True) == 'http://localhost/' + assert flask.url_for('index', _external=True, _scheme='https') == 'https://localhost/' + assert flask.url_for('index', _external=True) == 'http://localhost/' + def test_url_with_method(self): from flask.views import MethodView app = flask.Flask(__name__) @@ -643,11 +865,11 @@ class TestStreaming(object): @app.route('/') def index(): @flask.stream_with_context - def generate(): - yield 'Hello ' + def generate(hello): + yield hello yield flask.request.args['name'] yield '!' - return flask.Response(generate()) + return flask.Response(generate('Hello ')) c = app.test_client() rv = c.get('/?name=World') assert rv.data == b'Hello World!' @@ -678,3 +900,45 @@ class TestStreaming(object): rv = c.get('/?name=World') assert rv.data == b'Hello World!' assert called == [42] + + +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', ), + # If last path is '' add a slash + (('/a/b/c', '', ), '/a/b/c/', ), + # Preserve dot slash + (('/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'), + (('/..', ), '/..'), + ) + + for args, expected in passing: + assert flask.safe_join(*args) == expected + + def test_safe_join_exceptions(self): + # Should raise werkzeug.exceptions.NotFound on unsafe joins. + failing = ( + # path.isabs and ``..'' checks + ('/a', 'b', '/c'), + ('/a', '../b/c', ), + ('/a', '..', 'b/c'), + # Boundaries violations after path normalization + ('/a', 'b/../b/../../c', ), + ('/a', 'b', 'c/../..'), + ('/a', 'b/../../c', ), + ) + + for args in failing: + with pytest.raises(NotFound): + print(flask.safe_join(*args)) diff --git a/tests/test_regression.py b/tests/test_regression.py index 1497750d..ffeebf86 100644 --- a/tests/test_regression.py +++ b/tests/test_regression.py @@ -39,8 +39,7 @@ class assert_no_leak(object): self.old_objects = len(gc.get_objects()) def __exit__(self, exc_type, exc_value, tb): - if not hasattr(sys, 'getrefcount'): - gc.collect() + gc.collect() new_objects = len(gc.get_objects()) if new_objects > self.old_objects: pytest.fail('Example code leaked') diff --git a/tests/test_reqctx.py b/tests/test_reqctx.py index 558958dc..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 @@ -140,12 +141,8 @@ def test_manual_context_binding(): ctx.push() assert index() == 'Hello World!' ctx.pop() - try: + with pytest.raises(RuntimeError): index() - except RuntimeError: - pass - else: - assert 0, 'expected runtime error' @pytest.mark.skipif(greenlet is None, reason='greenlet not installed') def test_greenlet_context_copying(): @@ -197,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 diff --git a/tests/test_testing.py b/tests/test_testing.py index a9e06fab..9d353904 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -11,6 +11,7 @@ import pytest import flask +import werkzeug from flask._compat import text_type @@ -43,6 +44,40 @@ def test_environ_defaults(): rv = c.get('/') assert rv.data == b'http://localhost/' +def test_environ_base_default(): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + flask.g.user_agent = flask.request.headers["User-Agent"] + return flask.request.remote_addr + + with app.test_client() as c: + rv = c.get('/') + assert rv.data == b'127.0.0.1' + assert flask.g.user_agent == 'werkzeug/' + werkzeug.__version__ + +def test_environ_base_modified(): + app = flask.Flask(__name__) + app.testing = True + @app.route('/') + def index(): + flask.g.user_agent = flask.request.headers["User-Agent"] + return flask.request.remote_addr + + with app.test_client() as c: + c.environ_base['REMOTE_ADDR'] = '0.0.0.0' + c.environ_base['HTTP_USER_AGENT'] = 'Foo' + rv = c.get('/') + assert rv.data == b'0.0.0.0' + assert flask.g.user_agent == 'Foo' + + c.environ_base['REMOTE_ADDR'] = '0.0.0.1' + c.environ_base['HTTP_USER_AGENT'] = 'Bar' + rv = c.get('/') + assert rv.data == b'0.0.0.1' + assert flask.g.user_agent == 'Bar' + def test_redirect_keep_session(): app = flask.Flask(__name__) app.secret_key = 'testing' @@ -100,13 +135,10 @@ def test_session_transactions_no_null_sessions(): app.testing = True with app.test_client() as c: - try: + with pytest.raises(RuntimeError) as e: with c.session_transaction() as sess: pass - except RuntimeError as e: - assert 'Session backend did not open a session' in str(e) - else: - assert False, 'Expected runtime error' + assert 'Session backend did not open a session' in str(e.value) def test_session_transactions_keep_context(): app = flask.Flask(__name__) @@ -124,13 +156,10 @@ def test_session_transaction_needs_cookies(): app = flask.Flask(__name__) app.testing = True c = app.test_client(use_cookies=False) - try: + with pytest.raises(RuntimeError) as e: with c.session_transaction() as s: pass - except RuntimeError as e: - assert 'cookies' in str(e) - else: - assert False, 'Expected runtime error' + assert 'cookies' in str(e.value) def test_test_client_context_binding(): app = flask.Flask(__name__) 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'] diff --git a/tox.ini b/tox.ini index a138877d..764b4030 100644 --- a/tox.ini +++ b/tox.ini @@ -1,24 +1,32 @@ [tox] -envlist = {py26,py27,pypy}-{lowest,release,devel}, {py33,py34,py35}-{release,devel} +envlist = {py26,py27,pypy}-{lowest,release,devel}{,-simplejson}, {py33,py34,py35,py36}-{release,devel}{,-simplejson} + + [testenv] +passenv = LANG +usedevelop=true commands = - py.test [] - + # We need to install those after Flask is installed. + pip install -e examples/flaskr + pip install -e examples/minitwit + pip install -e examples/patterns/largerapp + pytest --cov=flask --cov-report html [] deps= pytest + pytest-cov greenlet - redbaron lowest: Werkzeug==0.7 lowest: Jinja2==2.4 lowest: itsdangerous==0.21 lowest: blinker==1.0 release: blinker - devel: git+https://github.com/mitsuhiko/werkzeug.git - devel: git+https://github.com/mitsuhiko/jinja2.git - devel: git+https://github.com/mitsuhiko/itsdangerous.git + devel: git+https://github.com/pallets/werkzeug.git + devel: git+https://github.com/pallets/jinja.git + devel: git+https://github.com/pallets/itsdangerous.git devel: git+https://github.com/jek/blinker.git + simplejson: simplejson [testenv:docs] deps = sphinx