Skip to content
Browse files

Merge branch 'master' into py3-dev

Conflicts:
	flask/helpers.py
	flask/testsuite/blueprints.py
	flask/testsuite/templating.py
	tox.ini
  • Loading branch information...
2 parents 7ab052c + 8339cb3 commit 709c40a64c6365c9840c7f9d17536b011c4a8cf5 @puzzlet committed
Showing with 1,197 additions and 319 deletions.
  1. +12 −0 .travis.yml
  2. +27 −0 CHANGES
  3. +0 −3 Makefile
  4. +53 −22 docs/api.rst
  5. +46 −2 docs/appcontext.rst
  6. +10 −0 docs/config.rst
  7. +0 −2 docs/deploying/fastcgi.rst
  8. +14 −0 docs/deploying/mod_wsgi.rst
  9. +2 −0 docs/patterns/index.rst
  10. +43 −0 docs/patterns/methodoverrides.rst
  11. +55 −0 docs/patterns/requestchecksum.rst
  12. +3 −3 docs/patterns/sqlalchemy.rst
  13. +59 −48 docs/patterns/sqlite3.rst
  14. +3 −3 docs/quickstart.rst
  15. +1 −1 docs/signals.rst
  16. +12 −3 docs/templating.rst
  17. +1 −1 docs/tutorial/dbinit.rst
  18. +20 −1 docs/upgrading.rst
  19. +22 −19 examples/flaskr/flaskr.py
  20. +44 −37 examples/minitwit/minitwit.py
  21. +12 −7 flask/__init__.py
  22. +73 −16 flask/app.py
  23. +28 −0 flask/blueprints.py
  24. +3 −2 flask/ctx.py
  25. +1 −2 flask/exceptions.py
  26. +1 −1 flask/exthook.py
  27. +5 −71 flask/helpers.py
  28. +167 −0 flask/json.py
  29. +0 −19 flask/session.py
  30. +111 −22 flask/sessions.py
  31. +5 −4 flask/templating.py
  32. +5 −1 flask/testing.py
  33. +4 −1 flask/testsuite/__init__.py
  34. +27 −1 flask/testsuite/basic.py
  35. +112 −4 flask/testsuite/blueprints.py
  36. +45 −0 flask/testsuite/helpers.py
  37. +6 −0 flask/testsuite/regression.py
  38. +4 −4 flask/testsuite/subclassing.py
  39. +3 −0 flask/testsuite/templates/template_test.html
  40. +96 −7 flask/testsuite/templating.py
  41. +39 −0 flask/testsuite/testing.py
  42. +18 −1 flask/testsuite/views.py
  43. +2 −4 flask/wrappers.py
  44. +1 −1 scripts/flaskext_compat.py
  45. +0 −1 setup.cfg
  46. +2 −1 setup.py
  47. +0 −4 tox.ini
