diff --git a/CHANGES b/CHANGES index 5436a857..bb03d088 100644 --- a/CHANGES +++ b/CHANGES @@ -8,6 +8,8 @@ Version 0.9 Relase date to be decided, codename to be chosen. +- The :func:`flask.Request.on_json_loading_failed` now returns a JSON formatted + response by default. - The :func:`flask.url_for` function now can generate anchors to the generated links. - The :func:`flask.url_for` function now can also explicitly generate @@ -48,7 +50,19 @@ Relase date to be decided, codename to be chosen. - Added :meth:`flask.Flask.app_context` which works very similar to the request context but only provides access to the current application. This also adds support for URL generation without an active request context. - +- View functions can now return a tuple with the first instance being an + instance of :class:`flask.Response`. This allows for returning + ``jsonify(error="error msg"), 400`` from a view function. +- :class:`flask.Flask` now provides a `get_send_file_options` hook for + subclasses to override behavior of serving static files from Flask when using + :meth:`flask.Flask.send_static_file` based on keywords in + :func:`flask.helpers.send_file`. This hook is provided a filename, which for + example allows changing cache controls by file extension. The default + max-age for `send_static_file` can be configured through a new + ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable, regardless of whether + the `get_send_file_options` hook is used. +- Fixed an assumption in sessions implementation which could break message + flashing on sessions implementations which use external storage. Version 0.8.1 ------------- diff --git a/LICENSE b/LICENSE index 5d269389..dc01ee1a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010 by Armin Ronacher and contributors. See AUTHORS +Copyright (c) 2012 by Armin Ronacher and contributors. See AUTHORS for more details. Some rights reserved. diff --git a/Makefile b/Makefile index 6fa48693..08811ac2 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,6 @@ clean-pyc: find . -name '*.pyo' -exec rm -f {} + find . -name '*~' -exec rm -f {} + -# ebook-convert docs: http://manual.calibre-ebook.com/cli/ebook-convert.html upload-docs: $(MAKE) -C docs html dirhtml latex epub $(MAKE) -C docs/_build/latex all-pdf @@ -31,7 +30,10 @@ upload-docs: rsync -a docs/_build/latex/Flask.pdf pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.pdf rsync -a docs/_build/flask-docs.zip pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.zip rsync -a docs/_build/epub/Flask.epub pocoo.org:/var/www/flask.pocoo.org/docs/flask-docs.epub - @echo 'Building .mobi from .epub...' + +# ebook-convert docs: http://manual.calibre-ebook.com/cli/ebook-convert.html +ebook: + @echo 'Using .epub from `make upload-docs` to create .mobi.' @echo 'Command `ebook-covert` is provided by calibre package.' @echo 'Requires X-forwarding for Qt features used in conversion (ssh -X).' @echo 'Do not mind "Invalid value for ..." CSS errors if .mobi renders.' diff --git a/docs/advanced_foreword.rst b/docs/advanced_foreword.rst new file mode 100644 index 00000000..cc1a1843 --- /dev/null +++ b/docs/advanced_foreword.rst @@ -0,0 +1,67 @@ +Foreword for Experienced Programmers +==================================== + +This chapter is for programmers who have worked with other frameworks in the +past, and who may have more specific or esoteric concerns that the typical +user. + +Threads in Flask +---------------- + +One of the design decisions with Flask was that simple tasks should be simple; +they should not take a lot of code and yet they should not limit you. Because +of that we made a few design choices that some people might find surprising or +unorthodox. For example, Flask uses thread-local objects internally so that +you don’t have to pass objects around from function to function within a +request in order to stay threadsafe. While this is a really easy approach and +saves you a lot of time, it might also cause some troubles for very large +applications because changes on these thread-local objects can happen anywhere +in the same thread. In order to solve these problems we don’t hide the thread +locals for you but instead embrace them and provide you with a lot of tools to +make it as pleasant as possible to work with them. + +Web Development is Dangerous +---------------------------- + +If you write a web application, you are probably allowing users to register +and leave their data on your server. The users are entrusting you with data. +And even if you are the only user that might leave data in your application, +you still want that data to be stored securely. + +Unfortunately, there are many ways the security of a web application can be +compromised. Flask protects you against one of the most common security +problems of modern web applications: cross-site scripting (XSS). Unless +you deliberately mark insecure HTML as secure, Flask and the underlying +Jinja2 template engine have you covered. But there are many more ways to +cause security problems. + +The documentation will warn you about aspects of web development that +require attention to security. Some of these security concerns +are far more complex than one might think, and we all sometimes underestimate +the likelihood that a vulnerability will be exploited - until a clever +attacker figures out a way to exploit our applications. And don't think +that your application is not important enough to attract an attacker. +Depending on the kind of attack, chances are that automated bots are +probing for ways to fill your database with spam, links to malicious +software, and the like. + +So always keep security in mind when doing web development. + +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 us 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. + +Werkzeug and Flask will be ported to Python 3 as soon as a solution for +the changes is found, and we will provide helpful tips how to upgrade +existing applications to Python 3. Until then, we strongly recommend +using Python 2.6 and 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 `_. diff --git a/docs/api.rst b/docs/api.rst index 78ed2d8d..97332870 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -525,7 +525,7 @@ Variable parts are passed to the view function as keyword arguments. The following converters are available: =========== =============================================== -`unicode` accepts any text without a slash (the default) +`string` accepts any text without a slash (the default) `int` accepts integers `float` like `int` but for floating point values `path` like the default but also accepts slashes diff --git a/docs/conf.py b/docs/conf.py index 16d7e670..30df7147 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -43,7 +43,7 @@ master_doc = 'index' # General information about the project. project = u'Flask' -copyright = u'2010, Armin Ronacher' +copyright = u'2012, Armin Ronacher' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/docs/config.rst b/docs/config.rst index a5d2a9f0..86bfb0d1 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -110,6 +110,13 @@ The following configuration values are used internally by Flask: reject incoming requests with a content length greater than this by returning a 413 status code. +``SEND_FILE_MAX_AGE_DEFAULT``: Default cache control max age to use with + :meth:`flask.Flask.send_static_file`, in + seconds. Override this value on a per-file + basis using the + :meth:`flask.Flask.get_send_file_options` and + :meth:`flask.Blueprint.get_send_file_options` + hooks. Defaults to 43200 (12 hours). ``TRAP_HTTP_EXCEPTIONS`` If this is set to ``True`` Flask will not execute the error handlers of HTTP exceptions but instead treat the @@ -276,7 +283,7 @@ configuration:: class ProductionConfig(Config): DATABASE_URI = 'mysql://user@localhost/foo' - + class DevelopmentConfig(Config): DEBUG = True diff --git a/docs/deploying/fastcgi.rst b/docs/deploying/fastcgi.rst index 6dace1a8..ebd68560 100644 --- a/docs/deploying/fastcgi.rst +++ b/docs/deploying/fastcgi.rst @@ -3,12 +3,11 @@ FastCGI ======= -FastCGI is a deployment option on servers like `nginx`_, `lighttpd`_, -and `cherokee`_; see :ref:`deploying-uwsgi` and -:ref:`deploying-other-servers` for other options. To use your WSGI -application with any of them you will need a FastCGI server first. The -most popular one is `flup`_ which we will use for this guide. Make sure -to have it installed to follow along. +FastCGI is a deployment option on servers like `nginx`_, `lighttpd`_, and +`cherokee`_; see :ref:`deploying-uwsgi` and :ref:`deploying-wsgi-standalone` +for other options. To use your WSGI application with any of them you will need +a FastCGI server first. The most popular one is `flup`_ which we will use for +this guide. Make sure to have it installed to follow along. .. admonition:: Watch Out diff --git a/docs/deploying/index.rst b/docs/deploying/index.rst index d258df89..bf78275d 100644 --- a/docs/deploying/index.rst +++ b/docs/deploying/index.rst @@ -13,11 +13,14 @@ If you have a different WSGI server look up the server documentation about how to use a WSGI app with it. Just remember that your :class:`Flask` application object is the actual WSGI application. +For hosted options to get up and running quickly, see +:ref:`quickstart_deployment` in the Quickstart. + .. toctree:: :maxdepth: 2 mod_wsgi - cgi - fastcgi + wsgi-standalone uwsgi - others + fastcgi + cgi diff --git a/docs/deploying/uwsgi.rst b/docs/deploying/uwsgi.rst index bdee15ba..b05fdeec 100644 --- a/docs/deploying/uwsgi.rst +++ b/docs/deploying/uwsgi.rst @@ -4,11 +4,10 @@ uWSGI ===== uWSGI is a deployment option on servers like `nginx`_, `lighttpd`_, and -`cherokee`_; see :ref:`deploying-fastcgi` and -:ref:`deploying-other-servers` for other options. To use your WSGI -application with uWSGI protocol you will need a uWSGI server -first. uWSGI is both a protocol and an application server; the -application server can serve uWSGI, FastCGI, and HTTP protocols. +`cherokee`_; see :ref:`deploying-fastcgi` and :ref:`deploying-wsgi-standalone` +for other options. To use your WSGI application with uWSGI protocol you will +need a uWSGI server first. uWSGI is both a protocol and an application server; +the application server can serve uWSGI, FastCGI, and HTTP protocols. The most popular uWSGI server is `uwsgi`_, which we will use for this guide. Make sure to have it installed to follow along. diff --git a/docs/deploying/others.rst b/docs/deploying/wsgi-standalone.rst similarity index 57% rename from docs/deploying/others.rst rename to docs/deploying/wsgi-standalone.rst index 6f3e5cc6..74385813 100644 --- a/docs/deploying/others.rst +++ b/docs/deploying/wsgi-standalone.rst @@ -1,11 +1,31 @@ -.. _deploying-other-servers: +.. _deploying-wsgi-standalone: -Other Servers -============= +Standalone WSGI Containers +========================== -There are popular servers written in Python that allow the execution of WSGI -applications as well. These servers stand alone when they run; you can proxy -to them from your web server. +There are popular servers written in Python that contain WSGI applications and +serve HTTP. These servers stand alone when they run; you can proxy to them +from your web server. Note the section on :ref:`deploying-proxy-setups` if you +run into issues. + +Gunicorn +-------- + +`Gunicorn`_ 'Green Unicorn' is a WSGI HTTP Server for UNIX. It's a pre-fork +worker model ported from Ruby's Unicorn project. It supports both `eventlet`_ +and `greenlet`_. Running a Flask application on this server is quite simple:: + + gunicorn myproject:app + +`Gunicorn`_ provides many command-line options -- see ``gunicorn -h``. +For example, to run a Flask application with 4 worker processes (``-w +4``) binding to localhost port 4000 (``-b 127.0.0.1:4000``):: + + gunicorn -w 4 -b 127.0.0.1:4000 myproject:app + +.. _Gunicorn: http://gunicorn.org/ +.. _eventlet: http://eventlet.net/ +.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html Tornado -------- @@ -14,7 +34,7 @@ Tornado server and tools that power `FriendFeed`_. Because it is non-blocking and uses epoll, it can handle thousands of simultaneous standing connections, which means it is ideal for real-time web services. Integrating this -service with Flask is a trivial task:: +service with Flask is straightforward:: from tornado.wsgi import WSGIContainer from tornado.httpserver import HTTPServer @@ -46,44 +66,54 @@ event loop:: .. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html .. _libevent: http://monkey.org/~provos/libevent/ -Gunicorn --------- +.. _deploying-proxy-setups: -`Gunicorn`_ 'Green Unicorn' is a WSGI HTTP Server for UNIX. It's a pre-fork -worker model ported from Ruby's Unicorn project. It supports both `eventlet`_ -and `greenlet`_. Running a Flask application on this server is quite simple:: +Proxy Setups +------------ - gunicorn myproject:app +If you deploy your application using one of these servers behind an HTTP proxy +you will need to rewrite a few headers in order for the application to work. +The two problematic values in the WSGI environment usually are `REMOTE_ADDR` +and `HTTP_HOST`. You can configure your httpd to pass these headers, or you +can fix them in middleware. Werkzeug ships a fixer that will solve some common +setups, but you might want to write your own WSGI middleware for specific +setups. -`Gunicorn`_ provides many command-line options -- see ``gunicorn -h``. -For example, to run a Flask application with 4 worker processes (``-w -4``) binding to localhost port 4000 (``-b 127.0.0.1:4000``):: +Here's a simple nginx configuration which proxies to an application served on +localhost at port 8000, setting appropriate headers: - gunicorn -w 4 -b 127.0.0.1:4000 myproject:app +.. sourcecode:: nginx -.. _Gunicorn: http://gunicorn.org/ -.. _eventlet: http://eventlet.net/ -.. _greenlet: http://codespeak.net/py/0.9.2/greenlet.html + server { + listen 80; -Proxy Setups ------------- + server_name _; + + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; -If you deploy your application using one of these servers behind an HTTP -proxy you will need to rewrite a few headers in order for the -application to work. The two problematic values in the WSGI environment -usually are `REMOTE_ADDR` and `HTTP_HOST`. Werkzeug ships a fixer that -will solve some common setups, but you might want to write your own WSGI -middleware for specific setups. + location / { + proxy_pass http://127.0.0.1:8000/; + proxy_redirect off; -The most common setup invokes the host being set from `X-Forwarded-Host` -and the remote address from `X-Forwarded-For`:: + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } + } + +If your httpd is not providing these headers, the most common setup invokes the +host being set from `X-Forwarded-Host` and the remote address from +`X-Forwarded-For`:: from werkzeug.contrib.fixers import ProxyFix app.wsgi_app = ProxyFix(app.wsgi_app) -Please keep in mind that it is a security issue to use such a middleware -in a non-proxy setup because it will blindly trust the incoming -headers which might be forged by malicious clients. +.. admonition:: Trusting Headers + + Please keep in mind that it is a security issue to use such a middleware in + a non-proxy setup because it will blindly trust the incoming headers which + might be forged by malicious clients. If you want to rewrite the headers from another header, you might want to use a fixer like this:: diff --git a/docs/extensiondev.rst b/docs/extensiondev.rst index 074d06ab..60aa6c37 100644 --- a/docs/extensiondev.rst +++ b/docs/extensiondev.rst @@ -125,9 +125,8 @@ Initializing Extensions ----------------------- Many extensions will need some kind of initialization step. For example, -consider your application is currently connecting to SQLite like the -documentation suggests (:ref:`sqlite3`) you will need to provide a few -functions and before / after request handlers. So how does the extension +consider an application that's currently connecting to SQLite like the +documentation suggests (:ref:`sqlite3`). So how does the extension know the name of the application object? Quite simple: you pass it to it. @@ -135,12 +134,14 @@ Quite simple: you pass it to it. There are two recommended ways for an extension to initialize: initialization functions: + If your extension is called `helloworld` you might have a function called ``init_helloworld(app[, extra_args])`` that initializes the extension for that application. It could attach before / after handlers etc. classes: + Classes work mostly like initialization functions but can later be used to further change the behaviour. For an example look at how the `OAuth extension`_ works: there is an `OAuth` object that provides @@ -148,22 +149,29 @@ classes: a remote application that uses OAuth. What to use depends on what you have in mind. For the SQLite 3 extension -we will use the class-based approach because it will provide users with a -manager object that handles opening and closing database connections. +we will use the class-based approach because it will provide users with an +object that handles opening and closing database connections. The Extension Code ------------------ Here's the contents of the `flask_sqlite3.py` for copy/paste:: - from __future__ import absolute_import import sqlite3 from flask import _request_ctx_stack + class SQLite3(object): - def __init__(self, app): + def __init__(self, app=None): + if app is not None: + self.app = app + self.init_app(self.app) + else: + self.app = None + + def init_app(self, app): self.app = app self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') self.app.teardown_request(self.teardown_request) @@ -180,27 +188,29 @@ Here's the contents of the `flask_sqlite3.py` for copy/paste:: ctx = _request_ctx_stack.top ctx.sqlite3_db.close() - def get_db(self): + @property + def connection(self): ctx = _request_ctx_stack.top if ctx is not None: return ctx.sqlite3_db + So here's what these lines of code do: -1. The ``__future__`` import is necessary to activate absolute imports. - Otherwise we could not call our module `sqlite3.py` and import the - top-level `sqlite3` module which actually implements the connection to - SQLite. -2. We create a class for our extension that requires a supplied `app` object, - sets a configuration for the database if it's not there - (:meth:`dict.setdefault`), and attaches `before_request` and - `teardown_request` handlers. -3. Next, we define a `connect` function that opens a database connection. +1. The ``__init__`` method takes an optional app object and, if supplied, will + call ``init_app``. +2. The ``init_app`` method exists so that the ``SQLite3`` object can be + instantiated without requiring an app object. This method supports the + factory pattern for creating applications. The ``init_app`` will set the + configuration for the database, defaulting to an in memory database if + no configuration is supplied. In addition, the ``init_app`` method attaches + ``before_request`` and ``teardown_request`` handlers. +3. Next, we define a ``connect`` method that opens a database connection. 4. Then we set up the request handlers we bound to the app above. Note here that we're attaching our database connection to the top request context via - `_request_ctx_stack.top`. Extensions should use the top context and not the - `g` object to store things like database connections. -5. Finally, we add a `get_db` function that simplifies access to the context's + ``_request_ctx_stack.top``. Extensions should use the top context and not the + ``g`` object to store things like database connections. +5. Finally, we add a ``connection`` property that simplifies access to the context's database. So why did we decide on a class-based approach here? Because using our @@ -211,68 +221,36 @@ extension looks something like this:: app = Flask(__name__) app.config.from_pyfile('the-config.cfg') - manager = SQLite3(app) - db = manager.get_db() + db = SQLite3(app) You can then use the database from views like this:: @app.route('/') def show_all(): - cur = db.cursor() + cur = db.connection.cursor() cur.execute(...) -Opening a database connection from outside a view function is simple. - ->>> from yourapplication import db ->>> cur = db.cursor() ->>> cur.execute(...) - -Adding an `init_app` Function ------------------------------ - -In practice, you'll almost always want to permit users to initialize your -extension and provide an app object after the fact. This can help avoid -circular import problems when a user is breaking their app into multiple files. -Our extension could add an `init_app` function as follows:: - - class SQLite3(object): - - def __init__(self, app=None): - if app is not None: - self.app = app - self.init_app(self.app) - else: - self.app = None - - def init_app(self, app): - self.app = app - self.app.config.setdefault('SQLITE3_DATABASE', ':memory:') - self.app.teardown_request(self.teardown_request) - self.app.before_request(self.before_request) - - def connect(self): - return sqlite3.connect(app.config['SQLITE3_DATABASE']) - - def before_request(self): - ctx = _request_ctx_stack.top - ctx.sqlite3_db = self.connect() +Additionally, the ``init_app`` method is used to support the factory pattern +for creating apps:: - def teardown_request(self, exception): - ctx = _request_ctx_stack.top - ctx.sqlite3_db.close() + db = Sqlite3() + # Then later on. + app = create_app('the-config.cfg') + db.init_app(app) - def get_db(self): - ctx = _request_ctx_stack.top - if ctx is not None: - return ctx.sqlite3_db +Keep in mind that supporting this factory pattern for creating apps is required +for approved flask extensions (described below). -The user could then initialize the extension in one file:: - manager = SQLite3() +Using _request_ctx_stack +------------------------ -and bind their app to the extension in another file:: - - manager.init_app(app) +In the example above, before every request, a ``sqlite3_db`` variable is assigned +to ``_request_ctx_stack.top``. In a view function, this variable is accessible +using the ``connection`` property of ``SQLite3``. During the teardown of a +request, the ``sqlite3_db`` connection is closed. By using this pattern, the +*same* connection to the sqlite3 database is accessible to anything that needs it +for the duration of the request. End-Of-Request Behavior ----------------------- @@ -292,6 +270,7 @@ pattern is a good way to support both:: else: app.after_request(close_connection) + Strictly speaking the above code is wrong, because teardown functions are passed the exception and typically don't return anything. However because the return value is discarded this will just work assuming that the code @@ -326,15 +305,19 @@ new releases. These approved extensions are listed on the `Flask Extension Registry`_ and marked appropriately. If you want your own extension to be approved you have to follow these guidelines: +0. An approved Flask extension requires a maintainer. In the event an + extension author would like to move beyond the project, the project should + find a new maintainer including full source hosting transition and PyPI + access. If no maintainer is available, give access to the Flask core team. 1. An approved Flask extension must provide exactly one package or module named ``flask_extensionname``. They might also reside inside a ``flaskext`` namespace packages though this is discouraged now. 2. It must ship a testing suite that can either be invoked with ``make test`` or ``python setup.py test``. For test suites invoked with ``make test`` the extension has to ensure that all dependencies for the test - are installed automatically, in case of ``python setup.py test`` - dependencies for tests alone can be specified in the `setup.py` - file. The test suite also has to be part of the distribution. + are installed automatically. If tests are invoked with ``python setup.py + test``, test dependencies can be specified in the `setup.py` file. The + test suite also has to be part of the distribution. 3. APIs of approved extensions will be checked for the following characteristics: diff --git a/docs/foreword.rst b/docs/foreword.rst index 7678d014..b186aba6 100644 --- a/docs/foreword.rst +++ b/docs/foreword.rst @@ -8,93 +8,48 @@ should or should not be using it. What does "micro" mean? ----------------------- -To me, the "micro" in microframework refers not only to the simplicity and -small size of the framework, but also the fact that it does not make many -decisions for you. While Flask does pick a templating engine for you, we -won't make such decisions for your datastore or other parts. - -However, to us the term “micro” does not mean that the whole implementation -has to fit into a single Python file. - -One of the design decisions with Flask was that simple tasks should be -simple; they should not take a lot of code and yet they should not limit you. -Because of that we made a few design choices that some people might find -surprising or unorthodox. For example, Flask uses thread-local objects -internally so that you don't have to pass objects around from function to -function within a request in order to stay threadsafe. While this is a -really easy approach and saves you a lot of time, it might also cause some -troubles for very large applications because changes on these thread-local -objects can happen anywhere in the same thread. In order to solve these -problems we don't hide the thread locals for you but instead embrace them -and provide you with a lot of tools to make it as pleasant as possible to -work with them. - -Flask is also based on convention over configuration, which means that -many things are preconfigured. For example, by convention templates and -static files are stored in subdirectories within the application's Python source tree. -While this can be changed you usually don't have to. - -The main reason Flask is called a "microframework" is the idea -to keep the core simple but extensible. There is no database abstraction -layer, no form validation or anything else where different libraries -already exist that can handle that. However Flask supports -extensions to add such functionality to your application as if it -was implemented in Flask itself. There are currently extensions for -object-relational mappers, form validation, upload handling, various open -authentication technologies and more. - -Since Flask is based on a very solid foundation there is not a lot of code -in Flask itself. As such it's easy to adapt even for large applications -and we are making sure that you can either configure it as much as -possible by subclassing things or by forking the entire codebase. If you -are interested in that, check out the :ref:`becomingbig` chapter. - -If you are curious about the Flask design principles, head over to the -section about :ref:`design`. - -Web Development is Dangerous ----------------------------- - -I'm not joking. Well, maybe a little. If you write a web -application, you are probably allowing users to register and leave their -data on your server. The users are entrusting you with data. And even if -you are the only user that might leave data in your application, you still -want that data to be stored securely. - -Unfortunately, there are many ways the security of a web application can be -compromised. Flask protects you against one of the most common security -problems of modern web applications: cross-site scripting (XSS). Unless -you deliberately mark insecure HTML as secure, Flask and the underlying -Jinja2 template engine have you covered. But there are many more ways to -cause security problems. - -The documentation will warn you about aspects of web development that -require attention to security. Some of these security concerns -are far more complex than one might think, and we all sometimes underestimate -the likelihood that a vulnerability will be exploited - until a clever -attacker figures out a way to exploit our applications. And don't think -that your application is not important enough to attract an attacker. -Depending on the kind of attack, chances are that automated bots are -probing for ways to fill your database with spam, links to malicious -software, and the like. - -So always keep security in mind when doing web development. - -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 us 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. - -Werkzeug and Flask will be ported to Python 3 as soon as a solution for -the changes is found, and we will provide helpful tips how to upgrade -existing applications to Python 3. Until then, we strongly recommend -using Python 2.6 and 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 `_. +“Micro” does not mean that your whole web application has to fit into +a single Python file (although it certainly can). Nor does it mean +that Flask is lacking in functionality. The "micro" in microframework +means Flask aims to keep the core simple but extensible. Flask won't make +many decisions for you, such as what database to use. Those decisions that +it does make, such as what templating engine to use, are easy to change. +Everything else is up to you, so that Flask can be everything you need +and nothing you don't. + +By default, Flask does not include a database abstraction layer, form +validation or anything else where different libraries already exist that can +handle that. Instead, FLask extensions add such functionality to your +application as if it was implemented in Flask itself. Numerous extensions +provide database integration, form validation, upload handling, various open +authentication technologies, and more. Flask may be "micro", but the +possibilities are endless. + +Convention over Configuration +----------------------------- + +Flask is based on convention over configuration, which means that many things +are preconfigured. For example, by convention templates and static files are +stored in subdirectories within the application's Python source tree. While +this can be changed you usually don't have to. We want to minimize the time +you need to spend in order to get up and running, without assuming things +about your needs. + +Growing Up +---------- + +Since Flask is based on a very solid foundation there is not a lot of code in +Flask itself. As such it's easy to adapt even for large applications and we +are making sure that you can either configure it as much as possible by +subclassing things or by forking the entire codebase. If you are interested +in that, check out the :ref:`becomingbig` chapter. + +If you are curious about the Flask design principles, head over to the section +about :ref:`design`. + +For the Stalwart and Wizened... +------------------------------- + +If you're more curious about the minutiae of Flask's implementation, and +whether its structure is right for your needs, read the +:ref:`advanced_foreword`. diff --git a/docs/installation.rst b/docs/installation.rst index 8e6a4497..791c99f1 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -94,7 +94,7 @@ System-Wide Installation ------------------------ This is possible as well, though I do not recommend it. Just run -`easy_install` with root privileges:: +`pip` with root privileges:: $ sudo pip install Flask diff --git a/docs/patterns/appdispatch.rst b/docs/patterns/appdispatch.rst index 177ade2b..a2d1176f 100644 --- a/docs/patterns/appdispatch.rst +++ b/docs/patterns/appdispatch.rst @@ -30,6 +30,24 @@ at :func:`werkzeug.serving.run_simple`:: Note that :func:`run_simple ` is not intended for use in production. Use a :ref:`full-blown WSGI server `. +In order to use the interactive debuggger, debugging must be enabled both on +the application and the simple server, here is the "hello world" example with +debugging and :func:`run_simple `:: + + from flask import Flask + from werkzeug.serving import run_simple + + app = Flask(__name__) + app.debug = True + + @app.route('/') + def hello_world(): + return 'Hello World!' + + if __name__ == '__main__': + run_simple('localhost', 5000, app, + use_reloader=True, use_debugger=True, use_evalex=True) + Combining Applications ---------------------- diff --git a/docs/patterns/mongokit.rst b/docs/patterns/mongokit.rst index a9c4eef5..b50cf456 100644 --- a/docs/patterns/mongokit.rst +++ b/docs/patterns/mongokit.rst @@ -141,4 +141,4 @@ These results are also dict-like objects: u'admin@localhost' For more information about MongoKit, head over to the -`website `_. +`website `_. diff --git a/docs/patterns/packages.rst b/docs/patterns/packages.rst index 79fd2c58..1c7f9bd0 100644 --- a/docs/patterns/packages.rst +++ b/docs/patterns/packages.rst @@ -55,7 +55,7 @@ following quick checklist: `__init__.py` file. That way each module can import it safely and the `__name__` variable will resolve to the correct package. 2. all the view functions (the ones with a :meth:`~flask.Flask.route` - decorator on top) have to be imported when in the `__init__.py` file. + decorator on top) have to be imported in the `__init__.py` file. Not the object itself, but the module it is in. Import the view module **after the application object is created**. diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 9fde0c2f..8497f082 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -168,8 +168,8 @@ The following converters exist: .. admonition:: Unique URLs / Redirection Behaviour - Flask's URL rules are based on Werkzeug's routing module. The idea behind - that module is to ensure beautiful and unique also unique URLs based on + Flask's URL rules are based on Werkzeug's routing module. The idea + behind that module is to ensure beautiful and unique URLs based on precedents laid down by Apache and earlier HTTP servers. Take these two rules:: @@ -193,7 +193,7 @@ The following converters exist: with a trailing slash will produce a 404 "Not Found" error. This behavior allows relative URLs to continue working if users access the - page when they forget a trailing slash, consistent with how with how Apache + page when they forget a trailing slash, consistent with how Apache and other servers work. Also, the URLs will stay unique, which helps search engines avoid indexing the same page twice. @@ -234,7 +234,7 @@ some examples: (This also uses the :meth:`~flask.Flask.test_request_context` method, explained below. It tells Flask to behave as though it is handling a request, even -though were are interacting with it through a Python shell. Have a look at the +though we are interacting with it through a Python shell. Have a look at the explanation below. :ref:`context-locals`). Why would you want to build URLs instead of hard-coding them into your @@ -333,8 +333,7 @@ configured to serve them for you, but during development Flask can do that as well. Just create a folder called `static` in your package or next to your module and it will be available at `/static` on the application. -To generate URLs that part of the URL, use the special ``'static'`` URL -name:: +To generate URLs for static files, use the special ``'static'`` endpoint name:: url_for('static', filename='style.css') @@ -826,3 +825,26 @@ can do it like this:: from werkzeug.contrib.fixers import LighttpdCGIRootFix app.wsgi_app = LighttpdCGIRootFix(app.wsgi_app) + +.. _quickstart_deployment: + +Deploying to a Web Server +------------------------- + +Ready to deploy your new Flask app? To wrap up the quickstart, you can +immediately deploy to a hosted platform, all of which offer a free plan for +small projects: + +- `Deploying Flask on Heroku `_ +- `Deploying Flask on ep.io `_ +- `Deploying WSGI on dotCloud `_ + with `Flask-specific notes `_ + +Other places where you can host your Flask app: + +- `Deploying Flask on Webfaction `_ +- `Deploying Flask on Google App Engine `_ +- `Sharing your Localhost Server with Localtunnel `_ + +If you manage your own hosts and would like to host yourself, see the chapter +on :ref:`deployment`. diff --git a/docs/signals.rst b/docs/signals.rst index 2d3878f7..a4cd4157 100644 --- a/docs/signals.rst +++ b/docs/signals.rst @@ -131,6 +131,8 @@ debugging. You can access the name of the signal with the missing blinker installations, you can do so by using the :class:`flask.signals.Namespace` class. +.. _signals-sending: + Sending Signals --------------- @@ -156,6 +158,17 @@ function, you can pass ``current_app._get_current_object()`` as sender. that :data:`~flask.current_app` is a proxy and not the real application object. + +Signals and Flask's Request Context +----------------------------------- + +Signals fully support :ref:`request-context` when receiving signals. +Context-local variables are consistently available between +:data:`~flask.request_started` and :data:`~flask.request_finished`, so you can +rely on :class:`flask.g` and others as needed. Note the limitations described +in :ref:`signals-sending` and the :data:`~flask.request_tearing_down` signal. + + Decorator Based Signal Subscriptions ------------------------------------ diff --git a/docs/tutorial/setup.rst b/docs/tutorial/setup.rst index e9e4d679..3a8fba33 100644 --- a/docs/tutorial/setup.rst +++ b/docs/tutorial/setup.rst @@ -11,7 +11,7 @@ into the module which we will be doing here. However a cleaner solution would be to create a separate `.ini` or `.py` file and load that or import the values from there. -:: +In `flaskr.py`:: # all the imports import sqlite3 @@ -26,7 +26,7 @@ the values from there. PASSWORD = 'default' Next we can create our actual application and initialize it with the -config from the same file:: +config from the same file, in `flaskr.py`:: # create our little application :) app = Flask(__name__) @@ -37,21 +37,21 @@ string it will import it) and then look for all uppercase variables defined there. In our case, the configuration we just wrote a few lines of code above. You can also move that into a separate file. -It is also a good idea to be able to load a configuration from a -configurable file. This is what :meth:`~flask.Config.from_envvar` can -do:: +Usually, it is a good idea to load a configuration from a configurable +file. This is what :meth:`~flask.Config.from_envvar` can do, replacing the +:meth:`~flask.Config.from_object` line above:: app.config.from_envvar('FLASKR_SETTINGS', silent=True) That way someone can set an environment variable called -:envvar:`FLASKR_SETTINGS` to specify a config file to be loaded which will -then override the default values. The silent switch just tells Flask to -not complain if no such environment key is set. +:envvar:`FLASKR_SETTINGS` to specify a config file to be loaded which will then +override the default values. The silent switch just tells Flask to not complain +if no such environment key is set. The `secret_key` is needed to keep the client-side sessions secure. Choose that key wisely and as hard to guess and complex as possible. The -debug flag enables or disables the interactive debugger. Never leave -debug mode activated in a production system because it will allow users to +debug flag enables or disables the interactive debugger. *Never leave +debug mode activated in a production system*, because it will allow users to execute code on the server! We also add a method to easily connect to the database specified. That diff --git a/flask/app.py b/flask/app.py index 215bf0ad..e16b35bf 100644 --- a/flask/app.py +++ b/flask/app.py @@ -249,6 +249,7 @@ class Flask(_PackageBoundObject): 'SESSION_COOKIE_HTTPONLY': True, 'SESSION_COOKIE_SECURE': False, 'MAX_CONTENT_LENGTH': None, + 'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False, 'PREFERRED_URL_SCHEME': 'http' @@ -1021,6 +1022,12 @@ class Flask(_PackageBoundObject): self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ .append((code_or_exception, f)) + def get_send_file_options(self, filename): + # Override: Hooks in SEND_FILE_MAX_AGE_DEFAULT config. + options = super(Flask, self).get_send_file_options(filename) + options['cache_timeout'] = self.config['SEND_FILE_MAX_AGE_DEFAULT'] + return options + @setupmethod def template_filter(self, name=None): """A decorator that is used to register custom template filter. @@ -1362,7 +1369,21 @@ class Flask(_PackageBoundObject): if isinstance(rv, basestring): return self.response_class(rv) if isinstance(rv, tuple): - return self.response_class(*rv) + if len(rv) > 0 and isinstance(rv[0], self.response_class): + original = rv[0] + new_response = self.response_class('', *rv[1:]) + if len(rv) < 3: + # The args for the response class are + # response=None, status=None, headers=None, + # mimetype=None, content_type=None, ... + # so if there's at least 3 elements the rv + # tuple contains header information so the + # headers from rv[0] "win." + new_response.headers = original.headers + new_response.response = original.response + return new_response + else: + return self.response_class(*rv) return self.response_class.force_type(rv, request.environ) def create_url_adapter(self, request): diff --git a/flask/config.py b/flask/config.py index 67dbf9b7..759fd488 100644 --- a/flask/config.py +++ b/flask/config.py @@ -106,8 +106,7 @@ class Config(dict): 'loaded. Set this variable and make it ' 'point to a configuration file' % variable_name) - self.from_pyfile(rv) - return True + return self.from_pyfile(rv, silent=silent) def from_pyfile(self, filename, silent=False): """Updates the values in the config from a Python file. This function diff --git a/flask/exceptions.py b/flask/exceptions.py new file mode 100644 index 00000000..9ccdedab --- /dev/null +++ b/flask/exceptions.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +""" + flask.exceptions + ~~~~~~~~~~~~ + + Flask specific additions to :class:`~werkzeug.exceptions.HTTPException` + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +from werkzeug.exceptions import HTTPException, BadRequest +from .helpers import json + + +class JSONHTTPException(HTTPException): + """A base class for HTTP exceptions with ``Content-Type: + application/json``. + + The ``description`` attribute of this class must set to a string (*not* an + HTML string) which describes the error. + + """ + + def get_body(self, environ): + """Overrides :meth:`werkzeug.exceptions.HTTPException.get_body` to + return the description of this error in JSON format instead of HTML. + + """ + return json.dumps(dict(description=self.get_description(environ))) + + def get_headers(self, environ): + """Returns a list of headers including ``Content-Type: + application/json``. + + """ + return [('Content-Type', 'application/json')] + + +class JSONBadRequest(JSONHTTPException, BadRequest): + """Represents an HTTP ``400 Bad Request`` error whose body contains an + error message in JSON format instead of HTML format (as in the superclass). + + """ + + #: The description of the error which occurred as a string. + description = ( + 'The browser (or proxy) sent a request that this server could not ' + 'understand.' + ) diff --git a/flask/helpers.py b/flask/helpers.py index b86ce158..01f4081e 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -118,9 +118,31 @@ def jsonify(*args, **kwargs): information about this, have a look at :ref:`json-security`. .. versionadded:: 0.2 + + .. versionadded:: 0.9 + If the ``padded`` argument is true, the JSON object will be padded + for JSONP calls and the response mimetype will be changed to + ``application/javascript``. By default, the request arguments ``callback`` + and ``jsonp`` will be used as the name for the callback function. + This will work with jQuery and most other JavaScript libraries + by default. + + If the ``padded`` argument is a string, jsonify will look for + the request argument with the same name and use that value as the + callback-function name. """ if __debug__: _assert_have_json() + if 'padded' in kwargs: + if isinstance(kwargs['padded'], str): + callback = request.args.get(kwargs['padded']) or 'jsonp' + else: + callback = request.args.get('callback') or \ + request.args.get('jsonp') or 'jsonp' + del kwargs['padded'] + json_str = json.dumps(dict(*args, **kwargs), indent=None) + content = str(callback) + "(" + json_str + ")" + return current_app.response_class(content, mimetype='application/javascript') return current_app.response_class(json.dumps(dict(*args, **kwargs), indent=None if request.is_xhr else 2), mimetype='application/json') @@ -283,7 +305,16 @@ def flash(message, category='message'): messages and ``'warning'`` for warnings. However any kind of string can be used as category. """ - session.setdefault('_flashes', []).append((category, message)) + # Original implementation: + # + # session.setdefault('_flashes', []).append((category, message)) + # + # This assumed that changes made to mutable structures in the session are + # are always in sync with the sess on object, which is not true for session + # implementations that use external storage for keeping their keys/values. + flashes = session.get('_flashes', []) + flashes.append((category, message)) + session['_flashes'] = flashes def get_flashed_messages(with_categories=False, category_filter=[]): @@ -341,6 +372,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False, guessing requires a `filename` or an `attachment_filename` to be provided. + Note `get_send_file_options` in :class:`flask.Flask` hooks the + ``SEND_FILE_MAX_AGE_DEFAULT`` configuration variable to set the default + cache_timeout. + Please never pass filenames to this function from user sources without checking them first. Something like this is usually sufficient to avoid security problems:: @@ -517,7 +552,8 @@ def send_from_directory(directory, filename, **options): filename = safe_join(directory, filename) if not os.path.isfile(filename): raise NotFound() - return send_file(filename, conditional=True, **options) + options.setdefault('conditional', True) + return send_file(filename, **options) def get_root_path(import_name): @@ -673,6 +709,25 @@ class _PackageBoundObject(object): return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) + def get_send_file_options(self, filename): + """Provides keyword arguments to send to :func:`send_from_directory`. + + This allows subclasses to change the behavior when sending files based + on the filename. For example, to set the cache timeout for .js files + to 60 seconds (note the options are keywords for :func:`send_file`):: + + class MyFlask(flask.Flask): + def get_send_file_options(self, filename): + options = super(MyFlask, self).get_send_file_options(filename) + if filename.lower().endswith('.js'): + options['cache_timeout'] = 60 + options['conditional'] = True + return options + + .. versionadded:: 0.9 + """ + return {} + def send_static_file(self, filename): """Function used internally to send static files from the static folder to the browser. @@ -681,7 +736,8 @@ class _PackageBoundObject(object): """ if not self.has_static_folder: raise RuntimeError('No static folder for this object') - return send_from_directory(self.static_folder, filename) + return send_from_directory(self.static_folder, filename, + **self.get_send_file_options(filename)) def open_resource(self, resource, mode='rb'): """Opens a resource from the application's resource folder. To see diff --git a/flask/testsuite/basic.py b/flask/testsuite/basic.py index e6a278e5..41efb196 100644 --- a/flask/testsuite/basic.py +++ b/flask/testsuite/basic.py @@ -659,6 +659,35 @@ class BasicFunctionalityTestCase(FlaskTestCase): self.assert_equal(rv.data, 'W00t') self.assert_equal(rv.mimetype, 'text/html') + def test_make_response_with_response_instance(self): + app = flask.Flask(__name__) + with app.test_request_context(): + rv = flask.make_response( + flask.jsonify({'msg': 'W00t'}), 400) + self.assertEqual(rv.status_code, 400) + self.assertEqual(rv.data, + '{\n "msg": "W00t"\n}') + self.assertEqual(rv.mimetype, 'application/json') + + rv = flask.make_response( + flask.Response(''), 400) + self.assertEqual(rv.status_code, 400) + self.assertEqual(rv.data, '') + self.assertEqual(rv.mimetype, 'text/html') + + rv = flask.make_response( + flask.Response('', headers={'Content-Type': 'text/html'}), + 400, None, 'application/json') + self.assertEqual(rv.status_code, 400) + self.assertEqual(rv.headers['Content-Type'], 'application/json') + + rv = flask.make_response( + flask.Response('', mimetype='application/json'), + 400, {'Content-Type': 'text/html'}) + self.assertEqual(rv.status_code, 400) + self.assertEqual(rv.headers['Content-Type'], 'text/html') + + def test_url_generation(self): app = flask.Flask(__name__) @app.route('/hello/', methods=['POST']) diff --git a/flask/testsuite/blueprints.py b/flask/testsuite/blueprints.py index 5bf81d92..5f3d3ab3 100644 --- a/flask/testsuite/blueprints.py +++ b/flask/testsuite/blueprints.py @@ -16,6 +16,7 @@ import unittest import warnings from flask.testsuite import FlaskTestCase, emits_module_deprecation_warning from werkzeug.exceptions import NotFound +from werkzeug.http import parse_cache_control_header from jinja2 import TemplateNotFound @@ -357,6 +358,19 @@ class BlueprintTestCase(FlaskTestCase): rv = c.get('/admin/static/css/test.css') self.assert_equal(rv.data.strip(), '/* nested file */') + # try/finally, in case other tests use this app for Blueprint tests. + max_age_default = app.config['SEND_FILE_MAX_AGE_DEFAULT'] + try: + expected_max_age = 3600 + if app.config['SEND_FILE_MAX_AGE_DEFAULT'] == expected_max_age: + expected_max_age = 7200 + app.config['SEND_FILE_MAX_AGE_DEFAULT'] = expected_max_age + rv = c.get('/admin/static/css/test.css') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, expected_max_age) + finally: + app.config['SEND_FILE_MAX_AGE_DEFAULT'] = max_age_default + with app.test_request_context(): self.assert_equal(flask.url_for('admin.static', filename='test.txt'), '/admin/static/test.txt') diff --git a/flask/testsuite/config.py b/flask/testsuite/config.py index 00f77cea..bf72925b 100644 --- a/flask/testsuite/config.py +++ b/flask/testsuite/config.py @@ -69,6 +69,24 @@ class ConfigTestCase(FlaskTestCase): finally: os.environ = env + def test_config_from_envvar_missing(self): + env = os.environ + try: + os.environ = {'FOO_SETTINGS': 'missing.cfg'} + try: + app = flask.Flask(__name__) + app.config.from_envvar('FOO_SETTINGS') + except IOError, e: + msg = str(e) + self.assert_(msg.startswith('[Errno 2] Unable to load configuration ' + 'file (No such file or directory):')) + self.assert_(msg.endswith("missing.cfg'")) + else: + self.fail('expected IOError') + self.assertFalse(app.config.from_envvar('FOO_SETTINGS', silent=True)) + finally: + os.environ = env + def test_config_missing(self): app = flask.Flask(__name__) try: diff --git a/flask/testsuite/helpers.py b/flask/testsuite/helpers.py index 89c6c188..be268645 100644 --- a/flask/testsuite/helpers.py +++ b/flask/testsuite/helpers.py @@ -17,7 +17,7 @@ import unittest from logging import StreamHandler from StringIO import StringIO from flask.testsuite import FlaskTestCase, catch_warnings, catch_stderr -from werkzeug.http import parse_options_header +from werkzeug.http import parse_cache_control_header, parse_options_header def has_encoding(name): @@ -40,6 +40,18 @@ class JSONTestCase(FlaskTestCase): rv = c.post('/json', data='malformed', content_type='application/json') self.assert_equal(rv.status_code, 400) + def test_json_bad_requests_content_type(self): + app = flask.Flask(__name__) + @app.route('/json', methods=['POST']) + def return_json(): + return unicode(flask.request.json) + c = app.test_client() + rv = c.post('/json', data='malformed', content_type='application/json') + self.assert_equal(rv.status_code, 400) + self.assert_equal(rv.mimetype, 'application/json') + self.assert_('description' in flask.json.loads(rv.data)) + self.assert_('

