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..0f99a7e8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,18 +11,27 @@ python: 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 install: diff --git a/AUTHORS b/AUTHORS index d081d9f8..cc157dc4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -15,6 +15,7 @@ Patches and Suggestions - Chris Grindstaff - Christopher Grebs - Daniel Neuhäuser +- Dan Sully - David Lord @davidism - Edmond Burnett - Florent Xicluna diff --git a/CHANGES b/CHANGES index 49b42dba..13ce156c 100644 --- a/CHANGES +++ b/CHANGES @@ -3,6 +3,25 @@ Flask Changelog Here you can see the full list of changes between each Flask release. +Version 0.12 +------------ + +- 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 -------------- @@ -41,7 +60,7 @@ Released on May 29th 2016, codename Absinthe. 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. @@ -316,7 +335,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 d72428e4..d9cd2214 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -36,7 +36,7 @@ Running the testsuite --------------------- You probably want to set up a `virtualenv -`_. +`_. The minimal requirement for running the testsuite is ``py.test``. You can install it with:: @@ -68,3 +68,47 @@ of ``pytest``. You can install it with:: 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:: + + py.test --cov=flask tests/ + +Generate a HTML report can be done using this command:: + + py.test --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/Makefile b/Makefile index 350aa9a4..f76c2099 100644 --- a/Makefile +++ b/Makefile @@ -3,10 +3,8 @@ all: clean-pyc test test: - FLASK_DEBUG= 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..baea6b24 100644 --- a/README +++ b/README @@ -37,6 +37,8 @@ $ py.test + Details on contributing can be found in CONTRIBUTING.rst + ~ Where can I get help? Either use the #pocoo IRC channel on irc.freenode.net or 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 e72c9ace..d77da3de 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -316,13 +316,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 diff --git a/docs/appcontext.rst b/docs/appcontext.rst index 672b6bfd..166c5aa3 100644 --- a/docs/appcontext.rst +++ b/docs/appcontext.rst @@ -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 d1b0850d..7ddf50f3 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -32,7 +32,7 @@ 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. For instance -`mymodule:application` would tell it to use the `application` object in +``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`` @@ -61,9 +61,7 @@ Debug Flag 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. - -Or with a filename:: +set to ``1`` debug is enabled or ``0`` disables it:: export FLASK_DEBUG=1 @@ -218,13 +216,13 @@ step. CLI Plugins ----------- -Flask extensions can always patch the `Flask.cli` instance with more +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 `setup.py` file +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: -Example `setup.py`:: +Example :file:`setup.py`:: from setuptools import setup @@ -237,7 +235,7 @@ Example `setup.py`:: ''', ) -Inside `mypackage/comamnds.py` you can then export a Click object:: +Inside :file:`mypackage/commands.py` you can then export a Click object:: import click diff --git a/docs/conf.py b/docs/conf.py index 880e9122..b37427a8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,13 +11,16 @@ # All configuration values have a default; values that are commented out # serve to show the default. from __future__ import print_function -import sys, os +from datetime import datetime +import os +import sys +import pkg_resources # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.append(os.path.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 ----------------------------------------------------- @@ -46,22 +49,21 @@ master_doc = 'index' # General information about the project. project = u'Flask' -copyright = u'2015, Armin Ronacher' +copyright = u'2010 - {0}, Armin Ronacher'.format(datetime.utcnow().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('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 @@ -100,14 +102,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'] @@ -126,7 +126,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, @@ -143,9 +143,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 @@ -187,8 +196,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. @@ -198,10 +206,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 @@ -245,21 +253,23 @@ 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': ('http://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 as e: + 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:') @@ -267,10 +277,6 @@ except ImportError as e: print(' git submodule update --init') print('-' * 74) - pygments_style = 'tango' - html_theme = 'default' - html_theme_options = {} - # unwrap decorators def unwrap_decorators(): diff --git a/docs/config.rst b/docs/config.rst index 3039b3ea..6d37c1e8 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -177,7 +177,7 @@ 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. + cost of cacheability. ``JSONIFY_PRETTYPRINT_REGULAR`` If this is set to ``True`` (the default) jsonify responses will be pretty printed if they are not requested by an @@ -262,7 +262,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:: @@ -310,6 +310,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 ------------------------ diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index 5d88cf72..20e71762 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -21,8 +21,10 @@ Hosted options - `Deploying Flask on OpenShift `_ - `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 b06a1904..0f4af6c3 100644 --- a/docs/deploying/mod_wsgi.rst +++ b/docs/deploying/mod_wsgi.rst @@ -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: http://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: http://modwsgi.readthedocs.io/en/develop/index.html Troubleshooting --------------- diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index 183bdb69..fc991e72 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -29,7 +29,7 @@ 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 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 diff --git a/docs/deploying/wsgi-standalone.rst b/docs/deploying/wsgi-standalone.rst index d546fcd7..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 diff --git a/docs/errorhandling.rst b/docs/errorhandling.rst index e2af7af4..64c0f8b3 100644 --- a/docs/errorhandling.rst +++ b/docs/errorhandling.rst @@ -36,7 +36,7 @@ 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 +`__ 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 @@ -51,7 +51,7 @@ And then add this to your Flask app:: from raven.contrib.flask import Sentry sentry = Sentry(app, dsn='YOUR_DSN_HERE') -Of if you are using factories you can also init it later:: +Or if you are using factories you can also init it later:: from raven.contrib.flask import Sentry sentry = Sentry(dsn='YOUR_DSN_HERE') @@ -77,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 @@ -216,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 ````` @@ -276,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/extensions.rst b/docs/extensions.rst index d1d24807..6deb9652 100644 --- a/docs/extensions.rst +++ b/docs/extensions.rst @@ -25,6 +25,13 @@ importable from ``flask_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 ---------------- diff --git a/docs/installation.rst b/docs/installation.rst index 533a6fff..6f833eac 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,19 +95,19 @@ 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:: @@ -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 :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:: @@ -173,4 +163,3 @@ 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://bootstrap.pypa.io/get-pip.py -.. _ez_setup.py: https://bitbucket.org/pypa/setuptools/raw/bootstrap/ez_setup.py 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/distribute.rst b/docs/patterns/distribute.rst index 872d6e01..72cc25d6 100644 --- a/docs/patterns/distribute.rst +++ b/docs/patterns/distribute.rst @@ -39,10 +39,8 @@ the process, also read the :ref:`fabric-deployment` chapter. Basic Setup Script ------------------ -Because you have Flask running, you have setuptools available on your system anyways. -Flask already depends upon setuptools. If you do not, fear not, there is a -script to install it for you: `ez_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 `. @@ -67,7 +65,7 @@ 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 setuptools to lookup the packages for you automatically, you can use -the `find_packages` function:: +the ``find_packages`` function:: from setuptools import setup, find_packages @@ -76,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 setuptools 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 @@ -104,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 a 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 @@ -140,20 +157,20 @@ 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. .. _pip: https://pypi.python.org/pypi/pip 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/fileuploads.rst b/docs/patterns/fileuploads.rst index 7a3cd28e..8ab8c033 100644 --- a/docs/patterns/fileuploads.rst +++ b/docs/patterns/fileuploads.rst @@ -47,7 +47,7 @@ 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(): @@ -71,7 +71,7 @@ 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:: 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:
+ + Caching Decorator ----------------- diff --git a/docs/patterns/wtforms.rst b/docs/patterns/wtforms.rst index 5a84fce8..2649cad6 100644 --- a/docs/patterns/wtforms.rst +++ b/docs/patterns/wtforms.rst @@ -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 af61584c..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 be 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 0d0028e2..b444e080 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 @@ -42,17 +42,17 @@ 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. Before you can do that you need +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_APP`` environment variable:: $ export FLASK_APP=hello.py $ flask run * Running on http://127.0.0.1:5000/ -If you are on Windows you need to use `set` instead of `export`. +If you are on Windows you need to use ``set`` instead of ``export``. -Alternatively you can use `python -m flask`:: +Alternatively you can use :command:`python -m flask`:: $ export FLASK_APP=hello.py $ python -m flask run @@ -86,7 +86,7 @@ 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. @@ -95,17 +95,17 @@ Old Version of Flask 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. 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. @@ -123,13 +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. -To enable debug mode you can export the `FLASK_DEBUG` environment variable +To enable debug mode you can 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`). +(On Windows you need to use ``set`` instead of ``export``). This does the following things: @@ -202,7 +202,7 @@ 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 @@ -226,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. @@ -250,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 @@ -288,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. @@ -298,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 @@ -446,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 @@ -540,7 +540,7 @@ 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 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 @@ -563,7 +563,7 @@ 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 @@ -725,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): @@ -743,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): @@ -784,7 +780,7 @@ sessions work:: session['username'] = request.form['username'] return redirect(url_for('index')) return ''' - +