View
12 .travis.yml
@@ -13,3 +13,15 @@ script: python setup.py test
branches:
except:
- website
+
+notifications:
+ # Disable travis notifications until they figured out how to hide
+ # their own builder failure from us. Travis currently fails way
+ # too many times by itself.
+ email: false
+
+ irc:
+ channels:
+ - "irc.freenode.org#pocoo"
+ use_notice: true
+ skip_join: true
View
27 CHANGES
@@ -8,6 +8,33 @@ Version 0.10
Release date to be decided.
+- Changed default cookie serialization format from pickle to JSON to
+ limit the impact an attacker can do if the secret key leaks. See
+ :ref:`upgrading-to-010` for more information.
+- Added ``template_test`` methods in addition to the already existing
+ ``template_filter`` method family.
+- Set the content-length header for x-sendfile.
+- ``tojson`` filter now does not escape script blocks in HTML5 parsers.
+- Flask will now raise an error if you attempt to register a new function
+ on an already used endpoint.
+- Added wrapper module around simplejson and added default serialization
+ of datetime objects. This allows much easier customization of how
+ JSON is handled by Flask or any Flask extension.
+- Removed deprecated internal ``flask.session`` module alias. Use
+ ``flask.sessions`` instead to get the session module. This is not to
+ be confused with ``flask.session`` the session proxy.
+- Templates can now be rendered without request context. The behavior is
+ slightly different as the ``request``, ``session`` and ``g`` objects
+ will not be available and blueprint's context processors are not
+ called.
+- The config object is now available to the template as a real global and
+ not through a context processor which makes it available even in imported
+ templates by default.
+- Added an option to generate non-ascii encoded JSON which should result
+ in less bytes being transmitted over the network. It's disabled by
+ default to not cause confusion with existing libraries that might expect
+ ``flask.json.dumps`` to return bytestrings by default.
+
Version 0.9
-----------
View
3 Makefile
@@ -11,9 +11,6 @@ audit:
release:
python scripts/make-release.py
-tox-test:
- PYTHONDONTWRITEBYTECODE= tox
-
ext-test:
python tests/flaskext_test.py --browse
View
75 docs/api.rst
@@ -215,6 +215,9 @@ implementation that Flask is using.
.. autoclass:: SecureCookieSessionInterface
:members:
+.. autoclass:: SecureCookieSession
+ :members:
+
.. autoclass:: NullSession
:members:
@@ -309,43 +312,71 @@ Message Flashing
.. autofunction:: get_flashed_messages
-Returning JSON
---------------
+JSON Support
+------------
-.. autofunction:: jsonify
+.. module:: flask.json
+
+Flask uses ``simplejson`` for the JSON implementation. Since simplejson
+is provided both by the standard library as well as extension Flask will
+try simplejson first and then fall back to the stdlib json module. On top
+of that it will delegate access to the current application's JSOn encoders
+and decoders for easier customization.
-.. data:: json
+So for starters instead of doing::
- If JSON support is picked up, this will be the module that Flask is
- using to parse and serialize JSON. So instead of doing this yourself::
+ try:
+ import simplejson as json
+ except ImportError:
+ import json
- try:
- import simplejson as json
- except ImportError:
- import json
+You can instead just do this::
- You can instead just do this::
+ from flask import json
- from flask import json
+For usage examples, read the :mod:`json` documentation in the standard
+lirbary. The following extensions are by default applied to the stdlib's
+JSON module:
- For usage examples, read the :mod:`json` documentation.
+1. ``datetime`` objects are serialized as :rfc:`822` strings.
+2. Any object with an ``__html__`` method (like :class:`~flask.Markup`)
+ will ahve that method called and then the return value is serialized
+ as string.
- The :func:`~json.dumps` function of this json module is also available
- as filter called ``|tojson`` in Jinja2. Note that inside `script`
- tags no escaping must take place, so make sure to disable escaping
- with ``|safe`` if you intend to use it inside `script` tags:
+The :func:`~htmlsafe_dumps` function of this json module is also available
+as filter called ``|tojson`` in Jinja2. Note that inside `script`
+tags no escaping must take place, so make sure to disable escaping
+with ``|safe`` if you intend to use it inside `script` tags:
- .. sourcecode:: html+jinja
+.. sourcecode:: html+jinja
+
+ <script type=text/javascript>
+ doSomethingWith({{ user.username|tojson|safe }});
+ </script>
+
+Note that the ``|tojson`` filter escapes forward slashes properly.
+
+.. autofunction:: jsonify
- <script type=text/javascript>
- doSomethingWith({{ user.username|tojson|safe }});
- </script>
+.. autofunction:: dumps
- Note that the ``|tojson`` filter escapes forward slashes properly.
+.. autofunction:: dump
+
+.. autofunction:: loads
+
+.. autofunction:: load
+
+.. autoclass:: JSONEncoder
+ :members:
+
+.. autoclass:: JSONDecoder
+ :members:
Template Rendering
------------------
+.. currentmodule:: flask
+
.. autofunction:: render_template
.. autofunction:: render_template_string
View
48 docs/appcontext.rst
@@ -37,7 +37,7 @@ context local.
Purpose of the Application Context
----------------------------------
-The main reason for the application's context existance is that in the
+The main reason for the application's context existence is that in the
past a bunch of functionality was attached to the request context in lack
of a better solution. Since one of the pillar's of Flask's design is that
you can have more than one application in the same Python process.
@@ -58,7 +58,7 @@ Creating an Application Context
To make an application context there are two ways. The first one is the
implicit one: whenever a request context is pushed, an application context
will be created alongside if this is necessary. As a result of that, you
-can ignore the existance of the application context unless you need it.
+can ignore the existence of the application context unless you need it.
The second way is the explicit way using the
:meth:`~flask.Flask.app_context` method::
@@ -85,3 +85,47 @@ Extensions are free to store additional information on the topmost level,
assuming they pick a sufficiently unique name.
For more information about that, see :ref:`extension-dev`.
+
+Context Usage
+-------------
+
+The context is typically used to cache resources on there that need to be
+created on a per-request or usage case. For instance database connects
+are destined to go there. When storing things on the application context
+unique names should be chosen as this is a place that is shared between
+Flask applications and extensions.
+
+The most common usage is to split resource management into two parts:
+
+1. an implicit resource caching on the context.
+2. a context teardown based resource deallocation.
+
+Generally there would be a ``get_X()`` function that creates resource
+``X`` if it does not exist yet and otherwise returns the same resource,
+and a ``teardown_X()`` function that is registered as teardown handler.
+
+This is an example that connects to a database::
+
+ import sqlite3
+ from flask import _app_ctx_stack
+
+ def get_db():
+ top = _app_ctx_stack.top
+ if not hasattr(top, 'database'):
+ top.database = connect_to_database()
+ return top.database
+
+ @app.teardown_appcontext
+ def teardown_db(exception):
+ top = _app_ctx_stack.top
+ if hasattr(top, 'database'):
+ top.database.close()
+
+The first time ``get_db()`` is called the connection will be established.
+To make this implicit a :class:`~werkzeug.local.LocalProxy` can be used::
+
+ from werkzeug.local import LocalProxy
+ db = LocalProxy(get_db)
+
+That way a user can directly access ``db`` which internally calls
+``get_db()``.
View
10 docs/config.rst
@@ -142,6 +142,13 @@ The following configuration values are used internally by Flask:
``PREFERRED_URL_SCHEME`` The URL scheme that should be used for
URL generation if no URL scheme is
available. This defaults to ``http``.
+``JSON_AS_ASCII`` By default Flask serialize object to
+ ascii-encoded JSON. If this is set to
+ ``False`` Flask will not encode to ASCII
+ and output strings as-is and return
+ unicode strings. ``jsonfiy`` will
+ automatically encode it in ``utf-8``
+ then for transport for instance.
================================= =========================================
.. admonition:: More on ``SERVER_NAME``
@@ -184,6 +191,9 @@ The following configuration values are used internally by Flask:
.. versionadded:: 0.9
``PREFERRED_URL_SCHEME``
+.. versionadded:: 0.10
+ ``JSON_AS_ASCII``
+
Configuring from Files
----------------------
View
2 docs/deploying/fastcgi.rst
@@ -95,8 +95,6 @@ Set yourapplication.fcgi::
from yourapplication import app
class ScriptNameStripper(object):
- to_strip = '/yourapplication.fcgi'
-
def __init__(self, app):
self.app = app
View
14 docs/deploying/mod_wsgi.rst
@@ -91,6 +91,20 @@ execute the application under a different user for security reasons:
</Directory>
</VirtualHost>
+Note: WSGIDaemonProcess isn't implemented in Windows and Apache will
+refuse to run with the above configuration. On a Windows system, eliminate those lines:
+
+.. sourcecode:: apache
+
+ <VirtualHost *>
+ ServerName example.com
+ WSGIScriptAlias / C:\yourdir\yourapp.wsgi
+ <Directory C:\yourdir>
+ Order deny,allow
+ Allow from all
+ </Directory>
+ </VirtualHost>
+
For more information consult the `mod_wsgi wiki`_.
.. _mod_wsgi: http://code.google.com/p/modwsgi/
View
2 docs/patterns/index.rst
@@ -37,3 +37,5 @@ Snippet Archives <http://flask.pocoo.org/snippets/>`_.
favicon
streaming
deferredcallbacks
+ methodoverrides
+ requestchecksum
View
43 docs/patterns/methodoverrides.rst
@@ -0,0 +1,43 @@
+Adding HTTP Method Overrides
+============================
+
+Some HTTP proxies do not support arbitrary HTTP methods or newer HTTP
+methods (such as PATCH). In that case it's possible to “proxy” HTTP
+methods through another HTTP method in total violation of the protocol.
+
+The way this works is by letting the client do an HTTP POST request and
+set the ``X-HTTP-Method-Override`` header and set the value to the
+intended HTTP method (such as ``PATCH``).
+
+This can easily be accomplished with an HTTP middleware::
+
+ class HTTPMethodOverrideMiddleware(object):
+ allowed_methods = frozenset([
+ 'GET',
+ 'HEAD',
+ 'POST',
+ 'DELETE',
+ 'PUT',
+ 'PATCH',
+ 'OPTIONS'
+ ])
+ bodyless_methods = frozenset(['GET', 'HEAD', 'OPTIONS', 'DELETE'])
+
+ def __init__(self, app):
+ self.app = app
+
+ def __call__(self, environ, start_response):
+ method = environ.get('HTTP_X_HTTP_METHOD_OVERRIDE', '').upper()
+ if method in self.allowed_methods:
+ method = method.encode('ascii', 'replace')
+ environ['REQUEST_METHOD'] = method
+ if method in self.bodyless_methods:
+ environ['CONTENT_LENGTH'] = '0'
+ return self.app(environ, start_response)
+
+To use this with Flask this is all that is necessary::
+
+ from flask import Flask
+
+ app = Flask(__name__)
+ app.wsgi_app = HTTPMethodOverrideMiddleware(app.wsgi_app)
View
55 docs/patterns/requestchecksum.rst
@@ -0,0 +1,55 @@
+Request Content Checksums
+=========================
+
+Various pieces of code can consume the request data and preprocess it.
+For instance JSON data ends up on the request object already read and
+processed, form data ends up there as well but goes through a different
+code path. This seems inconvenient when you want to calculate the
+checksum of the incoming request data. This is necessary sometimes for
+some APIs.
+
+Fortunately this is however very simple to change by wrapping the input
+stream.
+
+The following example calculates the SHA1 checksum of the incoming data as
+it gets read and stores it in the WSGI environment::
+
+ import hashlib
+
+ class ChecksumCalcStream(object):
+
+ def __init__(self, stream):
+ self._stream = stream
+ self._hash = hashlib.sha1()
+
+ def read(self, bytes):
+ rv = self._stream.read(bytes)
+ self._hash.update(rv)
+ return rv
+
+ def readline(self, size_hint):
+ rv = self._stream.readline(size_hint)
+ self._hash.update(rv)
+ return rv
+
+ def generate_checksum(request):
+ env = request.environ
+ stream = ChecksumCalcStream(env['wsgi.input'])
+ env['wsgi.input'] = stream
+ return stream._hash
+
+To use this, all you need to do is to hook the calculating stream in
+before the request starts consuming data. (Eg: be careful accessing
+``request.form`` or anything of that nature. ``before_request_handlers``
+for instance should be careful not to access it).
+
+Example usage::
+
+ @app.route('/special-api', methods=['POST'])
+ def special_api():
+ hash = generate_checksum(request)
+ # Accessing this parses the input stream
+ files = request.files
+ # At this point the hash is fully constructed.
+ checksum = hash.hexdigest()
+ return 'Hash was: %s' % checksum
View
6 docs/patterns/sqlalchemy.rst
@@ -42,7 +42,7 @@ Here the example `database.py` module for your application::
engine = create_engine('sqlite:////tmp/test.db', convert_unicode=True)
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
- bind=engine))
+ bind=engine))
Base = declarative_base()
Base.query = db_session.query_property()
@@ -130,7 +130,7 @@ Here is an example `database.py` module for your application::
metadata = MetaData()
db_session = scoped_session(sessionmaker(autocommit=False,
autoflush=False,
- bind=engine))
+ bind=engine))
def init_db():
metadata.create_all(bind=engine)
@@ -189,7 +189,7 @@ To insert data you can use the `insert` method. We have to get a
connection first so that we can use a transaction:
>>> con = engine.connect()
->>> con.execute(users.insert(name='admin', email='admin@localhost'))
+>>> con.execute(users.insert(), name='admin', email='admin@localhost')
SQLAlchemy will automatically commit for us.
View
107 docs/patterns/sqlite3.rst
@@ -3,61 +3,61 @@
Using SQLite 3 with Flask
=========================
-In Flask you can implement the opening of database connections at the
-beginning of the request and closing at the end with the
-:meth:`~flask.Flask.before_request` and :meth:`~flask.Flask.teardown_request`
-decorators in combination with the special :class:`~flask.g` object.
+In Flask you can implement the opening of database connections on demand
+and closing it when the context dies (usually at the end of the request)
+easily.
-So here is a simple example of how you can use SQLite 3 with Flask::
+Here is a simple example of how you can use SQLite 3 with Flask::
import sqlite3
- from flask import g
+ from flask import _app_ctx_stack
DATABASE = '/path/to/database.db'
- def connect_db():
- return sqlite3.connect(DATABASE)
+ def get_db():
+ top = _app_ctx_stack.top
+ if not hasattr(top, 'sqlite_db'):
+ top.sqlite_db = sqlite3.connect(DATABASE)
+ return top.sqlite_db
- @app.before_request
- def before_request():
- g.db = connect_db()
+ @app.teardown_appcontext
+ def close_connection(exception):
+ top = _app_ctx_stack.top
+ if hasattr(top, 'sqlite_db'):
+ top.sqlite_db.close()
- @app.teardown_request
- def teardown_request(exception):
- if hasattr(g, 'db'):
- g.db.close()
+All the application needs to do in order to now use the database is having
+an active application context (which is always true if there is an request
+in flight) or to create an application context itself. At that point the
+``get_db`` function can be used to get the current database connection.
+Whenever the context is destroyed the database connection will be
+terminated.
-.. note::
+Example::
- Please keep in mind that the teardown request functions are always
- executed, even if a before-request handler failed or was never
- executed. Because of this we have to make sure here that the database
- is there before we close it.
+ @app.route('/')
+ def index():
+ cur = get_db().cursor()
+ ...
-Connect on Demand
------------------
-The downside of this approach is that this will only work if Flask
-executed the before-request handlers for you. If you are attempting to
-use the database from a script or the interactive Python shell you would
-have to do something like this::
+.. note::
- with app.test_request_context():
- app.preprocess_request()
- # now you can use the g.db object
+ Please keep in mind that the teardown request and appcontext functions
+ are always executed, even if a before-request handler failed or was
+ never executed. Because of this we have to make sure here that the
+ database is there before we close it.
-In order to trigger the execution of the connection code. You won't be
-able to drop the dependency on the request context this way, but you could
-make it so that the application connects when necessary::
+Connect on Demand
+-----------------
- def get_connection():
- db = getattr(g, '_db', None)
- if db is None:
- db = g._db = connect_db()
- return db
+The upside of this approach (connecting on first use) is that this will
+only opening the connection if truly necessary. If you want to use this
+code outside a request context you can use it in a Python shell by opening
+the application context by hand::
-Downside here is that you have to use ``db = get_connection()`` instead of
-just being able to use ``g.db`` directly.
+ with app.app_context():
+ # now you can use get_db()
.. _easy-querying:
@@ -66,16 +66,28 @@ Easy Querying
Now in each request handling function you can access `g.db` to get the
current open database connection. To simplify working with SQLite, a
-helper function can be useful::
+row factory function is useful. It is executed for every result returned
+from the database to convert the result. For instance in order to get
+dictionaries instead of tuples this can be used::
+
+ def make_dicts(cursor, row):
+ return dict((cur.description[idx][0], value)
+ for idx, value in enumerate(row))
+
+ db.row_factory = make_dicts
+Additionally it is a good idea to provide a query function that combines
+getting the cursor, executing and fetching the results::
+
def query_db(query, args=(), one=False):
- cur = g.db.execute(query, args)
- rv = [dict((cur.description[idx][0], value)
- for idx, value in enumerate(row)) for row in cur.fetchall()]
+ cur = get_db().execute(query, args)
+ rv = cur.fetchall()
+ cur.close()
return (rv[0] if rv else None) if one else rv
-This handy little function makes working with the database much more
-pleasant than it is by just using the raw cursor and connection objects.
+This handy little function in combination with a row factory makes working
+with the database much more pleasant than it is by just using the raw
+cursor and connection objects.
Here is how you can use it::
@@ -105,10 +117,9 @@ Relational databases need schemas, so applications often ship a
a function that creates the database based on that schema. This function
can do that for you::
- from contextlib import closing
-
def init_db():
- with closing(connect_db()) as db:
+ with app.app_context():
+ db = get_db()
with app.open_resource('schema.sql') as f:
db.cursor().executescript(f.read())
db.commit()
View
6 docs/quickstart.rst
@@ -627,9 +627,9 @@ For this also see :ref:`about-responses`.
Redirects and Errors
--------------------
-To redirect a user to somewhere else you can use the
-:func:`~flask.redirect` function. To abort a request early with an error
-code use the :func:`~flask.abort` function. Here an example how this works::
+To redirect a user to another endpoint, use the :func:`~flask.redirect`
+function; to abort a request early with an error code, use the
+:func:`~flask.abort` function::
from flask import abort, redirect, url_for
View
2 docs/signals.rst
@@ -285,7 +285,7 @@ The following signals exist in Flask:
def close_db_connection(sender, **extra):
session.close()
- from flask import request_tearing_down
+ from flask import appcontext_tearing_down
appcontext_tearing_down.connect(close_db_connection, app)
This will also be passed an `exc` keyword argument that has a reference
View
15 docs/templating.rst
@@ -38,20 +38,29 @@ by default:
.. versionadded:: 0.6
+ .. versionchanged:: 0.10
+ This is now always available, even in imported templates.
+
.. data:: request
:noindex:
- The current request object (:class:`flask.request`)
+ The current request object (:class:`flask.request`). This variable is
+ unavailable if the template was rendered without an active request
+ context.
.. data:: session
:noindex:
- The current session object (:class:`flask.session`)
+ The current session object (:class:`flask.session`). This variable
+ is unavailable if the template was rendered without an active request
+ context.
.. data:: g
:noindex:
- The request-bound object for global variables (:data:`flask.g`)
+ The request-bound object for global variables (:data:`flask.g`). This
+ variable is unavailable if the template was rendered without an active
+ request context.
.. function:: url_for
:noindex:
View
2 docs/tutorial/dbinit.rst
@@ -32,7 +32,7 @@ add the following lines to your existing imports in `flaskr.py`::
Next we can create a function called `init_db` that initializes the
database. For this we can use the `connect_db` function we defined
earlier. Just add that function below the `connect_db` function in
-`flask.py`::
+`flaskr.py`::
def init_db():
with closing(connect_db()) as db:
View
21 docs/upgrading.rst
@@ -19,6 +19,25 @@ installation, make sure to pass it the ``-U`` parameter::
$ easy_install -U Flask
+.. _upgrading-to-010:
+
+Version 0.10
+------------
+
+The biggest change going from 0.9 to 0.10 is that the cookie serialization
+format changed from pickle to a specialized JSON format. This change has
+been done in order to avoid the damage an attacker can do if the secret
+key is leaked. When you upgrade you will notice two major changes: all
+sessions that were issued before the upgrade are invalidated and you can
+only store a limited amount of types in the session. The new sessions are
+by design much more restricted to only allow JSON with a few small
+extensions for tuples and strings with HTML markup.
+
+In order to not break people's sessions it is possible to continue using
+the old session system by using the `Flask-OldSessions_` extension.
+
+.. _Flask-OldSessions: http://packages.python.org/Flask-OldSessions/
+
Version 0.9
-----------
@@ -191,7 +210,7 @@ Manual Error Handler Attaching
While it is still possible to attach error handlers to
:attr:`Flask.error_handlers` it's discouraged to do so and in fact
-deprecated. In generaly we no longer recommend custom error handler
+deprecated. In general we no longer recommend custom error handler
attaching via assignments to the underlying dictionary due to the more
complex internal handling to support arbitrary exception classes and
blueprints. See :meth:`Flask.errorhandler` for more information.
View
41 examples/flaskr/flaskr.py
@@ -11,9 +11,8 @@
"""
from sqlite3 import dbapi2 as sqlite3
-from contextlib import closing
from flask import Flask, request, session, g, redirect, url_for, abort, \
- render_template, flash
+ render_template, flash, _app_ctx_stack
# configuration
DATABASE = '/tmp/flaskr.db'
@@ -28,35 +27,37 @@
app.config.from_envvar('FLASKR_SETTINGS', silent=True)
-def connect_db():
- """Returns a new connection to the database."""
- return sqlite3.connect(app.config['DATABASE'])
-
-
def init_db():
"""Creates the database tables."""
- with closing(connect_db()) as db:
+ with app.app_context():
+ db = get_db()
with app.open_resource('schema.sql') as f:
db.cursor().executescript(f.read().decode('utf-8'))
db.commit()
-@app.before_request
-def before_request():
- """Make sure we are connected to the database each request."""
- g.db = connect_db()
+def get_db():
+ """Opens a new database connection if there is none yet for the
+ current application context.
+ """
+ top = _app_ctx_stack.top
+ if not hasattr(top, 'sqlite_db'):
+ top.sqlite_db = sqlite3.connect(app.config['DATABASE'])
+ return top.sqlite_db
-@app.teardown_request
-def teardown_request(exception):
+@app.teardown_appcontext
+def close_db_connection(exception):
"""Closes the database again at the end of the request."""
- if hasattr(g, 'db'):
- g.db.close()
+ top = _app_ctx_stack.top
+ if hasattr(top, 'sqlite_db'):
+ top.sqlite_db.close()
@app.route('/')
def show_entries():
- cur = g.db.execute('select title, text from entries order by id desc')
+ db = get_db()
+ cur = db.execute('select title, text from entries order by id desc')
entries = [dict(title=row[0], text=row[1]) for row in cur.fetchall()]
return render_template('show_entries.html', entries=entries)
@@ -65,9 +66,10 @@ def show_entries():
def add_entry():
if not session.get('logged_in'):
abort(401)
- g.db.execute('insert into entries (title, text) values (?, ?)',
+ db = get_db()
+ db.execute('insert into entries (title, text) values (?, ?)',
[request.form['title'], request.form['text']])
- g.db.commit()
+ db.commit()
flash('New entry was successfully posted')
return redirect(url_for('show_entries'))
@@ -95,4 +97,5 @@ def logout():
if __name__ == '__main__':
+ init_db()
app.run()
View
81 examples/minitwit/minitwit.py
@@ -13,9 +13,8 @@
from sqlite3 import dbapi2 as sqlite3
from hashlib import md5
from datetime import datetime
-from contextlib import closing
from flask import Flask, request, session, url_for, redirect, \
- render_template, abort, g, flash
+ render_template, abort, g, flash, _app_ctx_stack
from werkzeug import check_password_hash, generate_password_hash
@@ -31,14 +30,29 @@
app.config.from_envvar('MINITWIT_SETTINGS', silent=True)
-def connect_db():
- """Returns a new connection to the database."""
- return sqlite3.connect(app.config['DATABASE'])
+def get_db():
+ """Opens a new database connection if there is none yet for the
+ current application context.
+ """
+ top = _app_ctx_stack.top
+ if not hasattr(top, 'sqlite_db'):
+ top.sqlite_db = sqlite3.connect(app.config['DATABASE'])
+ top.sqlite_db.row_factory = sqlite3.Row
+ return top.sqlite_db
+
+
+@app.teardown_appcontext
+def close_database(exception):
+ """Closes the database again at the end of the request."""
+ top = _app_ctx_stack.top
+ if hasattr(top, 'sqlite_db'):
+ top.sqlite_db.close()
def init_db():
"""Creates the database tables."""
- with closing(connect_db()) as db:
+ with app.app_context():
+ db = get_db()
with app.open_resource('schema.sql') as f:
db.cursor().executescript(f.read().decode('utf-8'))
db.commit()
@@ -46,16 +60,15 @@ def init_db():
def query_db(query, args=(), one=False):
"""Queries the database and returns a list of dictionaries."""
- cur = g.db.execute(query, args)
- rv = [dict((cur.description[idx][0], value)
- for idx, value in enumerate(row)) for row in cur.fetchall()]
+ cur = get_db().execute(query, args)
+ rv = cur.fetchall()
return (rv[0] if rv else None) if one else rv
def get_user_id(username):
"""Convenience method to look up the id for a username."""
- rv = g.db.execute('select user_id from user where username = ?',
- [username]).fetchone()
+ rv = query_db('select user_id from user where username = ?',
+ [username], one=True)
return rv[0] if rv else None
@@ -72,23 +85,12 @@ def gravatar_url(email, size=80):
@app.before_request
def before_request():
- """Make sure we are connected to the database each request and look
- up the current user so that we know he's there.
- """
- g.db = connect_db()
g.user = None
if 'user_id' in session:
g.user = query_db('select * from user where user_id = ?',
[session['user_id']], one=True)
-@app.teardown_request
-def teardown_request(exception):
- """Closes the database again at the end of the request."""
- if hasattr(g, 'db'):
- g.db.close()
-
-
@app.route('/')
def timeline():
"""Shows a users timeline or if no user is logged in it will
@@ -145,9 +147,10 @@ def follow_user(username):
whom_id = get_user_id(username)
if whom_id is None:
abort(404)
- g.db.execute('insert into follower (who_id, whom_id) values (?, ?)',
- [session['user_id'], whom_id])
- g.db.commit()
+ db = get_db()
+ db.execute('insert into follower (who_id, whom_id) values (?, ?)',
+ [session['user_id'], whom_id])
+ db.commit()
flash('You are now following "%s"' % username)
return redirect(url_for('user_timeline', username=username))
@@ -160,9 +163,10 @@ def unfollow_user(username):
whom_id = get_user_id(username)
if whom_id is None:
abort(404)
- g.db.execute('delete from follower where who_id=? and whom_id=?',
- [session['user_id'], whom_id])
- g.db.commit()
+ db = get_db()
+ db.execute('delete from follower where who_id=? and whom_id=?',
+ [session['user_id'], whom_id])
+ db.commit()
flash('You are no longer following "%s"' % username)
return redirect(url_for('user_timeline', username=username))
@@ -173,10 +177,11 @@ def add_message():
if 'user_id' not in session:
abort(401)
if request.form['text']:
- g.db.execute('''insert into message (author_id, text, pub_date)
- values (?, ?, ?)''', (session['user_id'], request.form['text'],
- int(time.time())))
- g.db.commit()
+ db = get_db()
+ db.execute('''insert into message (author_id, text, pub_date)
+ values (?, ?, ?)''', (session['user_id'], request.form['text'],
+ int(time.time())))
+ db.commit()
flash('Your message was recorded')
return redirect(url_for('timeline'))
@@ -221,11 +226,12 @@ def register():
elif get_user_id(request.form['username']) is not None:
error = 'The username is already taken'
else:
- g.db.execute('''insert into user (
- username, email, pw_hash) values (?, ?, ?)''',
- [request.form['username'], request.form['email'],
- generate_password_hash(request.form['password'])])
- g.db.commit()
+ db = get_db()
+ db.execute('''insert into user (
+ username, email, pw_hash) values (?, ?, ?)''',
+ [request.form['username'], request.form['email'],
+ generate_password_hash(request.form['password'])])
+ db.commit()
flash('You were successfully registered and can login now')
return redirect(url_for('login'))
return render_template('register.html', error=error)
@@ -245,4 +251,5 @@ def logout():
if __name__ == '__main__':
+ init_db()
app.run()
View
19 flask/__init__.py
@@ -20,9 +20,8 @@
from .app import Flask, Request, Response
from .config import Config
-from .helpers import url_for, jsonify, json_available, flash, \
- send_file, send_from_directory, get_flashed_messages, \
- get_template_attribute, make_response, safe_join, \
+from .helpers import url_for, flash, send_file, send_from_directory, \
+ get_flashed_messages, get_template_attribute, make_response, safe_join, \
stream_with_context
from .globals import current_app, g, request, session, _request_ctx_stack, \
_app_ctx_stack
@@ -34,11 +33,17 @@
# the signals
from .signals import signals_available, template_rendered, request_started, \
- request_finished, got_request_exception, request_tearing_down
+ request_finished, got_request_exception, request_tearing_down, \
+ appcontext_tearing_down
-# only import json if it's available
-if json_available:
- from .helpers import json
+# We're not exposing the actual json module but a convenient wrapper around
+# it.
+from . import json
+
+# This was the only thing that flask used to export at one point and it had
+# a more generic name.
+jsonify = json.jsonify
# backwards compat, goes away in 1.0
from .sessions import SecureCookieSession as Session
+json_available = True
View
89 flask/app.py
@@ -24,8 +24,8 @@
MethodNotAllowed, BadRequest
from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
- locked_cached_property, _tojson_filter, _endpoint_from_view_func, \
- find_package
+ locked_cached_property, _endpoint_from_view_func, find_package
+from . import json
from .wrappers import Request, Response
from .config import ConfigAttribute, Config
from .ctx import RequestContext, AppContext, _RequestGlobals
@@ -238,6 +238,16 @@ class Flask(_PackageBoundObject):
'-' * 80
)
+ #: The JSON encoder class to use. Defaults to :class:`~flask.json.JSONEncoder`.
+ #:
+ #: .. versionadded:: 0.10
+ json_encoder = json.JSONEncoder
+
+ #: The JSON decoder class to use. Defaults to :class:`~flask.json.JSONDecoder`.
+ #:
+ #: .. versionadded:: 0.10
+ json_decoder = json.JSONDecoder
+
#: Options that are passed directly to the Jinja2 environment.
jinja_options = ImmutableDict(
extensions=['jinja2.ext.autoescape', 'jinja2.ext.with_']
@@ -264,7 +274,8 @@ class Flask(_PackageBoundObject):
'SEND_FILE_MAX_AGE_DEFAULT': 12 * 60 * 60, # 12 hours
'TRAP_BAD_REQUEST_ERRORS': False,
'TRAP_HTTP_EXCEPTIONS': False,
- 'PREFERRED_URL_SCHEME': 'http'
+ 'PREFERRED_URL_SCHEME': 'http',
+ 'JSON_AS_ASCII': True
})
#: The rule object to use for URL rules created. This is used by
@@ -541,8 +552,8 @@ def logger(self):
Here some examples::
app.logger.debug('A value for debugging')
- app.logger.warning('A warning ocurred (%d apples)', 42)
- app.logger.error('An error occoured')
+ app.logger.warning('A warning occurred (%d apples)', 42)
+ app.logger.error('An error occurred')
.. versionadded:: 0.3
"""
@@ -635,9 +646,10 @@ def create_jinja_environment(self):
rv = Environment(self, **options)
rv.globals.update(
url_for=url_for,
- get_flashed_messages=get_flashed_messages
+ get_flashed_messages=get_flashed_messages,
+ config=self.config
)
- rv.filters['tojson'] = _tojson_filter
+ rv.filters['tojson'] = json.htmlsafe_dumps
return rv
def create_global_jinja_loader(self):
@@ -684,9 +696,11 @@ def update_template_context(self, context):
to add extra variables.
"""
funcs = self.template_context_processors[None]
- bp = _request_ctx_stack.top.request.blueprint
- if bp is not None and bp in self.template_context_processors:
- funcs = chain(funcs, self.template_context_processors[bp])
+ reqctx = _request_ctx_stack.top
+ if reqctx is not None:
+ bp = reqctx.request.blueprint
+ if bp is not None and bp in self.template_context_processors:
+ funcs = chain(funcs, self.template_context_processors[bp])
orig_ctx = context.copy()
for func in funcs:
context.update(func())
@@ -846,7 +860,7 @@ def register_blueprint(self, blueprint, **options):
first_registration = False
if blueprint.name in self.blueprints:
assert self.blueprints[blueprint.name] is blueprint, \
- 'A blueprint\'s name collision ocurred between %r and ' \
+ 'A blueprint\'s name collision occurred between %r and ' \
'%r. Both share the same name "%s". Blueprints that ' \
'are created on the fly need unique names.' % \
(blueprint, self.blueprints[blueprint.name], blueprint.name)
@@ -942,8 +956,13 @@ def index():
rule = self.url_rule_class(rule, methods=methods, **options)
rule.provide_automatic_options = provide_automatic_options
+
self.url_map.add(rule)
if view_func is not None:
+ old_func = self.view_functions.get(endpoint)
+ if old_func is not None and old_func is not view_func:
+ raise AssertionError('View function mapping is overwriting an '
+ 'existing endpoint function: %s' % endpoint)
self.view_functions[endpoint] = view_func
def route(self, rule, **options):
@@ -1087,6 +1106,44 @@ def add_template_filter(self, f, name=None):
self.jinja_env.filters[name or f.__name__] = f
@setupmethod
+ def template_test(self, name=None):
+ """A decorator that is used to register custom template test.
+ You can specify a name for the test, otherwise the function
+ name will be used. Example::
+
+ @app.template_test()
+ def is_prime(n):
+ if n == 2:
+ return True
+ for i in xrange(2, int(math.ceil(math.sqrt(n))) + 1):
+ if n % i == 0:
+ return False
+ return True
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
+ """
+ def decorator(f):
+ self.add_template_test(f, name=name)
+ return f
+ return decorator
+
+ @setupmethod
+ def add_template_test(self, f, name=None):
+ """Register a custom template test. Works exactly like the
+ :meth:`template_test` decorator.
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
+ """
+ self.jinja_env.tests[name or f.__name__] = f
+
+
+ @setupmethod
def before_request(self, f):
"""Registers a function to run before each request."""
self.before_request_funcs.setdefault(None, []).append(f)
@@ -1108,7 +1165,7 @@ def after_request(self, f):
a new response object or the same (see :meth:`process_response`).
As of Flask 0.7 this function might not be executed at the end of the
- request in case an unhandled exception ocurred.
+ request in case an unhandled exception occurred.
"""
self.after_request_funcs.setdefault(None, []).append(f)
return f
@@ -1132,10 +1189,10 @@ def teardown_request(self, f):
stack of active contexts. This becomes relevant if you are using
such constructs in tests.
- Generally teardown functions must take every necesary step to avoid
+ Generally teardown functions must take every necessary step to avoid
that they will fail. If they do execute code that might fail they
will have to surround the execution of these code by try/except
- statements and log ocurring errors.
+ statements and log occurring errors.
When a teardown function was called because of a exception it will
be passed an error object.
@@ -1522,7 +1579,7 @@ def preprocess_request(self):
request handling is stopped.
This also triggers the :meth:`url_value_processor` functions before
- the actualy :meth:`before_request` functions are called.
+ the actual :meth:`before_request` functions are called.
"""
bp = _request_ctx_stack.top.request.blueprint
@@ -1675,7 +1732,7 @@ def wsgi_app(self, environ, start_response):
The behavior of the before and after request callbacks was changed
under error conditions and a new callback was added that will
always execute at the end of the request, independent on if an
- error ocurred or not. See :ref:`callbacks-and-errors`.
+ error occurred or not. See :ref:`callbacks-and-errors`.
:param environ: a WSGI environment
:param start_response: a callable accepting a status code,
View
28 flask/blueprints.py
@@ -209,6 +209,34 @@ def register_template(state):
state.app.jinja_env.filters[name or f.__name__] = f
self.record_once(register_template)
+ def app_template_test(self, name=None):
+ """Register a custom template test, available application wide. Like
+ :meth:`Flask.template_test` but for a blueprint.
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
+ """
+ def decorator(f):
+ self.add_app_template_test(f, name=name)
+ return f
+ return decorator
+
+ def add_app_template_test(self, f, name=None):
+ """Register a custom template test, available application wide. Like
+ :meth:`Flask.add_template_test` but for a blueprint. Works exactly
+ like the :meth:`app_template_test` decorator.
+
+ .. versionadded:: 0.10
+
+ :param name: the optional name of the test, otherwise the
+ function name will be used.
+ """
+ def register_template(state):
+ state.app.jinja_env.tests[name or f.__name__] = f
+ self.record_once(register_template)
+
def before_request(self, f):
"""Like :meth:`Flask.before_request` but for a blueprint. This function
is only executed before each request that is handled by a function of
View
5 flask/ctx.py
@@ -217,7 +217,7 @@ def push(self):
# on the stack. The rationale is that you want to access that
# information under debug situations. However if someone forgets to
# pop that context again we want to make sure that on the next push
- # it's invalidated otherwise we run at risk that something leaks
+ # it's invalidated, otherwise we run at risk that something leaks
# memory. This is usually only a problem in testsuite since this
# functionality is not active in production environments.
top = _request_ctx_stack.top
@@ -238,7 +238,8 @@ def push(self):
# Open the session at the moment that the request context is
# available. This allows a custom open_session method to use the
- # request context (e.g. flask-sqlalchemy).
+ # request context (e.g. code that access database information
+ # stored on `g` instead of the appcontext).
self.session = self.app.open_session(self.request)
if self.session is None:
self.session = self.app.make_null_session()
View
3 flask/exceptions.py
@@ -9,7 +9,7 @@
:license: BSD, see LICENSE for more details.
"""
from werkzeug.exceptions import HTTPException, BadRequest
-from .helpers import json
+from . import json
class JSONHTTPException(HTTPException):
@@ -39,7 +39,6 @@ def get_headers(self, environ):
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.
View
2 flask/exthook.py
@@ -110,7 +110,7 @@ def is_important_frame(self, important_module, tb):
if module_name == important_module:
return True
- # Some python verisons will will clean up modules so early that the
+ # Some python versions will will clean up modules so early that the
# module name at that point is no longer set. Try guessing from
# the filename then.
filename = os.path.abspath(tb.tb_frame.f_code.co_filename)
View
76 flask/helpers.py
@@ -23,22 +23,6 @@
from werkzeug.urls import url_quote
from functools import update_wrapper
-# try to load the best simplejson implementation available. If JSON
-# is not installed, we add a failing class.
-json_available = True
-json = None
-try:
- import simplejson as json
-except ImportError:
- try:
- import json
- except ImportError:
- try:
- # Google Appengine offers simplejson via django
- from django.utils import simplejson as json
- except ImportError:
- json_available = False
-
from werkzeug.datastructures import Headers
from werkzeug.exceptions import NotFound
@@ -55,24 +39,6 @@
current_app, request
-def _assert_have_json():
- """Helper function that fails if JSON is unavailable."""
- if not json_available:
- raise RuntimeError('simplejson not installed')
-
-
-# figure out if simplejson escapes slashes. This behavior was changed
-# from one version to another without reason.
-if not json_available or '\\/' not in json.dumps('/'):
-
- def _tojson_filter(*args, **kwargs):
- if __debug__:
- _assert_have_json()
- return json.dumps(*args, **kwargs).replace('/', '\\/')
-else:
- _tojson_filter = json.dumps
-
-
# sentinel
_missing = object()
@@ -165,39 +131,6 @@ def generator():
return wrapped_g
-def jsonify(*args, **kwargs):
- """Creates a :class:`~flask.Response` with the JSON representation of
- the given arguments with an `application/json` mimetype. The arguments
- to this function are the same as to the :class:`dict` constructor.
-
- Example usage::
-
- @app.route('/_get_current_user')
- def get_current_user():
- return jsonify(username=g.user.username,
- email=g.user.email,
- id=g.user.id)
-
- This will send a JSON response like this to the browser::
-
- {
- "username": "admin",
- "email": "admin@localhost",
- "id": 42
- }
-
- This requires Python 2.6 or an installed version of simplejson. For
- security reasons only objects are supported toplevel. For more
- information about this, have a look at :ref:`json-security`.
-
- .. versionadded:: 0.2
- """
- if __debug__:
- _assert_have_json()
- return current_app.response_class(json.dumps(dict(*args, **kwargs),
- indent=None if request.is_xhr else 2), mimetype='application/json')
-
-
def make_response(*args):
"""Sometimes it is necessary to set additional headers in a view. Because
views do not have to return response objects but can return a value that
@@ -362,8 +295,6 @@ def external_url_handler(error, endpoint, **values):
values['_method'] = method
return appctx.app.handle_url_build_error(error, endpoint, values)
- rv = url_adapter.build(endpoint, values, method=method,
- force_external=external)
if anchor is not None:
rv += '#' + url_quote(anchor)
return rv
@@ -564,6 +495,7 @@ 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)
data = None
else:
if file is None:
@@ -591,7 +523,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
os.path.getmtime(filename),
os.path.getsize(filename),
adler32(
- filename.encode('utf8') if isinstance(filename, str)
+ filename.encode('utf-8') if isinstance(filename, str)
else filename
) & 0xffffffff
))
@@ -624,7 +556,9 @@ def wiki_page(filename):
for sep in _os_alt_seps:
if sep in filename:
raise NotFound()
- if os.path.isabs(filename) or filename.startswith('../'):
+ if os.path.isabs(filename) or \
+ filename == '..' or \
+ filename.startswith('../'):
raise NotFound()
return os.path.join(directory, filename)
View
167 flask/json.py
@@ -0,0 +1,167 @@
+# -*- coding: utf-8 -*-
+"""
+ flask.jsonimpl
+ ~~~~~~~~~~~~~~
+
+ Implementation helpers for the JSON support in Flask.
+
+ :copyright: (c) 2012 by Armin Ronacher.
+ :license: BSD, see LICENSE for more details.
+"""
+from datetime import datetime
+from .globals import current_app, request
+
+from werkzeug.http import http_date
+
+# Use the same json implementation as itsdangerous on which we
+# depend anyways.
+from itsdangerous import simplejson as _json
+
+
+# figure out if simplejson escapes slashes. This behavior was changed
+# from one version to another without reason.
+_slash_escape = '\\/' not in _json.dumps('/')
+
+
+__all__ = ['dump', 'dumps', 'load', 'loads', 'htmlsafe_dump',
+ 'htmlsafe_dumps', 'JSONDecoder', 'JSONEncoder',
+ 'jsonify']
+
+
+class JSONEncoder(_json.JSONEncoder):
+ """The default Flask JSON encoder. This one extends the default simplejson
+ encoder by also supporting ``datetime`` objects as well as ``Markup``
+ objects which are serialized as RFC 822 datetime strings (same as the HTTP
+ date format). In order to support more data types override the
+ :meth:`default` method.
+ """
+
+ def default(self, o):
+ """Implement this method in a subclass such that it returns a
+ serializable object for ``o``, or calls the base implementation (to
+ raise a ``TypeError``).
+
+ For example, to support arbitrary iterators, you could implement
+ default like this::
+
+ def default(self, o):
+ try:
+ iterable = iter(o)
+ except TypeError:
+ pass
+ else:
+ return list(iterable)
+ return JSONEncoder.default(self, o)
+ """
+ if isinstance(o, datetime):
+ return http_date(o)
+ if hasattr(o, '__html__'):
+ return unicode(o.__html__())
+ return _json.JSONEncoder.default(self, o)
+
+
+class JSONDecoder(_json.JSONDecoder):
+ """The default JSON decoder. This one does not change the behavior from
+ the default simplejson encoder. Consult the :mod:`json` documentation
+ for more information. This decoder is not only used for the load
+ functions of this module but also :attr:`~flask.Request`.
+ """
+
+
+def _dump_arg_defaults(kwargs):
+ """Inject default arguments for dump functions."""
+ if current_app:
+ kwargs.setdefault('cls', current_app.json_encoder)
+ if not current_app.config['JSON_AS_ASCII']:
+ kwargs.setdefault('ensure_ascii', False)
+
+
+def _load_arg_defaults(kwargs):
+ """Inject default arguments for load functions."""
+ if current_app:
+ kwargs.setdefault('cls', current_app.json_decoder)
+
+
+def dumps(obj, **kwargs):
+ """Serialize ``obj`` to a JSON formatted ``str`` by using the application's
+ configured encoder (:attr:`~flask.Flask.json_encoder`) if there is an
+ application on the stack.
+
+ This function can return ``unicode`` strings or ascii-only bytestrings by
+ default which coerce into unicode strings automatically. That behavior by
+ default is controlled by the ``JSON_AS_ASCII`` configuration variable
+ and can be overriden by the simplejson ``ensure_ascii`` parameter.
+ """
+ _dump_arg_defaults(kwargs)
+ return _json.dumps(obj, **kwargs)
+
+
+def dump(obj, fp, **kwargs):
+ """Like :func:`dumps` but writes into a file object."""
+ _dump_arg_defaults(kwargs)
+ return _json.dump(obj, fp, **kwargs)
+
+
+def loads(s, **kwargs):
+ """Unserialize a JSON object from a string ``s`` by using the application's
+ configured decoder (:attr:`~flask.Flask.json_decoder`) if there is an
+ application on the stack.
+ """
+ _load_arg_defaults(kwargs)
+ return _json.loads(s, **kwargs)
+
+
+def load(fp, **kwargs):
+ """Like :func:`loads` but reads from a file object.
+ """
+ _load_arg_defaults(kwargs)
+ return _json.load(fp, **kwargs)
+
+
+def htmlsafe_dumps(obj, **kwargs):
+ """Works exactly like :func:`dumps` but is safe for use in ``<script>``
+ tags. It accepts the same arguments and returns a JSON string. Note that
+ this is available in templates through the ``|tojson`` filter but it will
+ have to be wrapped in ``|safe`` unless **true** XHTML is being used.
+ """
+ rv = dumps(obj, **kwargs)
+ if _slash_escape:
+ rv = rv.replace('/', '\\/')
+ return rv.replace('<!', '<\\u0021')
+
+
+def htmlsafe_dump(obj, fp, **kwargs):
+ """Like :func:`htmlsafe_dumps` but writes into a file object."""
+ fp.write(htmlsafe_dumps(obj, **kwargs))
+
+
+def jsonify(*args, **kwargs):
+ """Creates a :class:`~flask.Response` with the JSON representation of
+ the given arguments with an `application/json` mimetype. The arguments
+ to this function are the same as to the :class:`dict` constructor.
+
+ Example usage::
+
+ @app.route('/_get_current_user')
+ def get_current_user():
+ return jsonify(username=g.user.username,
+ email=g.user.email,
+ id=g.user.id)
+
+ This will send a JSON response like this to the browser::
+
+ {
+ "username": "admin",
+ "email": "admin@localhost",
+ "id": 42
+ }
+
+ This requires Python 2.6 or an installed version of simplejson. For
+ security reasons only objects are supported toplevel. For more
+ information about this, have a look at :ref:`json-security`.
+
+ .. versionadded:: 0.2
+ """
+ return current_app.response_class(dumps(dict(*args, **kwargs),
+ indent=None if request.is_xhr else 2),
+ mimetype='application/json')
View
19 flask/session.py
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
- flask.session
- ~~~~~~~~~~~~~
-
- This module used to flask with the session global so we moved it
- over to flask.sessions
-
- :copyright: (c) 2011 by Armin Ronacher.
- :license: BSD, see LICENSE for more details.
-"""
-
-from warnings import warn
-warn(DeprecationWarning('please use flask.sessions instead'))
-
-from .sessions import SecureCookieSession, NullSession
-
-Session = SecureCookieSession
-_NullSession = NullSession
View
133 flask/sessions.py
@@ -3,15 +3,23 @@
flask.sessions
~~~~~~~~~~~~~~
- Implements cookie based sessions based on Werkzeug's secure cookie
- system.
+ Implements cookie based sessions based on itsdangerous.
- :copyright: (c) 2011 by Armin Ronacher.
+ :copyright: (c) 2012 by Armin Ronacher.
:license: BSD, see LICENSE for more details.
"""
+import hashlib
from datetime import datetime
-from werkzeug.contrib.securecookie import SecureCookie
+from werkzeug.http import http_date, parse_date
+from werkzeug.datastructures import CallbackDict
+from . import Markup, json
+
+from itsdangerous import URLSafeTimedSerializer, BadSignature
+
+
+def total_seconds(td):
+ return td.days * 60 * 60 * 24 + td.seconds
class SessionMixin(object):
@@ -41,11 +49,53 @@ def _set_permanent(self, value):
modified = True
-class SecureCookieSession(SecureCookie, SessionMixin):
- """Expands the session with support for switching between permanent
- and non-permanent sessions.
+class TaggedJSONSerializer(object):
+ """A customized JSON serializer that supports a few extra types that
+ we take for granted when serializing (tuples, markup objects, datetime).
"""
+ def dumps(self, value):
+ def _tag(value):
+ if isinstance(value, tuple):
+ return {' t': [_tag(x) for x in value]}
+ elif callable(getattr(value, '__html__', None)):
+ return {' m': unicode(value.__html__())}
+ elif isinstance(value, list):
+ return [_tag(x) for x in value]
+ elif isinstance(value, datetime):
+ return {' d': http_date(value)}
+ elif isinstance(value, dict):
+ return dict((k, _tag(v)) for k, v in value.iteritems())
+ return value
+ return json.dumps(_tag(value), separators=(',', ':'))
+
+ def loads(self, value):
+ def object_hook(obj):
+ if len(obj) != 1:
+ return obj
+ the_key, the_value = obj.iteritems().next()
+ if the_key == ' t':
+ return tuple(the_value)
+ elif the_key == ' m':
+ return Markup(the_value)
+ elif the_key == ' d':
+ return parse_date(the_value)
+ return obj
+ return json.loads(value, object_hook=object_hook)
+
+
+session_json_serializer = TaggedJSONSerializer()
+
+
+class SecureCookieSession(CallbackDict, SessionMixin):
+ """Baseclass for sessions based on signed cookies."""
+
+ def __init__(self, initial=None):
+ def on_update(self):
+ self.modified = True
+ CallbackDict.__init__(self, initial, on_update)
+ self.modified = False
+
class NullSession(SecureCookieSession):
"""Class used to generate nicer error messages if sessions are not
@@ -98,6 +148,13 @@ class Session(dict, SessionMixin):
#: this type.
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
+ #: to how to deal with the session object.
+ #:
+ #: .. versionadded:: 0.10
+ pickle_based = False
+
def make_null_session(self, app):
"""Creates a null session which acts as a replacement object if the
real session support could not be loaded due to a configuration
@@ -178,28 +235,60 @@ def save_session(self, app, session, response):
class SecureCookieSessionInterface(SessionInterface):
- """The cookie session interface that uses the Werkzeug securecookie
- as client side session backend.
+ """The default session interface that stores sessions in signed cookies
+ through the :mod:`itsdangerous` module.
"""
+ #: the salt that should be applied on top of the secret key for the
+ #: signing of cookie based sessions.
+ salt = 'cookie-session'
+ #: the hash function to use for the signature. The default is sha1
+ digest_method = staticmethod(hashlib.sha1)
+ #: the name of the itsdangerous supported key derivation. The default
+ #: is hmac.
+ key_derivation = 'hmac'
+ #: A python serializer for the payload. The default is a compact
+ #: JSON derived serializer with support for some extra Python types
+ #: such as datetime objects or tuples.
+ serializer = session_json_serializer
session_class = SecureCookieSession
+ def get_signing_serializer(self, app):
+ if not app.secret_key:
+ return None
+ signer_kwargs = dict(
+ key_derivation=self.key_derivation,
+ digest_method=self.digest_method
+ )
+ return URLSafeTimedSerializer(app.secret_key, salt=self.salt,
+ serializer=self.serializer,
+ signer_kwargs=signer_kwargs)
+
def open_session(self, app, request):
- key = app.secret_key
- if key is not None:
- return self.session_class.load_cookie(request,
- app.session_cookie_name,
- secret_key=key)
+ s = self.get_signing_serializer(app)
+ if s is None:
+ return None
+ val = request.cookies.get(app.session_cookie_name)
+ if not val:
+ return self.session_class()
+ max_age = total_seconds(app.permanent_session_lifetime)
+ try:
+ data = s.loads(val, max_age=max_age)
+ return self.session_class(data)
+ except BadSignature:
+ return self.session_class()
def save_session(self, app, session, response):
- expires = self.get_expiration_time(app, session)
domain = self.get_cookie_domain(app)
path = self.get_cookie_path(app)
+ if not session:
+ if session.modified:
+ response.delete_cookie(app.session_cookie_name,
+ domain=domain, path=path)
+ return
httponly = self.get_cookie_httponly(app)
secure = self.get_cookie_secure(app)
- if session.modified and not session:
- response.delete_cookie(app.session_cookie_name, path=path,
- domain=domain)
- else:
- session.save_cookie(response, app.session_cookie_name, path=path,
- expires=expires, httponly=httponly,
- secure=secure, domain=domain)
+ expires = self.get_expiration_time(app, session)
+ val = self.get_signing_serializer(app).dumps(dict(session))
+ response.set_cookie(app.session_cookie_name, val,
+ expires=expires, httponly=httponly,
+ domain=domain, path=path, secure=secure)
View
9 flask/templating.py
@@ -12,7 +12,7 @@
from jinja2 import BaseLoader, Environment as BaseEnvironment, \
TemplateNotFound
-from .globals import _request_ctx_stack
+from .globals import _request_ctx_stack, _app_ctx_stack
from .signals import template_rendered
from .module import blueprint_is_module
@@ -22,8 +22,9 @@ def _default_template_ctx_processor():
`session` and `g`.
"""
reqctx = _request_ctx_stack.top
+ if reqctx is None:
+ return {}
return dict(
- config=reqctx.app.config,
request=reqctx.request,
session=reqctx.session,
g=reqctx.g
@@ -119,7 +120,7 @@ def render_template(template_name_or_list, **context):
:param context: the variables that should be available in the
context of the template.
"""
- ctx = _request_ctx_stack.top
+ ctx = _app_ctx_stack.top
ctx.app.update_template_context(context)
return _render(ctx.app.jinja_env.get_or_select_template(template_name_or_list),
context, ctx.app)
@@ -134,7 +135,7 @@ def render_template_string(source, **context):
:param context: the variables that should be available in the
context of the template.
"""
- ctx = _request_ctx_stack.top
+ ctx = _app_ctx_stack.top
ctx.app.update_template_context(context)
return _render(ctx.app.jinja_env.from_string(source),
context, ctx.app)
View
6 flask/testing.py
@@ -15,6 +15,7 @@
from contextlib import contextmanager
from werkzeug.test import Client, EnvironBuilder
from flask import _request_ctx_stack
+from urlparse import urlparse
def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs):
@@ -22,9 +23,12 @@ def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs):
http_host = app.config.get('SERVER_NAME')
app_root = app.config.get('APPLICATION_ROOT')
if base_url is None:
- base_url = 'http://%s/' % (http_host or 'localhost')
+ url = urlparse(path)
+ base_url = 'http://%s/' % (url.netloc or http_host or 'localhost')
if app_root:
base_url += app_root.lstrip('/')
+ if url.netloc:
+ path = url.path
return EnvironBuilder(path, base_url, *args, **kwargs)
View
5 flask/testsuite/__init__.py
@@ -32,9 +32,12 @@ def add_to_path(path):
raise RuntimeError('Tried to add nonexisting path')
def _samefile(x, y):
+ if x == y:
+ return True
try:
return os.path.samefile(x, y)
- except (IOError, OSError):
+ except (IOError, OSError, AttributeError):
+ # Windows has no samefile
return False
sys.path[:] = [x for x in sys.path if not _samefile(path, x)]
sys.path.insert(0, path)
View
28 flask/testsuite/basic.py
@@ -13,6 +13,7 @@
import re
import flask
+import pickle
import unittest
from datetime import datetime
from threading import Thread
@@ -297,6 +298,31 @@ def dump_session_contents():
self.assert_equal(c.get('/').data, b'None')
self.assert_equal(c.get('/').data, b'42')
+ def test_session_special_types(self):
+ app = flask.Flask(__name__)
+ app.secret_key = 'development-key'
+ app.testing = True
+ now = datetime.utcnow().replace(microsecond=0)
+
+ @app.after_request
+ def modify_session(response):
+ flask.session['m'] = flask.Markup('Hello!')
+ flask.session['dt'] = now
+ flask.session['t'] = (1, 2, 3)
+ return response
+
+ @app.route('/')
+ def dump_session_contents():
+ return pickle.dumps(dict(flask.session))
+
+ c = app.test_client()
+ c.get('/')
+ rv = pickle.loads(c.get('/').data)
+ self.assert_equal(rv['m'], flask.Markup('Hello!'))
+ self.assert_equal(type(rv['m']), flask.Markup)
+ self.assert_equal(rv['dt'], now)
+ self.assert_equal(rv['t'], (1, 2, 3))
+
def test_flashes(self):
app = flask.Flask(__name__)
app.secret_key = 'testkey'
@@ -361,7 +387,7 @@ def test_filters():
return ''
@app.route('/test_filters_without_returning_categories/')
- def test_filters():
+ def test_filters2():
messages = flask.get_flashed_messages(category_filter=['message', 'warning'])
self.assert_equal(len(messages), 2)
self.assert_equal(messages[0], 'Hello World')
View
116 flask/testsuite/blueprints.py
@@ -548,7 +548,7 @@ def my_reverse(s):
return s[::-1]
app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
- self.assertTrue('my_reverse' in list(app.jinja_env.filters.keys()))
+ self.assertTrue('my_reverse' in app.jinja_env.filters.keys())
self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse)
self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba')
@@ -559,7 +559,7 @@ def my_reverse(s):
bp.add_app_template_filter(my_reverse)
app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
- self.assertTrue('my_reverse' in list(app.jinja_env.filters.keys()))
+ self.assertTrue('my_reverse' in app.jinja_env.filters.keys())
self.assert_equal(app.jinja_env.filters['my_reverse'], my_reverse)
self.assert_equal(app.jinja_env.filters['my_reverse']('abcd'), 'dcba')
@@ -570,7 +570,7 @@ def my_reverse(s):
return s[::-1]
app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
- self.assertTrue('strrev' in list(app.jinja_env.filters.keys()))
+ self.assertTrue('strrev' in app.jinja_env.filters.keys())
self.assert_equal(app.jinja_env.filters['strrev'], my_reverse)
self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba')
@@ -581,7 +581,7 @@ def my_reverse(s):
bp.add_app_template_filter(my_reverse, 'strrev')
app = flask.Flask(__name__)
app.register_blueprint(bp, url_prefix='/py')
- self.assertTrue('strrev' in list(app.jinja_env.filters.keys()))
+ self.assertTrue('strrev' in app.jinja_env.filters.keys())
self.assert_equal(app.jinja_env.filters['strrev'], my_reverse)
self.assert_equal(app.jinja_env.filters['strrev']('abcd'), 'dcba')
@@ -650,6 +650,114 @@ def index():
rv = app.test_client().get('/')
self.assert_equal(rv.data, b'dcba')
+ def test_template_test(self):
+ bp = flask.Blueprint('bp', __name__)
+ @bp.app_template_test()
+ def is_boolean(value):
+ return isinstance(value, bool)
+ app = flask.Flask(__name__)
+ app.register_blueprint(bp, url_prefix='/py')
+ self.assert_('is_boolean' in app.jinja_env.tests.keys())
+ self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean)
+ self.assert_(app.jinja_env.tests['is_boolean'](False))
+
+ def test_add_template_test(self):
+ bp = flask.Blueprint('bp', __name__)
+ def is_boolean(value):
+ return isinstance(value, bool)
+ bp.add_app_template_test(is_boolean)
+ app = flask.Flask(__name__)
+ app.register_blueprint(bp, url_prefix='/py')
+ self.assert_('is_boolean' in app.jinja_env.tests.keys())
+ self.assert_equal(app.jinja_env.tests['is_boolean'], is_boolean)
+ self.assert_(app.jinja_env.tests['is_boolean'](False))
+
+ def test_template_test_with_name(self):
+ bp = flask.Blueprint('bp', __name__)
+ @bp.app_template_test('boolean')
+ def is_boolean(value):
+ return isinstance(value, bool)
+ app = flask.Flask(__name__)
+ app.register_blueprint(bp, url_prefix='/py')
+ self.assert_('boolean' in app.jinja_env.tests.keys())
+ self.assert_equal(app.jinja_env.tests['boolean'], is_boolean)
+ self.assert_(app.jinja_env.tests['boolean'](False))
+
+ def test_add_template_test_with_name(self):
+ bp = flask.Blueprint('bp', __name__)
+ def is_boolean(value):
+ return isinstance(value, bool)
+ bp.add_app_template_test(is_boolean, 'boolean'