' not in flask.json.loads(rv.data)['description']) + def test_json_body_encoding(self): app = flask.Flask(__name__) app.testing = True @@ -61,11 +73,25 @@ class JSONTestCase(FlaskTestCase): @app.route('/dict') def return_dict(): return flask.jsonify(d) + @app.route("/padded") + def return_padded_json(): + return flask.jsonify(d, padded=True) + @app.route("/padded_custom") + def return_padded_json_custom_callback(): + return flask.jsonify(d, padded='my_func_name') c = app.test_client() for url in '/kw', '/dict': rv = c.get(url) self.assert_equal(rv.mimetype, 'application/json') self.assert_equal(flask.json.loads(rv.data), d) + for get_arg in 'callback=funcName', 'jsonp=funcName': + rv = c.get('/padded?' + get_arg) + self.assert_( rv.data.startswith("funcName(") ) + self.assert_( rv.data.endswith(")") ) + rv_json = rv.data.split('(')[1].split(')')[0] + self.assert_equal(flask.json.loads(rv_json), d) + rv = c.get('/padded_custom?my_func_name=funcName') + self.assert_( rv.data.startswith("funcName(") ) def test_json_attr(self): app = flask.Flask(__name__) @@ -204,6 +230,33 @@ class SendfileTestCase(FlaskTestCase): self.assert_equal(value, 'attachment') self.assert_equal(options['filename'], 'index.txt') + def test_static_file(self): + app = flask.Flask(__name__) + # default cache timeout is 12 hours + with app.test_request_context(): + rv = app.send_static_file('index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 12 * 60 * 60) + app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 3600 + with app.test_request_context(): + rv = app.send_static_file('index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 3600) + # override get_send_file_options with some new values and check them + class StaticFileApp(flask.Flask): + def get_send_file_options(self, filename): + opts = super(StaticFileApp, self).get_send_file_options(filename) + opts['cache_timeout'] = 10 + # this test catches explicit inclusion of the conditional + # keyword arg in the guts + opts['conditional'] = True + return opts + app = StaticFileApp(__name__) + with app.test_request_context(): + rv = app.send_static_file('index.html') + cc = parse_cache_control_header(rv.headers['Cache-Control']) + self.assert_equal(cc.max_age, 10) + class LoggingTestCase(FlaskTestCase): diff --git a/flask/views.py b/flask/views.py index 79d62992..5192c1c1 100644 --- a/flask/views.py +++ b/flask/views.py @@ -107,7 +107,7 @@ class MethodViewType(type): rv = type.__new__(cls, name, bases, d) if 'methods' not in d: methods = set(rv.methods or []) - for key, value in d.iteritems(): + for key in d: if key in http_method_funcs: methods.add(key.upper()) # if we have no method at all in there we don't want to diff --git a/flask/wrappers.py b/flask/wrappers.py index f6ec2788..3ee718ff 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -10,9 +10,9 @@ """ from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase -from werkzeug.exceptions import BadRequest from werkzeug.utils import cached_property +from .exceptions import JSONBadRequest from .debughelpers import attach_enctype_error_multidict from .helpers import json, _assert_have_json from .globals import _request_ctx_stack @@ -108,12 +108,22 @@ class Request(RequestBase): def on_json_loading_failed(self, e): """Called if decoding of the JSON data failed. The return value of - this method is used by :attr:`json` when an error ocurred. The - default implementation raises a :class:`~werkzeug.exceptions.BadRequest`. + this method is used by :attr:`json` when an error ocurred. The default + implementation raises a :class:`JSONBadRequest`, which is a subclass of + :class:`~werkzeug.exceptions.BadRequest` which sets the + ``Content-Type`` to ``application/json`` and provides a JSON-formatted + error description:: + + {"description": "The browser (or proxy) sent a request that \ + this server could not understand."} + + .. versionchanged:: 0.9 + Return a :class:`JSONBadRequest` instead of a + :class:`~werkzeug.exceptions.BadRequest` by default. .. versionadded:: 0.8 """ - raise BadRequest() + raise JSONBadRequest() def _load_form_data(self): RequestBase._load_form_data(self) diff --git a/setup.py b/setup.py index 3a302cea..8169a517 100644 --- a/setup.py +++ b/setup.py @@ -59,7 +59,7 @@ class run_audit(Command): try: import pyflakes.scripts.pyflakes as flakes except ImportError: - print "Audit requires PyFlakes installed in your system.""" + print "Audit requires PyFlakes installed in your system." sys.exit(-1) warns = 0 @@ -100,6 +100,9 @@ setup( 'License :: OSI Approved :: BSD License', 'Operating System :: OS Independent', 'Programming Language :: Python', + 'Programming Language :: Python :: 2.5', + 'Programming Language :: Python :: 2.6', + 'Programming Language :: Python :: 2.7', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: Software Development :: Libraries :: Python Modules' ], diff --git a/tox.ini b/tox.ini index 91c6d664..82c1588e 100644 --- a/tox.ini +++ b/tox.ini @@ -3,3 +3,6 @@ envlist=py25,py26,py27,pypy [testenv] commands=python run-tests.py + +[testenv:py25] +deps=simplejson