@@ -807,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', 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. diff --git a/docs/tutorial/dbinit.rst b/docs/tutorial/dbinit.rst index 09997906..fbbcde00 100644 --- a/docs/tutorial/dbinit.rst +++ b/docs/tutorial/dbinit.rst @@ -35,7 +35,7 @@ just 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 diff --git a/docs/tutorial/folders.rst b/docs/tutorial/folders.rst index 4e117d1f..ba62b3b7 100644 --- a/docs/tutorial/folders.rst +++ b/docs/tutorial/folders.rst @@ -19,7 +19,7 @@ creating the database schema as well as the main module. 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 +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`. diff --git a/docs/tutorial/packaging.rst b/docs/tutorial/packaging.rst index 8db6531e..18f5c9b3 100644 --- a/docs/tutorial/packaging.rst +++ b/docs/tutorial/packaging.rst @@ -55,7 +55,7 @@ into this file, :file:`flaskr/__init__.py`: .. sourcecode:: python - from flaskr import app + 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 diff --git a/docs/upgrading.rst b/docs/upgrading.rst index 5ab73868..41b70f03 100644 --- a/docs/upgrading.rst +++ b/docs/upgrading.rst @@ -14,12 +14,50 @@ 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 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 a 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 ------------ @@ -30,7 +68,7 @@ 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. -In case you did track the master branch you will notice that `flask --app` +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. @@ -68,7 +106,7 @@ 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 ````````````````` @@ -133,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 @@ -199,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 @@ -316,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 @@ -334,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, @@ -354,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 @@ -380,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/flaskr/__init__.py b/examples/flaskr/flaskr/__init__.py index 14a36539..d096b1e7 100644 --- a/examples/flaskr/flaskr/__init__.py +++ b/examples/flaskr/flaskr/__init__.py @@ -1 +1 @@ -from flaskr import app \ No newline at end of file +from .flaskr import app diff --git a/examples/flaskr/flaskr/templates/show_entries.html b/examples/flaskr/flaskr/templates/show_entries.html index 9cbd3229..2f68b9d3 100644 --- a/examples/flaskr/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 index b7e47898..db50667a 100644 --- a/examples/flaskr/setup.cfg +++ b/examples/flaskr/setup.cfg @@ -1,2 +1,2 @@ -[aliases] +[tool:pytest] test=pytest diff --git a/examples/minitwit/minitwit/__init__.py b/examples/minitwit/minitwit/__init__.py index 0b8bd697..96c81aec 100644 --- a/examples/minitwit/minitwit/__init__.py +++ b/examples/minitwit/minitwit/__init__.py @@ -1 +1 @@ -from minitwit import app \ No newline at end of file +from .minitwit import app diff --git a/flask/__init__.py b/flask/__init__.py index 509b944f..b1360e84 100644 --- a/flask/__init__.py +++ b/flask/__init__.py @@ -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/app.py b/flask/app.py index b1ea0464..942992dc 100644 --- a/flask/app.py +++ b/flask/app.py @@ -14,7 +14,6 @@ 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 @@ -519,7 +518,7 @@ 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__) @@ -838,12 +837,11 @@ class Flask(_PackageBoundObject): self.debug = bool(debug) options.setdefault('use_reloader', self.debug) options.setdefault('use_debugger', self.debug) - options.setdefault('passthrough_errors', True) try: 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 @@ -877,9 +875,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 ....') @@ -935,22 +933,7 @@ class Flask(_PackageBoundObject): @setupmethod def register_blueprint(self, blueprint, **options): - """Register a blueprint on the application. For information about - blueprints head over to :ref:`blueprints`. - - The blueprint name is passed in as the first argument. - Options are passed as additional keyword arguments and forwarded to - `blueprints` in an "options" dictionary. - - :param subdomain: set a subdomain for the blueprint - :param url_prefix: set the prefix for all URLs defined on the blueprint. - ``(url_prefix='/')`` - :param url_defaults: a dictionary with URL defaults that is added to - each and every URL defined with this blueprint - :param static_folder: add a static folder to urls in this blueprint - :param static_url_path: add a static url path to urls in this blueprint - :param template_folder: set an alternate template folder - :param root_path: set an alternate root path for this blueprint + """Registers a blueprint on the application. .. versionadded:: 0.7 """ @@ -1131,7 +1114,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) @@ -1169,7 +1152,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) @@ -1452,24 +1436,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, {}) @@ -1571,7 +1544,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` @@ -1639,9 +1612,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): @@ -1819,7 +1813,7 @@ class Flask(_PackageBoundObject): if it was the return value from the view and further request handling is stopped. - This also triggers the :meth:`url_value_processor` functions before + This also triggers the :meth:`url_value_preprocessor` functions before the actual :meth:`before_request` functions are called. """ bp = _request_ctx_stack.top.request.blueprint @@ -1988,7 +1982,7 @@ class Flask(_PackageBoundObject): response = self.full_dispatch_request() except Exception as e: error = e - response = self.make_response(self.handle_exception(e)) + response = self.handle_exception(e) return response(environ, start_response) finally: if self.should_ignore_error(error): diff --git a/flask/cli.py b/flask/cli.py index d3e74d99..da988e80 100644 --- a/flask/cli.py +++ b/flask/cli.py @@ -18,7 +18,7 @@ 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.""" @@ -86,7 +86,13 @@ def locate_app(app_id): module = app_id app_obj = None - __import__(module) + try: + __import__(module) + except ImportError: + 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) @@ -108,10 +114,26 @@ def find_default_import_path(): 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. """ @@ -270,12 +292,19 @@ class FlaskGroup(AppGroup): :param add_default_commands: if this is True then the default run and shell commands wil be added. + :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, create_app=None, **extra): - AppGroup.__init__(self, **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_version_option: + params.append(version_option) + + AppGroup.__init__(self, params=params, **extra) self.create_app = create_app if add_default_commands: @@ -387,7 +416,7 @@ def run_command(info, host, port, reload, debugger, eager_loading, 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 @@ -440,7 +469,7 @@ 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 +It loads the application configured (through the FLASK_APP environment variable) and then provides commands either provided by the application or Flask itself. @@ -449,7 +478,7 @@ The most useful commands are the "run" and "shell" command. Example usage: \b - %(prefix)s%(cmd)s FLASK_APP=hello + %(prefix)s%(cmd)s FLASK_APP=hello.py %(prefix)s%(cmd)s FLASK_DEBUG=1 %(prefix)sflask run """ % { diff --git a/flask/config.py b/flask/config.py index 426a23a2..36e8a123 100644 --- a/flask/config.py +++ b/flask/config.py @@ -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): diff --git a/flask/exthook.py b/flask/exthook.py index 6522e063..d8842802 100644 --- a/flask/exthook.py +++ b/flask/exthook.py @@ -68,7 +68,7 @@ class ExtensionImporter(object): warnings.warn( "Importing flask.ext.{x} is deprecated, use flask_{x} instead." - .format(x=modname), ExtDeprecationWarning + .format(x=modname), ExtDeprecationWarning, stacklevel=2 ) for path in self.module_choices: diff --git a/flask/helpers.py b/flask/helpers.py index c744bb8c..c6c2cddc 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -25,8 +25,9 @@ try: except ImportError: from urlparse import quote as url_quote -from werkzeug.datastructures import Headers -from werkzeug.exceptions import BadRequest, NotFound +from werkzeug.datastructures import Headers, Range +from werkzeug.exceptions import BadRequest, NotFound, \ + RequestedRangeNotSatisfiable # this was moved in 0.7 try: @@ -429,7 +430,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 @@ -443,6 +444,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. @@ -461,6 +469,15 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, .. versionchanged:: 0.9 cache_timeout pulls its default from application config, when None. + .. 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. + :param filename_or_fp: the filename of the file to send in `latin-1`. This is relative to the :attr:`~Flask.root_path` if a relative path is specified. @@ -469,8 +486,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 @@ -482,46 +500,40 @@ 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) + raise TypeError('filename unavailable, required for ' + 'sending as attachment') headers.add('Content-Disposition', 'attachment', filename=attachment_filename) @@ -529,22 +541,24 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, 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: @@ -554,6 +568,8 @@ 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('%s-%s-%s' % ( os.path.getmtime(filename), @@ -567,17 +583,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:: @@ -587,20 +614,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): diff --git a/flask/json.py b/flask/json.py index b9ce4a08..16e0c295 100644 --- a/flask/json.py +++ b/flask/json.py @@ -19,10 +19,7 @@ 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 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 b9120712..4d67658a 100644 --- a/flask/sessions.py +++ b/flask/sessions.py @@ -168,7 +168,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 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/testing.py b/flask/testing.py index 8eacf58b..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 @@ -43,11 +44,23 @@ class FlaskClient(Client): 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 @@ -101,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 6e249180..83394c1f 100644 --- a/flask/views.py +++ b/flask/views.py @@ -123,7 +123,7 @@ class MethodViewType(type): 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 + :meth:`get` it means it will respond to ``'GET'`` requests and the :meth:`dispatch_request` implementation will automatically forward your request to that. Also :attr:`options` is set for you automatically:: diff --git a/scripts/flask-07-upgrade.py b/scripts/flask-07-upgrade.py index 7cab94e1..7fbdd49c 100644 --- a/scripts/flask-07-upgrade.py +++ b/scripts/flask-07-upgrade.py @@ -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/make-release.py b/scripts/make-release.py index 27cbe0ee..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 @@ -85,12 +86,12 @@ def build_and_upload(): 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(): diff --git a/setup.cfg b/setup.cfg index 47c225ea..cd282e48 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,5 +4,8 @@ release = egg_info -RDb '' [wheel] universal = 1 -[pytest] -norecursedirs = .* *.egg *.egg-info env* artwork docs examples +[metadata] +license_file = LICENSE + +[tool:pytest] +norecursedirs = .* *.egg *.egg-info env* artwork docs diff --git a/setup.py b/setup.py index 6cbc4306..983f7611 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,8 @@ And run it: $ python hello.py * Running on http://localhost:5000/ + Ready for production? `Read this first `. + Links ````` 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_basic.py b/tests/test_basic.py index 8c5b0def..be3d5edd 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -768,6 +768,29 @@ 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_before_request_and_routing_errors(): app = flask.Flask(__name__) @@ -1031,6 +1054,14 @@ def test_jsonify_mimetype(): 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__) @@ -1116,6 +1147,30 @@ def test_static_files(): rv.close() +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() + + 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_none_response(): app = flask.Flask(__name__) app.testing = True @@ -1236,8 +1291,6 @@ def test_werkzeug_passthrough_errors(monkeypatch, debug, use_debugger, 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) - # make sure werkzeug always passes errors through - assert rv['passthrough_errors'] def test_max_content_length(): diff --git a/tests/test_cli.py b/tests/test_cli.py index 1e8feb02..18026a75 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -12,6 +12,8 @@ # Copyright (C) 2015 CERN. # from __future__ import absolute_import, print_function +import os +import sys import click import pytest @@ -19,7 +21,8 @@ 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_best_app, locate_app, with_appcontext, prepare_exec_for_file, \ + find_default_import_path, get_version def test_cli_name(test_apps): @@ -29,31 +32,47 @@ def test_cli_name(test_apps): def test_find_best_app(test_apps): - """Test of find_best_app.""" - class mod: + """Test if `find_best_app` behaves as expected with different combinations of input.""" + class Module: app = Flask('appname') - assert find_best_app(mod) == mod.app + assert find_best_app(Module) == Module.app - class mod: + class Module: application = Flask('appname') - assert find_best_app(mod) == mod.application + assert find_best_app(Module) == Module.application - class mod: + class Module: myapp = Flask('appname') - assert find_best_app(mod) == mod.myapp + assert find_best_app(Module) == Module.myapp - class mod: - myapp = Flask('appname') - myapp2 = Flask('appname2') + class Module: + pass + pytest.raises(NoAppException, find_best_app, Module) - pytest.raises(NoAppException, find_best_app, mod) + class Module: + myapp1 = Flask('appname1') + myapp2 = Flask('appname2') + pytest.raises(NoAppException, find_best_app, Module) def test_prepare_exec_for_file(test_apps): - assert prepare_exec_for_file('test.py') == 'test' - assert prepare_exec_for_file('/usr/share/__init__.py') == 'share' + """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('test.txt') + prepare_exec_for_file('/tmp/share/test.txt') def test_locate_app(test_apps): @@ -61,9 +80,39 @@ def test_locate_app(test_apps): 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") +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") 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..d336e404 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 diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 6dc41fad..69fbaf3b 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -12,10 +12,13 @@ import pytest import os +import uuid import datetime + import flask from logging import StreamHandler -from werkzeug.exceptions import BadRequest +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 @@ -99,20 +102,28 @@ class TestJSON(object): rv = flask.json.dumps(u'\N{SNOWMAN}') assert rv == u'"\u2603"' - def test_jsonify_basic_types(self): + 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.""" - # Should be able to use pytest parametrize on this, but I couldn't - # figure out the correct syntax - # https://pytest.org/latest/parametrize.html#pytest-mark-parametrize-parametrizing-test-functions - test_data = (0, 1, 23, 3.14, 's', "longer string", True, False,) app = flask.Flask(__name__) c = app.test_client() - for i, d in enumerate(test_data): - url = '/jsonify_basic_types{0}'.format(i) - app.add_url_rule(url, str(i), lambda x=d: flask.jsonify(x)) - rv = c.get(url) - assert rv.mimetype == 'application/json' - assert flask.json.loads(rv.data) == d + + 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.""" @@ -156,12 +167,10 @@ 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) ) - app = flask.Flask(__name__) c = app.test_client() @@ -172,6 +181,22 @@ class TestJSON(object): 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__) @app.route('/add', methods=['POST']) @@ -337,7 +362,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(): @@ -349,90 +374,163 @@ 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__) - 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) + 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 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__) + + @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.fromtimestamp(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 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']) + 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' rv.close() - # mimetypes + etag - assert len(captured) == 2 with app.test_request_context(): assert options['filename'] == 'index.html' @@ -737,3 +835,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_testing.py b/tests/test_testing.py index 7bb99e79..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' diff --git a/tox.ini b/tox.ini index bd936a4b..406de5dd 100644 --- a/tox.ini +++ b/tox.ini @@ -1,14 +1,20 @@ [tox] -envlist = {py26,py27,pypy}-{lowest,release,devel}, {py33,py34,py35}-{release,devel} +envlist = {py26,py27,pypy}-{lowest,release,devel}{,-simplejson}, {py33,py34,py35}-{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 + py.test --cov=flask --cov-report html [] deps= pytest + pytest-cov greenlet - redbaron lowest: Werkzeug==0.7 lowest: Jinja2==2.4 @@ -19,6 +25,7 @@ deps= 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