forked from orbit-oss/flask
Merge branch 'master' into master
This commit is contained in:
commit
d911c897ee
67 changed files with 3585 additions and 2201 deletions
|
|
@ -25,6 +25,7 @@ if not PY2:
|
|||
itervalues = lambda d: iter(d.values())
|
||||
iteritems = lambda d: iter(d.items())
|
||||
|
||||
from inspect import getfullargspec as getargspec
|
||||
from io import StringIO
|
||||
|
||||
def reraise(tp, value, tb=None):
|
||||
|
|
@ -43,6 +44,7 @@ else:
|
|||
itervalues = lambda d: d.itervalues()
|
||||
iteritems = lambda d: d.iteritems()
|
||||
|
||||
from inspect import getargspec
|
||||
from cStringIO import StringIO
|
||||
|
||||
exec('def reraise(tp, value, tb=None):\n raise tp, value, tb')
|
||||
|
|
|
|||
307
flask/app.py
307
flask/app.py
|
|
@ -10,30 +10,30 @@
|
|||
"""
|
||||
import os
|
||||
import sys
|
||||
from threading import Lock
|
||||
from datetime import timedelta
|
||||
from itertools import chain
|
||||
from functools import update_wrapper
|
||||
from itertools import chain
|
||||
from threading import Lock
|
||||
|
||||
from werkzeug.datastructures import ImmutableDict
|
||||
from werkzeug.routing import Map, Rule, RequestRedirect, BuildError
|
||||
from werkzeug.exceptions import HTTPException, InternalServerError, \
|
||||
MethodNotAllowed, BadRequest, default_exceptions
|
||||
from werkzeug.datastructures import ImmutableDict, Headers
|
||||
from werkzeug.exceptions import BadRequest, HTTPException, \
|
||||
InternalServerError, MethodNotAllowed, default_exceptions
|
||||
from werkzeug.routing import BuildError, Map, RequestRedirect, Rule
|
||||
|
||||
from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \
|
||||
locked_cached_property, _endpoint_from_view_func, find_package, \
|
||||
get_debug_flag
|
||||
from . import json, cli
|
||||
from .wrappers import Request, Response
|
||||
from .config import ConfigAttribute, Config
|
||||
from .ctx import RequestContext, AppContext, _AppCtxGlobals
|
||||
from .globals import _request_ctx_stack, request, session, g
|
||||
from . import cli, json
|
||||
from ._compat import integer_types, reraise, string_types, text_type
|
||||
from .config import Config, ConfigAttribute
|
||||
from .ctx import AppContext, RequestContext, _AppCtxGlobals
|
||||
from .globals import _request_ctx_stack, g, request, session
|
||||
from .helpers import _PackageBoundObject, \
|
||||
_endpoint_from_view_func, find_package, get_debug_flag, \
|
||||
get_flashed_messages, locked_cached_property, url_for
|
||||
from .sessions import SecureCookieSessionInterface
|
||||
from .signals import appcontext_tearing_down, got_request_exception, \
|
||||
request_finished, request_started, request_tearing_down
|
||||
from .templating import DispatchingJinjaLoader, Environment, \
|
||||
_default_template_ctx_processor
|
||||
from .signals import request_started, request_finished, got_request_exception, \
|
||||
request_tearing_down, appcontext_tearing_down
|
||||
from ._compat import reraise, string_types, text_type, integer_types
|
||||
_default_template_ctx_processor
|
||||
from .wrappers import Request, Response
|
||||
|
||||
# a lock used for logger initialization
|
||||
_logger_lock = Lock()
|
||||
|
|
@ -123,6 +123,9 @@ class Flask(_PackageBoundObject):
|
|||
.. versionadded:: 0.11
|
||||
The `root_path` parameter was added.
|
||||
|
||||
.. versionadded:: 0.13
|
||||
The `host_matching` and `static_host` parameters were added.
|
||||
|
||||
:param import_name: the name of the application package
|
||||
:param static_url_path: can be used to specify a different path for the
|
||||
static files on the web. Defaults to the name
|
||||
|
|
@ -130,6 +133,11 @@ class Flask(_PackageBoundObject):
|
|||
:param static_folder: the folder with static files that should be served
|
||||
at `static_url_path`. Defaults to the ``'static'``
|
||||
folder in the root path of the application.
|
||||
:param host_matching: sets the app's ``url_map.host_matching`` to the given
|
||||
given value. Defaults to False.
|
||||
:param static_host: the host to use when adding the static route. Defaults
|
||||
to None. Required when using ``host_matching=True``
|
||||
with a ``static_folder`` configured.
|
||||
:param template_folder: the folder that contains the templates that should
|
||||
be used by the application. Defaults to
|
||||
``'templates'`` folder in the root path of the
|
||||
|
|
@ -212,7 +220,7 @@ class Flask(_PackageBoundObject):
|
|||
|
||||
#: The testing flag. Set this to ``True`` to enable the test mode of
|
||||
#: Flask extensions (and in the future probably also Flask itself).
|
||||
#: For example this might activate unittest helpers that have an
|
||||
#: For example this might activate test helpers that have an
|
||||
#: additional runtime cost which should not be enabled by default.
|
||||
#:
|
||||
#: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the
|
||||
|
|
@ -314,7 +322,7 @@ class Flask(_PackageBoundObject):
|
|||
'PREFERRED_URL_SCHEME': 'http',
|
||||
'JSON_AS_ASCII': True,
|
||||
'JSON_SORT_KEYS': True,
|
||||
'JSONIFY_PRETTYPRINT_REGULAR': True,
|
||||
'JSONIFY_PRETTYPRINT_REGULAR': False,
|
||||
'JSONIFY_MIMETYPE': 'application/json',
|
||||
'TEMPLATES_AUTO_RELOAD': None,
|
||||
})
|
||||
|
|
@ -337,7 +345,8 @@ class Flask(_PackageBoundObject):
|
|||
session_interface = SecureCookieSessionInterface()
|
||||
|
||||
def __init__(self, import_name, static_path=None, static_url_path=None,
|
||||
static_folder='static', template_folder='templates',
|
||||
static_folder='static', static_host=None,
|
||||
host_matching=False, template_folder='templates',
|
||||
instance_path=None, instance_relative_config=False,
|
||||
root_path=None):
|
||||
_PackageBoundObject.__init__(self, import_name,
|
||||
|
|
@ -391,7 +400,7 @@ class Flask(_PackageBoundObject):
|
|||
#: is the class for the instance check and the second the error handler
|
||||
#: function.
|
||||
#:
|
||||
#: To register a error handler, use the :meth:`errorhandler`
|
||||
#: To register an error handler, use the :meth:`errorhandler`
|
||||
#: decorator.
|
||||
self.error_handler_spec = {None: self._error_handlers}
|
||||
|
||||
|
|
@ -404,17 +413,16 @@ class Flask(_PackageBoundObject):
|
|||
#: .. versionadded:: 0.9
|
||||
self.url_build_error_handlers = []
|
||||
|
||||
#: A dictionary with lists of functions that should be called at the
|
||||
#: beginning of the request. The key of the dictionary is the name of
|
||||
#: the blueprint this function is active for, ``None`` for all requests.
|
||||
#: This can for example be used to open database connections or
|
||||
#: getting hold of the currently logged in user. To register a
|
||||
#: function here, use the :meth:`before_request` decorator.
|
||||
#: A dictionary with lists of functions that will be called at the
|
||||
#: beginning of each request. The key of the dictionary is the name of
|
||||
#: the blueprint this function is active for, or ``None`` for all
|
||||
#: requests. To register a function, use the :meth:`before_request`
|
||||
#: decorator.
|
||||
self.before_request_funcs = {}
|
||||
|
||||
#: A lists of functions that should be called at the beginning of the
|
||||
#: first request to this instance. To register a function here, use
|
||||
#: the :meth:`before_first_request` decorator.
|
||||
#: A list of functions that will be called at the beginning of the
|
||||
#: first request to this instance. To register a function, use the
|
||||
#: :meth:`before_first_request` decorator.
|
||||
#:
|
||||
#: .. versionadded:: 0.8
|
||||
self.before_first_request_funcs = []
|
||||
|
|
@ -446,12 +454,11 @@ class Flask(_PackageBoundObject):
|
|||
#: .. versionadded:: 0.9
|
||||
self.teardown_appcontext_funcs = []
|
||||
|
||||
#: A dictionary with lists of functions that can be used as URL
|
||||
#: value processor functions. Whenever a URL is built these functions
|
||||
#: are called to modify the dictionary of values in place. The key
|
||||
#: ``None`` here is used for application wide
|
||||
#: callbacks, otherwise the key is the name of the blueprint.
|
||||
#: Each of these functions has the chance to modify the dictionary
|
||||
#: A dictionary with lists of functions that are called before the
|
||||
#: :attr:`before_request_funcs` functions. The key of the dictionary is
|
||||
#: the name of the blueprint this function is active for, or ``None``
|
||||
#: for all requests. To register a function, use
|
||||
#: :meth:`url_value_preprocessor`.
|
||||
#:
|
||||
#: .. versionadded:: 0.7
|
||||
self.url_value_preprocessors = {}
|
||||
|
|
@ -525,19 +532,22 @@ class Flask(_PackageBoundObject):
|
|||
#: app.url_map.converters['list'] = ListConverter
|
||||
self.url_map = Map()
|
||||
|
||||
self.url_map.host_matching = host_matching
|
||||
|
||||
# tracks internally if the application already handled at least one
|
||||
# request.
|
||||
self._got_first_request = False
|
||||
self._before_request_lock = Lock()
|
||||
|
||||
# register the static folder for the application. Do that even
|
||||
# if the folder does not exist. First of all it might be created
|
||||
# while the server is running (usually happens during development)
|
||||
# but also because google appengine stores static files somewhere
|
||||
# else when mapped with the .yml file.
|
||||
# Add a static route using the provided static_url_path, static_host,
|
||||
# and static_folder if there is a configured static_folder.
|
||||
# Note we do this without checking if static_folder exists.
|
||||
# For one, it might be created while the server is running (e.g. during
|
||||
# development). Also, Google App Engine stores static files somewhere
|
||||
if self.has_static_folder:
|
||||
assert bool(static_host) == host_matching, 'Invalid static_host/host_matching combination'
|
||||
self.add_url_rule(self.static_url_path + '/<path:filename>',
|
||||
endpoint='static',
|
||||
endpoint='static', host=static_host,
|
||||
view_func=self.send_static_file)
|
||||
|
||||
#: The click command line context for this application. Commands
|
||||
|
|
@ -813,7 +823,8 @@ class Flask(_PackageBoundObject):
|
|||
|
||||
:param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to
|
||||
have the server available externally as well. Defaults to
|
||||
``'127.0.0.1'``.
|
||||
``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config
|
||||
variable if present.
|
||||
:param port: the port of the webserver. Defaults to ``5000`` or the
|
||||
port defined in the ``SERVER_NAME`` config variable if
|
||||
present.
|
||||
|
|
@ -965,7 +976,7 @@ class Flask(_PackageBoundObject):
|
|||
return iter(self._blueprint_order)
|
||||
|
||||
@setupmethod
|
||||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
|
||||
def add_url_rule(self, rule, endpoint=None, view_func=None, provide_automatic_options=None, **options):
|
||||
"""Connects a URL rule. Works exactly like the :meth:`route`
|
||||
decorator. If a view_func is provided it will be registered with the
|
||||
endpoint.
|
||||
|
|
@ -1005,6 +1016,10 @@ class Flask(_PackageBoundObject):
|
|||
endpoint
|
||||
:param view_func: the function to call when serving a request to the
|
||||
provided endpoint
|
||||
:param provide_automatic_options: controls whether the ``OPTIONS``
|
||||
method should be added automatically. This can also be controlled
|
||||
by setting the ``view_func.provide_automatic_options = False``
|
||||
before adding the rule.
|
||||
:param options: the options to be forwarded to the underlying
|
||||
:class:`~werkzeug.routing.Rule` object. A change
|
||||
to Werkzeug is handling of method options. methods
|
||||
|
|
@ -1034,8 +1049,9 @@ class Flask(_PackageBoundObject):
|
|||
|
||||
# starting with Flask 0.8 the view_func object can disable and
|
||||
# force-enable the automatic options handling.
|
||||
provide_automatic_options = getattr(view_func,
|
||||
'provide_automatic_options', None)
|
||||
if provide_automatic_options is None:
|
||||
provide_automatic_options = getattr(view_func,
|
||||
'provide_automatic_options', None)
|
||||
|
||||
if provide_automatic_options is None:
|
||||
if 'OPTIONS' not in methods:
|
||||
|
|
@ -1294,11 +1310,13 @@ class Flask(_PackageBoundObject):
|
|||
@setupmethod
|
||||
def before_request(self, f):
|
||||
"""Registers a function to run before each request.
|
||||
|
||||
For example, this can be used to open a database connection, or to load
|
||||
the logged in user from the session.
|
||||
|
||||
The function will be called without any arguments.
|
||||
If the function returns a non-None value, it's handled as
|
||||
if it was the return value from the view and further
|
||||
request handling is stopped.
|
||||
The function will be called without any arguments. If it returns a
|
||||
non-None value, the value is handled as if it was the return value from
|
||||
the view, and further request handling is stopped.
|
||||
"""
|
||||
self.before_request_funcs.setdefault(None, []).append(f)
|
||||
return f
|
||||
|
|
@ -1354,7 +1372,7 @@ class Flask(_PackageBoundObject):
|
|||
will have to surround the execution of these code by try/except
|
||||
statements and log occurring errors.
|
||||
|
||||
When a teardown function was called because of a exception it will
|
||||
When a teardown function was called because of an exception it will
|
||||
be passed an error object.
|
||||
|
||||
The return values of teardown functions are ignored.
|
||||
|
|
@ -1417,9 +1435,17 @@ class Flask(_PackageBoundObject):
|
|||
|
||||
@setupmethod
|
||||
def url_value_preprocessor(self, f):
|
||||
"""Registers a function as URL value preprocessor for all view
|
||||
functions of the application. It's called before the view functions
|
||||
are called and can modify the url values provided.
|
||||
"""Register a URL value preprocessor function for all view
|
||||
functions in the application. These functions will be called before the
|
||||
:meth:`before_request` functions.
|
||||
|
||||
The function can modify the values captured from the matched url before
|
||||
they are passed to the view. For example, this can be used to pop a
|
||||
common language code value and place it in ``g`` rather than pass it to
|
||||
every view.
|
||||
|
||||
The function is passed the endpoint name and values dict. The return
|
||||
value is ignored.
|
||||
"""
|
||||
self.url_value_preprocessors.setdefault(None, []).append(f)
|
||||
return f
|
||||
|
|
@ -1434,15 +1460,17 @@ class Flask(_PackageBoundObject):
|
|||
return f
|
||||
|
||||
def _find_error_handler(self, e):
|
||||
"""Finds a registered error handler for the request’s blueprint.
|
||||
Otherwise falls back to the app, returns None if not a suitable
|
||||
handler is found.
|
||||
"""Find a registered error handler for a request in this order:
|
||||
blueprint handler for a specific code, app handler for a specific code,
|
||||
blueprint generic HTTPException handler, app generic HTTPException handler,
|
||||
and returns None if a suitable handler is not found.
|
||||
"""
|
||||
exc_class, code = self._get_exc_class_and_code(type(e))
|
||||
|
||||
def find_handler(handler_map):
|
||||
if not handler_map:
|
||||
return
|
||||
|
||||
for cls in exc_class.__mro__:
|
||||
handler = handler_map.get(cls)
|
||||
if handler is not None:
|
||||
|
|
@ -1450,15 +1478,13 @@ class Flask(_PackageBoundObject):
|
|||
handler_map[exc_class] = handler
|
||||
return handler
|
||||
|
||||
# try blueprint handlers
|
||||
handler = find_handler(self.error_handler_spec
|
||||
.get(request.blueprint, {})
|
||||
.get(code))
|
||||
if handler is not None:
|
||||
return handler
|
||||
# check for any in blueprint or app
|
||||
for name, c in ((request.blueprint, code), (None, code),
|
||||
(request.blueprint, None), (None, None)):
|
||||
handler = find_handler(self.error_handler_spec.get(name, {}).get(c))
|
||||
|
||||
# fall back to app handlers
|
||||
return find_handler(self.error_handler_spec[None].get(code))
|
||||
if handler:
|
||||
return handler
|
||||
|
||||
def handle_http_exception(self, e):
|
||||
"""Handles an HTTP exception. By default this will invoke the
|
||||
|
|
@ -1695,62 +1721,106 @@ class Flask(_PackageBoundObject):
|
|||
return False
|
||||
|
||||
def make_response(self, rv):
|
||||
"""Converts the return value from a view function to a real
|
||||
response object that is an instance of :attr:`response_class`.
|
||||
"""Convert the return value from a view function to an instance of
|
||||
:attr:`response_class`.
|
||||
|
||||
The following types are allowed for `rv`:
|
||||
:param rv: the return value from the view function. The view function
|
||||
must return a response. Returning ``None``, or the view ending
|
||||
without returning, is not allowed. The following types are allowed
|
||||
for ``view_rv``:
|
||||
|
||||
.. tabularcolumns:: |p{3.5cm}|p{9.5cm}|
|
||||
|
||||
======================= ===========================================
|
||||
:attr:`response_class` the object is returned unchanged
|
||||
:class:`str` a response object is created with the
|
||||
string as body
|
||||
:class:`unicode` a response object is created with the
|
||||
string encoded to utf-8 as body
|
||||
a WSGI function the function is called as WSGI application
|
||||
and buffered as response object
|
||||
:class:`tuple` A tuple in the form ``(response, status,
|
||||
headers)`` or ``(response, headers)``
|
||||
where `response` is any of the
|
||||
types defined here, `status` is a string
|
||||
or an integer and `headers` is a list or
|
||||
a dictionary with header values.
|
||||
======================= ===========================================
|
||||
|
||||
:param rv: the return value from the view function
|
||||
``str`` (``unicode`` in Python 2)
|
||||
A response object is created with the string encoded to UTF-8
|
||||
as the body.
|
||||
|
||||
``bytes`` (``str`` in Python 2)
|
||||
A response object is created with the bytes as the body.
|
||||
|
||||
``tuple``
|
||||
Either ``(body, status, headers)``, ``(body, status)``, or
|
||||
``(body, headers)``, where ``body`` is any of the other types
|
||||
allowed here, ``status`` is a string or an integer, and
|
||||
``headers`` is a dictionary or a list of ``(key, value)``
|
||||
tuples. If ``body`` is a :attr:`response_class` instance,
|
||||
``status`` overwrites the exiting value and ``headers`` are
|
||||
extended.
|
||||
|
||||
:attr:`response_class`
|
||||
The object is returned unchanged.
|
||||
|
||||
other :class:`~werkzeug.wrappers.Response` class
|
||||
The object is coerced to :attr:`response_class`.
|
||||
|
||||
:func:`callable`
|
||||
The function is called as a WSGI application. The result is
|
||||
used to create a response object.
|
||||
|
||||
.. versionchanged:: 0.9
|
||||
Previously a tuple was interpreted as the arguments for the
|
||||
response object.
|
||||
"""
|
||||
status_or_headers = headers = None
|
||||
if isinstance(rv, tuple):
|
||||
rv, status_or_headers, headers = rv + (None,) * (3 - len(rv))
|
||||
|
||||
status = headers = None
|
||||
|
||||
# unpack tuple returns
|
||||
if isinstance(rv, (tuple, list)):
|
||||
len_rv = len(rv)
|
||||
|
||||
# a 3-tuple is unpacked directly
|
||||
if len_rv == 3:
|
||||
rv, status, headers = rv
|
||||
# decide if a 2-tuple has status or headers
|
||||
elif len_rv == 2:
|
||||
if isinstance(rv[1], (Headers, dict, tuple, list)):
|
||||
rv, headers = rv
|
||||
else:
|
||||
rv, status = rv
|
||||
# other sized tuples are not allowed
|
||||
else:
|
||||
raise TypeError(
|
||||
'The view function did not return a valid response tuple.'
|
||||
' The tuple must have the form (body, status, headers),'
|
||||
' (body, status), or (body, headers).'
|
||||
)
|
||||
|
||||
# the body must not be None
|
||||
if rv is None:
|
||||
raise ValueError('View function did not return a response')
|
||||
|
||||
if isinstance(status_or_headers, (dict, list)):
|
||||
headers, status_or_headers = status_or_headers, None
|
||||
raise TypeError(
|
||||
'The view function did not return a valid response. The'
|
||||
' function either returned None or ended without a return'
|
||||
' statement.'
|
||||
)
|
||||
|
||||
# make sure the body is an instance of the response class
|
||||
if not isinstance(rv, self.response_class):
|
||||
# When we create a response object directly, we let the constructor
|
||||
# set the headers and status. We do this because there can be
|
||||
# some extra logic involved when creating these objects with
|
||||
# specific values (like default content type selection).
|
||||
if isinstance(rv, (text_type, bytes, bytearray)):
|
||||
rv = self.response_class(rv, headers=headers,
|
||||
status=status_or_headers)
|
||||
headers = status_or_headers = None
|
||||
# let the response class set the status and headers instead of
|
||||
# waiting to do it manually, so that the class can handle any
|
||||
# special logic
|
||||
rv = self.response_class(rv, status=status, headers=headers)
|
||||
status = headers = None
|
||||
else:
|
||||
rv = self.response_class.force_type(rv, request.environ)
|
||||
# evaluate a WSGI callable, or coerce a different response
|
||||
# class to the correct type
|
||||
try:
|
||||
rv = self.response_class.force_type(rv, request.environ)
|
||||
except TypeError as e:
|
||||
new_error = TypeError(
|
||||
'{e}\nThe view function did not return a valid'
|
||||
' response. The return type must be a string, tuple,'
|
||||
' Response instance, or WSGI callable, but it was a'
|
||||
' {rv.__class__.__name__}.'.format(e=e, rv=rv)
|
||||
)
|
||||
reraise(TypeError, new_error, sys.exc_info()[2])
|
||||
|
||||
if status_or_headers is not None:
|
||||
if isinstance(status_or_headers, string_types):
|
||||
rv.status = status_or_headers
|
||||
# prefer the status if it was provided
|
||||
if status is not None:
|
||||
if isinstance(status, (text_type, bytes, bytearray)):
|
||||
rv.status = status
|
||||
else:
|
||||
rv.status_code = status_or_headers
|
||||
rv.status_code = status
|
||||
|
||||
# extend existing headers with provided headers
|
||||
if headers:
|
||||
rv.headers.extend(headers)
|
||||
|
||||
|
|
@ -1813,16 +1883,16 @@ class Flask(_PackageBoundObject):
|
|||
raise error
|
||||
|
||||
def preprocess_request(self):
|
||||
"""Called before the actual request dispatching and will
|
||||
call each :meth:`before_request` decorated function, passing no
|
||||
arguments.
|
||||
If any of these functions returns a value, it's handled as
|
||||
if it was the return value from the view and further
|
||||
request handling is stopped.
|
||||
|
||||
This also triggers the :meth:`url_value_preprocessor` functions before
|
||||
the actual :meth:`before_request` functions are called.
|
||||
"""Called before the request is dispatched. Calls
|
||||
:attr:`url_value_preprocessors` registered with the app and the
|
||||
current blueprint (if any). Then calls :attr:`before_request_funcs`
|
||||
registered with the app and the blueprint.
|
||||
|
||||
If any :meth:`before_request` handler returns a non-None value, the
|
||||
value is handled as if it was the return value from the view, and
|
||||
further request handling is stopped.
|
||||
"""
|
||||
|
||||
bp = _request_ctx_stack.top.request.blueprint
|
||||
|
||||
funcs = self.url_value_preprocessors.get(None, ())
|
||||
|
|
@ -1982,14 +2052,17 @@ class Flask(_PackageBoundObject):
|
|||
exception context to start the response
|
||||
"""
|
||||
ctx = self.request_context(environ)
|
||||
ctx.push()
|
||||
error = None
|
||||
try:
|
||||
try:
|
||||
ctx.push()
|
||||
response = self.full_dispatch_request()
|
||||
except Exception as e:
|
||||
error = e
|
||||
response = self.handle_exception(e)
|
||||
except:
|
||||
error = sys.exc_info()[1]
|
||||
raise
|
||||
return response(environ, start_response)
|
||||
finally:
|
||||
if self.should_ignore_error(error):
|
||||
|
|
|
|||
|
|
@ -89,6 +89,13 @@ class Blueprint(_PackageBoundObject):
|
|||
warn_on_modifications = False
|
||||
_got_registered_once = False
|
||||
|
||||
#: Blueprint local JSON decoder class to use.
|
||||
#: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_encoder`.
|
||||
json_encoder = None
|
||||
#: Blueprint local JSON decoder class to use.
|
||||
#: Set to ``None`` to use the app's :class:`~flask.app.Flask.json_decoder`.
|
||||
json_decoder = None
|
||||
|
||||
def __init__(self, name, import_name, static_folder=None,
|
||||
static_url_path=None, template_folder=None,
|
||||
url_prefix=None, subdomain=None, url_defaults=None,
|
||||
|
|
|
|||
133
flask/cli.py
133
flask/cli.py
|
|
@ -11,41 +11,86 @@
|
|||
|
||||
import os
|
||||
import sys
|
||||
from threading import Lock, Thread
|
||||
import traceback
|
||||
from functools import update_wrapper
|
||||
from operator import attrgetter
|
||||
from threading import Lock, Thread
|
||||
|
||||
import click
|
||||
|
||||
from ._compat import iteritems, reraise
|
||||
from .helpers import get_debug_flag
|
||||
from . import __version__
|
||||
from ._compat import iteritems, reraise
|
||||
from .globals import current_app
|
||||
from .helpers import get_debug_flag
|
||||
from ._compat import getargspec
|
||||
|
||||
|
||||
class NoAppException(click.UsageError):
|
||||
"""Raised if an application cannot be found or loaded."""
|
||||
|
||||
|
||||
def find_best_app(module):
|
||||
def find_best_app(script_info, module):
|
||||
"""Given a module instance this tries to find the best possible
|
||||
application in the module or raises an exception.
|
||||
"""
|
||||
from . import Flask
|
||||
|
||||
# Search for the most common names first.
|
||||
for attr_name in 'app', 'application':
|
||||
for attr_name in ('app', 'application'):
|
||||
app = getattr(module, attr_name, None)
|
||||
if app is not None and isinstance(app, Flask):
|
||||
if isinstance(app, Flask):
|
||||
return app
|
||||
|
||||
# Otherwise find the only object that is a Flask instance.
|
||||
matches = [v for k, v in iteritems(module.__dict__)
|
||||
if isinstance(v, Flask)]
|
||||
matches = [
|
||||
v for k, v in iteritems(module.__dict__) if isinstance(v, Flask)
|
||||
]
|
||||
|
||||
if len(matches) == 1:
|
||||
return matches[0]
|
||||
raise NoAppException('Failed to find application in module "%s". Are '
|
||||
'you sure it contains a Flask application? Maybe '
|
||||
'you wrapped it in a WSGI middleware or you are '
|
||||
'using a factory function.' % module.__name__)
|
||||
elif len(matches) > 1:
|
||||
raise NoAppException(
|
||||
'Auto-detected multiple Flask applications in module "{module}".'
|
||||
' Use "FLASK_APP={module}:name" to specify the correct'
|
||||
' one.'.format(module=module.__name__)
|
||||
)
|
||||
|
||||
# Search for app factory callables.
|
||||
for attr_name in ('create_app', 'make_app'):
|
||||
app_factory = getattr(module, attr_name, None)
|
||||
|
||||
if callable(app_factory):
|
||||
try:
|
||||
app = call_factory(app_factory, script_info)
|
||||
if isinstance(app, Flask):
|
||||
return app
|
||||
except TypeError:
|
||||
raise NoAppException(
|
||||
'Auto-detected "{callable}()" in module "{module}", but '
|
||||
'could not call it without specifying arguments.'.format(
|
||||
callable=attr_name, module=module.__name__
|
||||
)
|
||||
)
|
||||
|
||||
raise NoAppException(
|
||||
'Failed to find application in module "{module}". Are you sure '
|
||||
'it contains a Flask application? Maybe you wrapped it in a WSGI '
|
||||
'middleware.'.format(module=module.__name__)
|
||||
)
|
||||
|
||||
|
||||
def call_factory(func, script_info):
|
||||
"""Checks if the given app factory function has an argument named
|
||||
``script_info`` or just a single argument and calls the function passing
|
||||
``script_info`` if so. Otherwise, calls the function without any arguments
|
||||
and returns the result.
|
||||
"""
|
||||
arguments = getargspec(func).args
|
||||
if 'script_info' in arguments:
|
||||
return func(script_info=script_info)
|
||||
elif len(arguments) == 1:
|
||||
return func(script_info)
|
||||
return func()
|
||||
|
||||
|
||||
def prepare_exec_for_file(filename):
|
||||
|
|
@ -77,7 +122,7 @@ def prepare_exec_for_file(filename):
|
|||
return '.'.join(module[::-1])
|
||||
|
||||
|
||||
def locate_app(app_id):
|
||||
def locate_app(script_info, app_id):
|
||||
"""Attempts to locate the application."""
|
||||
__traceback_hide__ = True
|
||||
if ':' in app_id:
|
||||
|
|
@ -92,7 +137,9 @@ def locate_app(app_id):
|
|||
# Reraise the ImportError if it occurred within the imported module.
|
||||
# Determine this by checking whether the trace has a depth > 1.
|
||||
if sys.exc_info()[-1].tb_next:
|
||||
raise
|
||||
stack_trace = traceback.format_exc()
|
||||
raise NoAppException('There was an error trying to import'
|
||||
' the app (%s):\n%s' % (module, stack_trace))
|
||||
else:
|
||||
raise NoAppException('The file/path provided (%s) does not appear'
|
||||
' to exist. Please verify the path is '
|
||||
|
|
@ -101,7 +148,7 @@ def locate_app(app_id):
|
|||
|
||||
mod = sys.modules[module]
|
||||
if app_obj is None:
|
||||
app = find_best_app(mod)
|
||||
app = find_best_app(script_info, mod)
|
||||
else:
|
||||
app = getattr(mod, app_obj, None)
|
||||
if app is None:
|
||||
|
|
@ -226,7 +273,7 @@ class ScriptInfo(object):
|
|||
if self._loaded_app is not None:
|
||||
return self._loaded_app
|
||||
if self.create_app is not None:
|
||||
rv = self.create_app(self)
|
||||
rv = call_factory(self.create_app, self)
|
||||
else:
|
||||
if not self.app_import_path:
|
||||
raise NoAppException(
|
||||
|
|
@ -234,7 +281,7 @@ class ScriptInfo(object):
|
|||
'the FLASK_APP environment variable.\n\nFor more '
|
||||
'information see '
|
||||
'http://flask.pocoo.org/docs/latest/quickstart/')
|
||||
rv = locate_app(self.app_import_path)
|
||||
rv = locate_app(self, self.app_import_path)
|
||||
debug = get_debug_flag()
|
||||
if debug is not None:
|
||||
rv.debug = debug
|
||||
|
|
@ -316,6 +363,7 @@ class FlaskGroup(AppGroup):
|
|||
if add_default_commands:
|
||||
self.add_command(run_command)
|
||||
self.add_command(shell_command)
|
||||
self.add_command(routes_command)
|
||||
|
||||
self._loaded_plugin_commands = False
|
||||
|
||||
|
|
@ -368,7 +416,9 @@ class FlaskGroup(AppGroup):
|
|||
# want the help page to break if the app does not exist.
|
||||
# If someone attempts to use the command we try to create
|
||||
# the app again and this will give us the error.
|
||||
pass
|
||||
# However, we will not do so silently because that would confuse
|
||||
# users.
|
||||
traceback.print_exc()
|
||||
return sorted(rv)
|
||||
|
||||
def main(self, *args, **kwargs):
|
||||
|
|
@ -479,6 +529,53 @@ def shell_command():
|
|||
code.interact(banner=banner, local=ctx)
|
||||
|
||||
|
||||
@click.command('routes', short_help='Show the routes for the app.')
|
||||
@click.option(
|
||||
'--sort', '-s',
|
||||
type=click.Choice(('endpoint', 'methods', 'rule', 'match')),
|
||||
default='endpoint',
|
||||
help=(
|
||||
'Method to sort routes by. "match" is the order that Flask will match '
|
||||
'routes when dispatching a request.'
|
||||
)
|
||||
)
|
||||
@click.option(
|
||||
'--all-methods',
|
||||
is_flag=True,
|
||||
help="Show HEAD and OPTIONS methods."
|
||||
)
|
||||
@with_appcontext
|
||||
def routes_command(sort, all_methods):
|
||||
"""Show all registered routes with endpoints and methods."""
|
||||
|
||||
rules = list(current_app.url_map.iter_rules())
|
||||
ignored_methods = set(() if all_methods else ('HEAD', 'OPTIONS'))
|
||||
|
||||
if sort in ('endpoint', 'rule'):
|
||||
rules = sorted(rules, key=attrgetter(sort))
|
||||
elif sort == 'methods':
|
||||
rules = sorted(rules, key=lambda rule: sorted(rule.methods))
|
||||
|
||||
rule_methods = [
|
||||
', '.join(sorted(rule.methods - ignored_methods)) for rule in rules
|
||||
]
|
||||
|
||||
headers = ('Endpoint', 'Methods', 'Rule')
|
||||
widths = (
|
||||
max(len(rule.endpoint) for rule in rules),
|
||||
max(len(methods) for methods in rule_methods),
|
||||
max(len(rule.rule) for rule in rules),
|
||||
)
|
||||
widths = [max(len(h), w) for h, w in zip(headers, widths)]
|
||||
row = '{{0:<{0}}} {{1:<{1}}} {{2:<{2}}}'.format(*widths)
|
||||
|
||||
click.echo(row.format(*headers).strip())
|
||||
click.echo(row.format(*('-' * width for width in widths)))
|
||||
|
||||
for rule, methods in zip(rules, rule_methods):
|
||||
click.echo(row.format(rule.endpoint, methods, rule.rule).rstrip())
|
||||
|
||||
|
||||
cli = FlaskGroup(help="""\
|
||||
This shell command acts as general utility script for Flask applications.
|
||||
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
"""
|
||||
|
||||
import os
|
||||
import socket
|
||||
import sys
|
||||
import pkgutil
|
||||
import posixpath
|
||||
|
|
@ -17,6 +18,7 @@ import mimetypes
|
|||
from time import time
|
||||
from zlib import adler32
|
||||
from threading import RLock
|
||||
import unicodedata
|
||||
from werkzeug.routing import BuildError
|
||||
from functools import update_wrapper
|
||||
|
||||
|
|
@ -330,6 +332,7 @@ def url_for(endpoint, **values):
|
|||
values['_external'] = external
|
||||
values['_anchor'] = anchor
|
||||
values['_method'] = method
|
||||
values['_scheme'] = scheme
|
||||
return appctx.app.handle_url_build_error(error, endpoint, values)
|
||||
|
||||
if anchor is not None:
|
||||
|
|
@ -477,8 +480,13 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
|
|||
.. versionchanged:: 0.12
|
||||
The `attachment_filename` is preferred over `filename` for MIME-type
|
||||
detection.
|
||||
|
||||
.. versionchanged:: 0.13
|
||||
UTF-8 filenames, as specified in `RFC 2231`_, are supported.
|
||||
|
||||
.. _RFC 2231: https://tools.ietf.org/html/rfc2231#section-4
|
||||
|
||||
:param filename_or_fp: the filename of the file to send in `latin-1`.
|
||||
:param filename_or_fp: the filename of the file to send.
|
||||
This is relative to the :attr:`~Flask.root_path`
|
||||
if a relative path is specified.
|
||||
Alternatively a file object might be provided in
|
||||
|
|
@ -534,8 +542,19 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
|
|||
if attachment_filename is None:
|
||||
raise TypeError('filename unavailable, required for '
|
||||
'sending as attachment')
|
||||
headers.add('Content-Disposition', 'attachment',
|
||||
filename=attachment_filename)
|
||||
|
||||
try:
|
||||
attachment_filename = attachment_filename.encode('latin-1')
|
||||
except UnicodeEncodeError:
|
||||
filenames = {
|
||||
'filename': unicodedata.normalize(
|
||||
'NFKD', attachment_filename).encode('latin-1', 'ignore'),
|
||||
'filename*': "UTF-8''%s" % url_quote(attachment_filename),
|
||||
}
|
||||
else:
|
||||
filenames = {'filename': attachment_filename}
|
||||
|
||||
headers.add('Content-Disposition', 'attachment', **filenames)
|
||||
|
||||
if current_app.use_x_sendfile and filename:
|
||||
if file is not None:
|
||||
|
|
@ -619,18 +638,24 @@ def safe_join(directory, *pathnames):
|
|||
:raises: :class:`~werkzeug.exceptions.NotFound` if one or more passed
|
||||
paths fall out of its boundaries.
|
||||
"""
|
||||
|
||||
parts = [directory]
|
||||
|
||||
for filename in pathnames:
|
||||
if filename != '':
|
||||
filename = posixpath.normpath(filename)
|
||||
for sep in _os_alt_seps:
|
||||
if sep in filename:
|
||||
raise NotFound()
|
||||
if os.path.isabs(filename) or \
|
||||
filename == '..' or \
|
||||
filename.startswith('../'):
|
||||
|
||||
if (
|
||||
any(sep in filename for sep in _os_alt_seps)
|
||||
or os.path.isabs(filename)
|
||||
or filename == '..'
|
||||
or filename.startswith('../')
|
||||
):
|
||||
raise NotFound()
|
||||
directory = os.path.join(directory, filename)
|
||||
return directory
|
||||
|
||||
parts.append(filename)
|
||||
|
||||
return posixpath.join(*parts)
|
||||
|
||||
|
||||
def send_from_directory(directory, filename, **options):
|
||||
|
|
@ -958,3 +983,38 @@ def total_seconds(td):
|
|||
:rtype: int
|
||||
"""
|
||||
return td.days * 60 * 60 * 24 + td.seconds
|
||||
|
||||
|
||||
def is_ip(value):
|
||||
"""Determine if the given string is an IP address.
|
||||
|
||||
:param value: value to check
|
||||
:type value: str
|
||||
|
||||
:return: True if string is an IP address
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
for family in (socket.AF_INET, socket.AF_INET6):
|
||||
try:
|
||||
socket.inet_pton(family, value)
|
||||
except socket.error:
|
||||
pass
|
||||
else:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def patch_vary_header(response, value):
|
||||
"""Add a value to the ``Vary`` header if it is not already present."""
|
||||
|
||||
header = response.headers.get('Vary', '')
|
||||
headers = [h for h in (h.strip() for h in header.split(',')) if h]
|
||||
lower_value = value.lower()
|
||||
|
||||
if not any(h.lower() == lower_value for h in headers):
|
||||
headers.append(value)
|
||||
|
||||
updated_header = ', '.join(headers)
|
||||
response.headers['Vary'] = updated_header
|
||||
|
|
|
|||
|
|
@ -91,9 +91,16 @@ class JSONDecoder(_json.JSONDecoder):
|
|||
def _dump_arg_defaults(kwargs):
|
||||
"""Inject default arguments for dump functions."""
|
||||
if current_app:
|
||||
kwargs.setdefault('cls', current_app.json_encoder)
|
||||
bp = current_app.blueprints.get(request.blueprint) if request else None
|
||||
kwargs.setdefault(
|
||||
'cls',
|
||||
bp.json_encoder if bp and bp.json_encoder
|
||||
else current_app.json_encoder
|
||||
)
|
||||
|
||||
if not current_app.config['JSON_AS_ASCII']:
|
||||
kwargs.setdefault('ensure_ascii', False)
|
||||
|
||||
kwargs.setdefault('sort_keys', current_app.config['JSON_SORT_KEYS'])
|
||||
else:
|
||||
kwargs.setdefault('sort_keys', True)
|
||||
|
|
@ -103,7 +110,12 @@ def _dump_arg_defaults(kwargs):
|
|||
def _load_arg_defaults(kwargs):
|
||||
"""Inject default arguments for load functions."""
|
||||
if current_app:
|
||||
kwargs.setdefault('cls', current_app.json_decoder)
|
||||
bp = current_app.blueprints.get(request.blueprint) if request else None
|
||||
kwargs.setdefault(
|
||||
'cls',
|
||||
bp.json_decoder if bp and bp.json_decoder
|
||||
else current_app.json_decoder
|
||||
)
|
||||
else:
|
||||
kwargs.setdefault('cls', JSONDecoder)
|
||||
|
||||
|
|
@ -236,11 +248,10 @@ def jsonify(*args, **kwargs):
|
|||
Added support for serializing top-level arrays. This introduces a
|
||||
security risk in ancient browsers. See :ref:`json-security` for details.
|
||||
|
||||
This function's response will be pretty printed if it was not requested
|
||||
with ``X-Requested-With: XMLHttpRequest`` to simplify debugging unless
|
||||
the ``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to false.
|
||||
Compressed (not pretty) formatting currently means no indents and no
|
||||
spaces after separators.
|
||||
This function's response will be pretty printed if the
|
||||
``JSONIFY_PRETTYPRINT_REGULAR`` config parameter is set to True or the
|
||||
Flask app is running in debug mode. Compressed (not pretty) formatting
|
||||
currently means no indents and no spaces after separators.
|
||||
|
||||
.. versionadded:: 0.2
|
||||
"""
|
||||
|
|
@ -248,7 +259,7 @@ def jsonify(*args, **kwargs):
|
|||
indent = None
|
||||
separators = (',', ':')
|
||||
|
||||
if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] and not request.is_xhr:
|
||||
if current_app.config['JSONIFY_PRETTYPRINT_REGULAR'] or current_app.debug:
|
||||
indent = 2
|
||||
separators = (', ', ': ')
|
||||
|
||||
|
|
|
|||
|
|
@ -8,17 +8,20 @@
|
|||
:copyright: (c) 2015 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import uuid
|
||||
import hashlib
|
||||
from base64 import b64encode, b64decode
|
||||
import uuid
|
||||
import warnings
|
||||
from base64 import b64decode, b64encode
|
||||
from datetime import datetime
|
||||
from werkzeug.http import http_date, parse_date
|
||||
|
||||
from itsdangerous import BadSignature, URLSafeTimedSerializer
|
||||
from werkzeug.datastructures import CallbackDict
|
||||
from werkzeug.http import http_date, parse_date
|
||||
|
||||
from flask.helpers import patch_vary_header
|
||||
from . import Markup, json
|
||||
from ._compat import iteritems, text_type
|
||||
from .helpers import total_seconds
|
||||
|
||||
from itsdangerous import URLSafeTimedSerializer, BadSignature
|
||||
from .helpers import is_ip, total_seconds
|
||||
|
||||
|
||||
class SessionMixin(object):
|
||||
|
|
@ -47,6 +50,13 @@ class SessionMixin(object):
|
|||
#: The default mixin implementation just hardcodes ``True`` in.
|
||||
modified = True
|
||||
|
||||
#: the accessed variable indicates whether or not the session object has
|
||||
#: been accessed in that request. This allows flask to append a `Vary:
|
||||
#: Cookie` header to the response if the session is being accessed. This
|
||||
#: allows caching proxy servers, like Varnish, to use both the URL and the
|
||||
#: session cookie as keys when caching pages, preventing multiple users
|
||||
#: from being served the same cache.
|
||||
accessed = True
|
||||
|
||||
class TaggedJSONSerializer(object):
|
||||
"""A customized JSON serializer that supports a few extra types that
|
||||
|
|
@ -175,8 +185,23 @@ class SecureCookieSession(CallbackDict, SessionMixin):
|
|||
def __init__(self, initial=None):
|
||||
def on_update(self):
|
||||
self.modified = True
|
||||
CallbackDict.__init__(self, initial, on_update)
|
||||
self.accessed = True
|
||||
|
||||
super(SecureCookieSession, self).__init__(initial, on_update)
|
||||
self.modified = False
|
||||
self.accessed = False
|
||||
|
||||
def __getitem__(self, key):
|
||||
self.accessed = True
|
||||
return super(SecureCookieSession, self).__getitem__(key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
self.accessed = True
|
||||
return super(SecureCookieSession, self).get(key, default)
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
self.accessed = True
|
||||
return super(SecureCookieSession, self).setdefault(key, default)
|
||||
|
||||
|
||||
class NullSession(SecureCookieSession):
|
||||
|
|
@ -259,30 +284,62 @@ class SessionInterface(object):
|
|||
return isinstance(obj, self.null_session_class)
|
||||
|
||||
def get_cookie_domain(self, app):
|
||||
"""Helpful helper method that returns the cookie domain that should
|
||||
be used for the session cookie if session cookies are used.
|
||||
"""Returns the domain that should be set for the session cookie.
|
||||
|
||||
Uses ``SESSION_COOKIE_DOMAIN`` if it is configured, otherwise
|
||||
falls back to detecting the domain based on ``SERVER_NAME``.
|
||||
|
||||
Once detected (or if not set at all), ``SESSION_COOKIE_DOMAIN`` is
|
||||
updated to avoid re-running the logic.
|
||||
"""
|
||||
if app.config['SESSION_COOKIE_DOMAIN'] is not None:
|
||||
return app.config['SESSION_COOKIE_DOMAIN']
|
||||
if app.config['SERVER_NAME'] is not None:
|
||||
# chop off the port which is usually not supported by browsers
|
||||
rv = '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
|
||||
|
||||
# Google chrome does not like cookies set to .localhost, so
|
||||
# we just go with no domain then. Flask documents anyways that
|
||||
# cross domain cookies need a fully qualified domain name
|
||||
if rv == '.localhost':
|
||||
rv = None
|
||||
rv = app.config['SESSION_COOKIE_DOMAIN']
|
||||
|
||||
# If we infer the cookie domain from the server name we need
|
||||
# to check if we are in a subpath. In that case we can't
|
||||
# set a cross domain cookie.
|
||||
if rv is not None:
|
||||
path = self.get_cookie_path(app)
|
||||
if path != '/':
|
||||
rv = rv.lstrip('.')
|
||||
# set explicitly, or cached from SERVER_NAME detection
|
||||
# if False, return None
|
||||
if rv is not None:
|
||||
return rv if rv else None
|
||||
|
||||
return rv
|
||||
rv = app.config['SERVER_NAME']
|
||||
|
||||
# server name not set, cache False to return none next time
|
||||
if not rv:
|
||||
app.config['SESSION_COOKIE_DOMAIN'] = False
|
||||
return None
|
||||
|
||||
# chop off the port which is usually not supported by browsers
|
||||
# remove any leading '.' since we'll add that later
|
||||
rv = rv.rsplit(':', 1)[0].lstrip('.')
|
||||
|
||||
if '.' not in rv:
|
||||
# Chrome doesn't allow names without a '.'
|
||||
# this should only come up with localhost
|
||||
# hack around this by not setting the name, and show a warning
|
||||
warnings.warn(
|
||||
'"{rv}" is not a valid cookie domain, it must contain a ".".'
|
||||
' Add an entry to your hosts file, for example'
|
||||
' "{rv}.localdomain", and use that instead.'.format(rv=rv)
|
||||
)
|
||||
app.config['SESSION_COOKIE_DOMAIN'] = False
|
||||
return None
|
||||
|
||||
ip = is_ip(rv)
|
||||
|
||||
if ip:
|
||||
warnings.warn(
|
||||
'The session cookie domain is an IP address. This may not work'
|
||||
' as intended in some browsers. Add an entry to your hosts'
|
||||
' file, for example "localhost.localdomain", and use that'
|
||||
' instead.'
|
||||
)
|
||||
|
||||
# if this is not an ip and app is mounted at the root, allow subdomain
|
||||
# matching by adding a '.' prefix
|
||||
if self.get_cookie_path(app) == '/' and not ip:
|
||||
rv = '.' + rv
|
||||
|
||||
app.config['SESSION_COOKIE_DOMAIN'] = rv
|
||||
return rv
|
||||
|
||||
def get_cookie_path(self, app):
|
||||
"""Returns the path for which the cookie should be valid. The
|
||||
|
|
@ -316,22 +373,20 @@ class SessionInterface(object):
|
|||
return datetime.utcnow() + app.permanent_session_lifetime
|
||||
|
||||
def should_set_cookie(self, app, session):
|
||||
"""Indicates whether a cookie should be set now or not. This is
|
||||
used by session backends to figure out if they should emit a
|
||||
set-cookie header or not. The default behavior is controlled by
|
||||
the ``SESSION_REFRESH_EACH_REQUEST`` config variable. If
|
||||
it's set to ``False`` then a cookie is only set if the session is
|
||||
modified, if set to ``True`` it's always set if the session is
|
||||
permanent.
|
||||
|
||||
This check is usually skipped if sessions get deleted.
|
||||
"""Used by session backends to determine if a ``Set-Cookie`` header
|
||||
should be set for this session cookie for this response. If the session
|
||||
has been modified, the cookie is set. If the session is permanent and
|
||||
the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is
|
||||
always set.
|
||||
|
||||
This check is usually skipped if the session was deleted.
|
||||
|
||||
.. versionadded:: 0.11
|
||||
"""
|
||||
if session.modified:
|
||||
return True
|
||||
save_each = app.config['SESSION_REFRESH_EACH_REQUEST']
|
||||
return save_each and session.permanent
|
||||
|
||||
return session.modified or (
|
||||
session.permanent and app.config['SESSION_REFRESH_EACH_REQUEST']
|
||||
)
|
||||
|
||||
def open_session(self, app, request):
|
||||
"""This method has to be implemented and must either return ``None``
|
||||
|
|
@ -397,22 +452,22 @@ class SecureCookieSessionInterface(SessionInterface):
|
|||
domain = self.get_cookie_domain(app)
|
||||
path = self.get_cookie_path(app)
|
||||
|
||||
# Delete case. If there is no session we bail early.
|
||||
# If the session was modified to be empty we remove the
|
||||
# whole cookie.
|
||||
# If the session is modified to be empty, remove the cookie.
|
||||
# If the session is empty, return without setting the cookie.
|
||||
if not session:
|
||||
if session.modified:
|
||||
response.delete_cookie(app.session_cookie_name,
|
||||
domain=domain, path=path)
|
||||
response.delete_cookie(
|
||||
app.session_cookie_name,
|
||||
domain=domain,
|
||||
path=path
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
# Modification case. There are upsides and downsides to
|
||||
# emitting a set-cookie header each request. The behavior
|
||||
# is controlled by the :meth:`should_set_cookie` method
|
||||
# which performs a quick check to figure out if the cookie
|
||||
# should be set or not. This is controlled by the
|
||||
# SESSION_REFRESH_EACH_REQUEST config flag as well as
|
||||
# the permanent flag on the session itself.
|
||||
# Add a "Vary: Cookie" header if the session was accessed at all.
|
||||
if session.accessed:
|
||||
patch_vary_header(response, 'Cookie')
|
||||
|
||||
if not self.should_set_cookie(app, session):
|
||||
return
|
||||
|
||||
|
|
@ -420,6 +475,12 @@ class SecureCookieSessionInterface(SessionInterface):
|
|||
secure = self.get_cookie_secure(app)
|
||||
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)
|
||||
response.set_cookie(
|
||||
app.session_cookie_name,
|
||||
val,
|
||||
expires=expires,
|
||||
httponly=httponly,
|
||||
domain=domain,
|
||||
path=path,
|
||||
secure=secure
|
||||
)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ class View(object):
|
|||
#: A list of methods this view can handle.
|
||||
methods = None
|
||||
|
||||
#: Setting this disables or force-enables the automatic options handling.
|
||||
provide_automatic_options = None
|
||||
|
||||
#: The canonical way to decorate class-based views is to decorate the
|
||||
#: return value of as_view(). However since this moves parts of the
|
||||
#: logic from the class declaration to the place where it's hooked
|
||||
|
|
@ -99,37 +102,39 @@ class View(object):
|
|||
view.__doc__ = cls.__doc__
|
||||
view.__module__ = cls.__module__
|
||||
view.methods = cls.methods
|
||||
view.provide_automatic_options = cls.provide_automatic_options
|
||||
return view
|
||||
|
||||
|
||||
class MethodViewType(type):
|
||||
"""Metaclass for :class:`MethodView` that determines what methods the view
|
||||
defines.
|
||||
"""
|
||||
|
||||
def __init__(cls, name, bases, d):
|
||||
super(MethodViewType, cls).__init__(name, bases, d)
|
||||
|
||||
def __new__(cls, name, bases, d):
|
||||
rv = type.__new__(cls, name, bases, d)
|
||||
if 'methods' not in d:
|
||||
methods = set(rv.methods or [])
|
||||
for key in d:
|
||||
if key in http_method_funcs:
|
||||
methods = set()
|
||||
|
||||
for key in http_method_funcs:
|
||||
if hasattr(cls, key):
|
||||
methods.add(key.upper())
|
||||
# If we have no method at all in there we don't want to
|
||||
# add a method list. (This is for instance the case for
|
||||
# the base class or another subclass of a base method view
|
||||
# that does not introduce new methods).
|
||||
|
||||
# If we have no method at all in there we don't want to add a
|
||||
# method list. This is for instance the case for the base class
|
||||
# or another subclass of a base method view that does not introduce
|
||||
# new methods.
|
||||
if methods:
|
||||
rv.methods = sorted(methods)
|
||||
return rv
|
||||
cls.methods = methods
|
||||
|
||||
|
||||
class MethodView(with_metaclass(MethodViewType, View)):
|
||||
"""Like a regular class-based view but that dispatches requests to
|
||||
particular methods. For instance if you implement a method called
|
||||
:meth:`get` it means it will respond to ``'GET'`` requests and
|
||||
the :meth:`dispatch_request` implementation will automatically
|
||||
forward your request to that. Also :attr:`options` is set for you
|
||||
automatically::
|
||||
"""A class-based view that dispatches request methods to the corresponding
|
||||
class methods. For example, if you implement a ``get`` method, it will be
|
||||
used to handle ``GET`` requests. ::
|
||||
|
||||
class CounterAPI(MethodView):
|
||||
|
||||
def get(self):
|
||||
return session.get('counter', 0)
|
||||
|
||||
|
|
@ -139,11 +144,14 @@ class MethodView(with_metaclass(MethodViewType, View)):
|
|||
|
||||
app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
|
||||
"""
|
||||
|
||||
def dispatch_request(self, *args, **kwargs):
|
||||
meth = getattr(self, request.method.lower(), None)
|
||||
|
||||
# If the request method is HEAD and we don't have a handler for it
|
||||
# retry with GET.
|
||||
if meth is None and request.method == 'HEAD':
|
||||
meth = getattr(self, 'get', None)
|
||||
|
||||
assert meth is not None, 'Unimplemented method %r' % request.method
|
||||
return meth(*args, **kwargs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue