Flask update
@@ -3,9 +3,9 @@ from couchpotato.core.event import fireEvent
|
||||
from couchpotato.core.logger import CPLog
|
||||
from couchpotato.environment import Env
|
||||
from flask.app import Flask
|
||||
from flask.blueprints import Blueprint
|
||||
from flask.globals import request
|
||||
from flask.helpers import url_for
|
||||
from flask.module import Module
|
||||
from flask.templating import render_template
|
||||
from sqlalchemy.engine import create_engine
|
||||
from sqlalchemy.orm import scoped_session
|
||||
@@ -16,7 +16,7 @@ import os
|
||||
log = CPLog(__name__)
|
||||
|
||||
app = Flask(__name__)
|
||||
web = Module(__name__, 'web')
|
||||
web = Blueprint('web', __name__)
|
||||
|
||||
|
||||
def get_session(engine = None):
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from couchpotato.core.helpers.request import jsonified
|
||||
from flask import Module
|
||||
from flask.blueprints import Blueprint
|
||||
|
||||
api = Module(__name__)
|
||||
api = Blueprint('api', __name__)
|
||||
|
||||
def addApiView(route, func, static = False):
|
||||
api.add_url_rule(route + ('' if static else '/'), endpoint = route if route else 'index', view_func = func)
|
||||
|
||||
@@ -132,14 +132,15 @@ def cmd_couchpotato(base_path, args):
|
||||
app.port = Env.setting('port', default = 5000)
|
||||
app.debug = debug
|
||||
app.secret_key = api_key
|
||||
app.static_path = url_base + '/static'
|
||||
app.add_url_rule(app.static_path + '/<path:filename>',
|
||||
|
||||
# Static path
|
||||
web.add_url_rule(url_base + '/static/<path:filename>',
|
||||
endpoint = 'static',
|
||||
view_func = app.send_static_file)
|
||||
|
||||
# Register modules
|
||||
app.register_module(web, url_prefix = '%s/' % url_base)
|
||||
app.register_module(api, url_prefix = '%s/%s/' % (url_base, api_key))
|
||||
app.register_blueprint(web, url_prefix = '%s/' % url_base)
|
||||
app.register_blueprint(api, url_prefix = '%s/%s/' % (url_base, api_key))
|
||||
|
||||
# Go go go!
|
||||
app.run(use_reloader = reloader)
|
||||
|
||||
@@ -10,25 +10,32 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
__version__ = '0.8-dev'
|
||||
|
||||
# utilities we import from Werkzeug and Jinja2 that are unused
|
||||
# in the module but are exported as public interface.
|
||||
from werkzeug import abort, redirect
|
||||
from werkzeug.exceptions import abort
|
||||
from werkzeug.utils import redirect
|
||||
from jinja2 import Markup, escape
|
||||
|
||||
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
|
||||
get_template_attribute, make_response, safe_join
|
||||
from .globals import current_app, g, request, session, _request_ctx_stack
|
||||
from .ctx import has_request_context
|
||||
from .module import Module
|
||||
from .blueprints import Blueprint
|
||||
from .templating import render_template, render_template_string
|
||||
from .session import Session
|
||||
|
||||
# the signals
|
||||
from .signals import signals_available, template_rendered, request_started, \
|
||||
request_finished, got_request_exception
|
||||
request_finished, got_request_exception, request_tearing_down
|
||||
|
||||
# only import json if it's available
|
||||
if json_available:
|
||||
from .helpers import json
|
||||
|
||||
# backwards compat, goes away in 1.0
|
||||
from .sessions import SecureCookieSession as Session
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.blueprints
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
Blueprints are the recommended way to implement larger or more
|
||||
pluggable applications in Flask 0.7 and later.
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from functools import update_wrapper
|
||||
|
||||
from .helpers import _PackageBoundObject, _endpoint_from_view_func
|
||||
|
||||
|
||||
class BlueprintSetupState(object):
|
||||
"""Temporary holder object for registering a blueprint with the
|
||||
application. An instance of this class is created by the
|
||||
:meth:`~flask.Blueprint.make_setup_state` method and later passed
|
||||
to all register callback functions.
|
||||
"""
|
||||
|
||||
def __init__(self, blueprint, app, options, first_registration):
|
||||
#: a reference to the current application
|
||||
self.app = app
|
||||
|
||||
#: a reference to the blurprint that created this setup state.
|
||||
self.blueprint = blueprint
|
||||
|
||||
#: a dictionary with all options that were passed to the
|
||||
#: :meth:`~flask.Flask.register_blueprint` method.
|
||||
self.options = options
|
||||
|
||||
#: as blueprints can be registered multiple times with the
|
||||
#: application and not everything wants to be registered
|
||||
#: multiple times on it, this attribute can be used to figure
|
||||
#: out if the blueprint was registered in the past already.
|
||||
self.first_registration = first_registration
|
||||
|
||||
subdomain = self.options.get('subdomain')
|
||||
if subdomain is None:
|
||||
subdomain = self.blueprint.subdomain
|
||||
|
||||
#: The subdomain that the blueprint should be active for, `None`
|
||||
#: otherwise.
|
||||
self.subdomain = subdomain
|
||||
|
||||
url_prefix = self.options.get('url_prefix')
|
||||
if url_prefix is None:
|
||||
url_prefix = self.blueprint.url_prefix
|
||||
|
||||
#: The prefix that should be used for all URLs defined on the
|
||||
#: blueprint.
|
||||
self.url_prefix = url_prefix
|
||||
|
||||
#: A dictionary with URL defaults that is added to each and every
|
||||
#: URL that was defined with the blueprint.
|
||||
self.url_defaults = dict(self.blueprint.url_values_defaults)
|
||||
self.url_defaults.update(self.options.get('url_defaults', ()))
|
||||
|
||||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
|
||||
"""A helper method to register a rule (and optionally a view function)
|
||||
to the application. The endpoint is automatically prefixed with the
|
||||
blueprint's name.
|
||||
"""
|
||||
if self.url_prefix:
|
||||
rule = self.url_prefix + rule
|
||||
options.setdefault('subdomain', self.subdomain)
|
||||
if endpoint is None:
|
||||
endpoint = _endpoint_from_view_func(view_func)
|
||||
defaults = self.url_defaults
|
||||
if 'defaults' in options:
|
||||
defaults = dict(defaults, **options.pop('defaults'))
|
||||
self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint),
|
||||
view_func, defaults=defaults, **options)
|
||||
|
||||
|
||||
class Blueprint(_PackageBoundObject):
|
||||
"""Represents a blueprint. A blueprint is an object that records
|
||||
functions that will be called with the
|
||||
:class:`~flask.blueprint.BlueprintSetupState` later to register functions
|
||||
or other things on the main application. See :ref:`blueprints` for more
|
||||
information.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
|
||||
warn_on_modifications = False
|
||||
_got_registered_once = False
|
||||
|
||||
def __init__(self, name, import_name, static_folder=None,
|
||||
static_url_path=None, template_folder=None,
|
||||
url_prefix=None, subdomain=None, url_defaults=None):
|
||||
_PackageBoundObject.__init__(self, import_name, template_folder)
|
||||
self.name = name
|
||||
self.url_prefix = url_prefix
|
||||
self.subdomain = subdomain
|
||||
self.static_folder = static_folder
|
||||
self.static_url_path = static_url_path
|
||||
self.deferred_functions = []
|
||||
self.view_functions = {}
|
||||
if url_defaults is None:
|
||||
url_defaults = {}
|
||||
self.url_values_defaults = url_defaults
|
||||
|
||||
def record(self, func):
|
||||
"""Registers a function that is called when the blueprint is
|
||||
registered on the application. This function is called with the
|
||||
state as argument as returned by the :meth:`make_setup_state`
|
||||
method.
|
||||
"""
|
||||
if self._got_registered_once and self.warn_on_modifications:
|
||||
from warnings import warn
|
||||
warn(Warning('The blueprint was already registered once '
|
||||
'but is getting modified now. These changes '
|
||||
'will not show up.'))
|
||||
self.deferred_functions.append(func)
|
||||
|
||||
def record_once(self, func):
|
||||
"""Works like :meth:`record` but wraps the function in another
|
||||
function that will ensure the function is only called once. If the
|
||||
blueprint is registered a second time on the application, the
|
||||
function passed is not called.
|
||||
"""
|
||||
def wrapper(state):
|
||||
if state.first_registration:
|
||||
func(state)
|
||||
return self.record(update_wrapper(wrapper, func))
|
||||
|
||||
def make_setup_state(self, app, options, first_registration=False):
|
||||
"""Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState`
|
||||
object that is later passed to the register callback functions.
|
||||
Subclasses can override this to return a subclass of the setup state.
|
||||
"""
|
||||
return BlueprintSetupState(self, app, options, first_registration)
|
||||
|
||||
def register(self, app, options, first_registration=False):
|
||||
"""Called by :meth:`Flask.register_blueprint` to register a blueprint
|
||||
on the application. This can be overridden to customize the register
|
||||
behavior. Keyword arguments from
|
||||
:func:`~flask.Flask.register_blueprint` are directly forwarded to this
|
||||
method in the `options` dictionary.
|
||||
"""
|
||||
self._got_registered_once = True
|
||||
state = self.make_setup_state(app, options, first_registration)
|
||||
if self.has_static_folder:
|
||||
state.add_url_rule(self.static_url_path + '/<path:filename>',
|
||||
view_func=self.send_static_file,
|
||||
endpoint='static')
|
||||
|
||||
for deferred in self.deferred_functions:
|
||||
deferred(state)
|
||||
|
||||
def route(self, rule, **options):
|
||||
"""Like :meth:`Flask.route` but for a blueprint. The endpoint for the
|
||||
:func:`url_for` function is prefixed with the name of the blueprint.
|
||||
"""
|
||||
def decorator(f):
|
||||
self.add_url_rule(rule, f.__name__, f, **options)
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
|
||||
"""Like :meth:`Flask.add_url_rule` but for a blueprint. The endpoint for
|
||||
the :func:`url_for` function is prefixed with the name of the blueprint.
|
||||
"""
|
||||
self.record(lambda s:
|
||||
s.add_url_rule(rule, endpoint, view_func, **options))
|
||||
|
||||
def endpoint(self, endpoint):
|
||||
"""Like :meth:`Flask.endpoint` but for a blueprint. This does not
|
||||
prefix the endpoint with the blueprint name, this has to be done
|
||||
explicitly by the user of this method. If the endpoint is prefixed
|
||||
with a `.` it will be registered to the current blueprint, otherwise
|
||||
it's an application independent endpoint.
|
||||
"""
|
||||
def decorator(f):
|
||||
def register_endpoint(state):
|
||||
state.app.view_functions[endpoint] = f
|
||||
self.record_once(register_endpoint)
|
||||
return f
|
||||
return decorator
|
||||
|
||||
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
|
||||
that blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.before_request_funcs
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def before_app_request(self, f):
|
||||
"""Like :meth:`Flask.before_request`. Such a function is executed
|
||||
before each request, even if outside of a blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.before_request_funcs
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def before_app_first_request(self, f):
|
||||
"""Like :meth:`Flask.before_first_request`. Such a function is
|
||||
executed before the first request to the application.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.before_first_request_funcs.append(f))
|
||||
return f
|
||||
|
||||
def after_request(self, f):
|
||||
"""Like :meth:`Flask.after_request` but for a blueprint. This function
|
||||
is only executed after each request that is handled by a function of
|
||||
that blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.after_request_funcs
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def after_app_request(self, f):
|
||||
"""Like :meth:`Flask.after_request` but for a blueprint. Such a function
|
||||
is executed after each request, even if outside of the blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.after_request_funcs
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def teardown_request(self, f):
|
||||
"""Like :meth:`Flask.teardown_request` but for a blueprint. This
|
||||
function is only executed when tearing down requests handled by a
|
||||
function of that blueprint. Teardown request functions are executed
|
||||
when the request context is popped, even when no actual request was
|
||||
performed.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.teardown_request_funcs
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def teardown_app_request(self, f):
|
||||
"""Like :meth:`Flask.teardown_request` but for a blueprint. Such a
|
||||
function is executed when tearing down each request, even if outside of
|
||||
the blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.teardown_request_funcs
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def context_processor(self, f):
|
||||
"""Like :meth:`Flask.context_processor` but for a blueprint. This
|
||||
function is only executed for requests handled by a blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.template_context_processors
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def app_context_processor(self, f):
|
||||
"""Like :meth:`Flask.context_processor` but for a blueprint. Such a
|
||||
function is executed each request, even if outside of the blueprint.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.template_context_processors
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def app_errorhandler(self, code):
|
||||
"""Like :meth:`Flask.errorhandler` but for a blueprint. This
|
||||
handler is used for all requests, even if outside of the blueprint.
|
||||
"""
|
||||
def decorator(f):
|
||||
self.record_once(lambda s: s.app.errorhandler(code)(f))
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def url_value_preprocessor(self, f):
|
||||
"""Registers a function as URL value preprocessor for this
|
||||
blueprint. It's called before the view functions are called and
|
||||
can modify the url values provided.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.url_value_preprocessors
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def url_defaults(self, f):
|
||||
"""Callback function for URL defaults for this blueprint. It's called
|
||||
with the endpoint and values and should update the values passed
|
||||
in place.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.url_default_functions
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def app_url_value_preprocessor(self, f):
|
||||
"""Same as :meth:`url_value_preprocessor` but application wide.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.url_value_preprocessor
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def app_url_defaults(self, f):
|
||||
"""Same as :meth:`url_defaults` but application wide.
|
||||
"""
|
||||
self.record_once(lambda s: s.app.url_default_functions
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def errorhandler(self, code_or_exception):
|
||||
"""Registers an error handler that becomes active for this blueprint
|
||||
only. Please be aware that routing does not happen local to a
|
||||
blueprint so an error handler for 404 usually is not handled by
|
||||
a blueprint unless it is caused inside a view function. Another
|
||||
special case is the 500 internal server error which is always looked
|
||||
up from the application.
|
||||
|
||||
Otherwise works as the :meth:`~flask.Flask.errorhandler` decorator
|
||||
of the :class:`~flask.Flask` object.
|
||||
"""
|
||||
def decorator(f):
|
||||
self.record_once(lambda s: s.app._register_error_handler(
|
||||
self.name, code_or_exception, f))
|
||||
return f
|
||||
return decorator
|
||||
@@ -13,9 +13,9 @@ from __future__ import with_statement
|
||||
|
||||
import imp
|
||||
import os
|
||||
import sys
|
||||
import errno
|
||||
|
||||
from werkzeug import import_string
|
||||
from werkzeug.utils import import_string
|
||||
|
||||
|
||||
class ConfigAttribute(object):
|
||||
@@ -83,13 +83,13 @@ class Config(dict):
|
||||
|
||||
def from_envvar(self, variable_name, silent=False):
|
||||
"""Loads a configuration from an environment variable pointing to
|
||||
a configuration file. This basically is just a shortcut with nicer
|
||||
a configuration file. This is basically just a shortcut with nicer
|
||||
error messages for this line of code::
|
||||
|
||||
app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS'])
|
||||
|
||||
:param variable_name: name of the environment variable
|
||||
:param silent: set to `True` if you want silent failing for missing
|
||||
:param silent: set to `True` if you want silent failure for missing
|
||||
files.
|
||||
:return: bool. `True` if able to load config, `False` otherwise.
|
||||
"""
|
||||
@@ -105,7 +105,7 @@ class Config(dict):
|
||||
self.from_pyfile(rv)
|
||||
return True
|
||||
|
||||
def from_pyfile(self, filename):
|
||||
def from_pyfile(self, filename, silent=False):
|
||||
"""Updates the values in the config from a Python file. This function
|
||||
behaves as if the file was imported as module with the
|
||||
:meth:`from_object` function.
|
||||
@@ -113,6 +113,11 @@ class Config(dict):
|
||||
:param filename: the filename of the config. This can either be an
|
||||
absolute filename or a filename relative to the
|
||||
root path.
|
||||
:param silent: set to `True` if you want silent failure for missing
|
||||
files.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
`silent` parameter.
|
||||
"""
|
||||
filename = os.path.join(self.root_path, filename)
|
||||
d = imp.new_module('config')
|
||||
@@ -120,9 +125,12 @@ class Config(dict):
|
||||
try:
|
||||
execfile(filename, d.__dict__)
|
||||
except IOError, e:
|
||||
if silent and e.errno in (errno.ENOENT, errno.EISDIR):
|
||||
return False
|
||||
e.strerror = 'Unable to load configuration file (%s)' % e.strerror
|
||||
raise
|
||||
self.from_object(d)
|
||||
return True
|
||||
|
||||
def from_object(self, obj):
|
||||
"""Updates the values from the given object. An object can be of one
|
||||
@@ -133,8 +141,8 @@ class Config(dict):
|
||||
|
||||
Objects are usually either modules or classes.
|
||||
|
||||
Just the uppercase variables in that object are stored in the config
|
||||
after lowercasing. Example usage::
|
||||
Just the uppercase variables in that object are stored in the config.
|
||||
Example usage::
|
||||
|
||||
app.config.from_object('yourapplication.default_config')
|
||||
from yourapplication import default_config
|
||||
|
||||
@@ -12,30 +12,99 @@
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from .globals import _request_ctx_stack
|
||||
from .session import _NullSession
|
||||
from .module import blueprint_is_module
|
||||
|
||||
|
||||
class _RequestGlobals(object):
|
||||
pass
|
||||
|
||||
|
||||
class _RequestContext(object):
|
||||
def has_request_context():
|
||||
"""If you have code that wants to test if a request context is there or
|
||||
not this function can be used. For instance if you want to take advantage
|
||||
of request information is it's available but fail silently if the request
|
||||
object is unavailable.
|
||||
|
||||
::
|
||||
|
||||
class User(db.Model):
|
||||
|
||||
def __init__(self, username, remote_addr=None):
|
||||
self.username = username
|
||||
if remote_addr is None and has_request_context():
|
||||
remote_addr = request.remote_addr
|
||||
self.remote_addr = remote_addr
|
||||
|
||||
Alternatively you can also just test any of the context bound objects
|
||||
(such as :class:`request` or :class:`g` for truthness)::
|
||||
|
||||
class User(db.Model):
|
||||
|
||||
def __init__(self, username, remote_addr=None):
|
||||
self.username = username
|
||||
if remote_addr is None and request:
|
||||
remote_addr = request.remote_addr
|
||||
self.remote_addr = remote_addr
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
return _request_ctx_stack.top is not None
|
||||
|
||||
|
||||
class RequestContext(object):
|
||||
"""The request context contains all request relevant information. It is
|
||||
created at the beginning of the request and pushed to the
|
||||
`_request_ctx_stack` and removed at the end of it. It will create the
|
||||
URL adapter and request object for the WSGI environment provided.
|
||||
|
||||
Do not attempt to use this class directly, instead use
|
||||
:meth:`~flask.Flask.test_request_context` and
|
||||
:meth:`~flask.Flask.request_context` to create this object.
|
||||
|
||||
When the request context is popped, it will evaluate all the
|
||||
functions registered on the application for teardown execution
|
||||
(:meth:`~flask.Flask.teardown_request`).
|
||||
|
||||
The request context is automatically popped at the end of the request
|
||||
for you. In debug mode the request context is kept around if
|
||||
exceptions happen so that interactive debuggers have a chance to
|
||||
introspect the data. With 0.4 this can also be forced for requests
|
||||
that did not fail and outside of `DEBUG` mode. By setting
|
||||
``'flask._preserve_context'`` to `True` on the WSGI environment the
|
||||
context will not pop itself at the end of the request. This is used by
|
||||
the :meth:`~flask.Flask.test_client` for example to implement the
|
||||
deferred cleanup functionality.
|
||||
|
||||
You might find this helpful for unittests where you need the
|
||||
information from the context local around for a little longer. Make
|
||||
sure to properly :meth:`~werkzeug.LocalStack.pop` the stack yourself in
|
||||
that situation, otherwise your unittests will leak memory.
|
||||
"""
|
||||
|
||||
def __init__(self, app, environ):
|
||||
self.app = app
|
||||
self.request = app.request_class(environ)
|
||||
self.url_adapter = app.create_url_adapter(self.request)
|
||||
self.session = app.open_session(self.request)
|
||||
if self.session is None:
|
||||
self.session = _NullSession()
|
||||
self.g = _RequestGlobals()
|
||||
self.flashes = None
|
||||
self.session = None
|
||||
|
||||
self.match_request()
|
||||
|
||||
# Support for deprecated functionality. This is doing away with
|
||||
# Flask 1.0
|
||||
blueprint = self.request.blueprint
|
||||
if blueprint is not None:
|
||||
# better safe than sorry, we don't want to break code that
|
||||
# already worked
|
||||
bp = app.blueprints.get(blueprint)
|
||||
if bp is not None and blueprint_is_module(bp):
|
||||
self.request._is_old_module = True
|
||||
|
||||
def match_request(self):
|
||||
"""Can be overridden by a subclass to hook into the matching
|
||||
of the request.
|
||||
"""
|
||||
try:
|
||||
url_rule, self.request.view_args = \
|
||||
self.url_adapter.match(return_rule=True)
|
||||
@@ -44,11 +113,22 @@ class _RequestContext(object):
|
||||
self.request.routing_exception = e
|
||||
|
||||
def push(self):
|
||||
"""Binds the request context."""
|
||||
"""Binds the request context to the current context."""
|
||||
_request_ctx_stack.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).
|
||||
self.session = self.app.open_session(self.request)
|
||||
if self.session is None:
|
||||
self.session = self.app.make_null_session()
|
||||
|
||||
def pop(self):
|
||||
"""Pops the request context."""
|
||||
"""Pops the request context and unbinds it by doing that. This will
|
||||
also trigger the execution of functions registered by the
|
||||
:meth:`~flask.Flask.teardown_request` decorator.
|
||||
"""
|
||||
self.app.do_teardown_request()
|
||||
_request_ctx_stack.pop()
|
||||
|
||||
def __enter__(self):
|
||||
@@ -62,5 +142,5 @@ class _RequestContext(object):
|
||||
# the context can be force kept alive for the test client.
|
||||
# See flask.testing for how this works.
|
||||
if not self.request.environ.get('flask._preserve_context') and \
|
||||
(tb is None or not self.app.debug):
|
||||
(tb is None or not self.app.preserve_context_on_exception):
|
||||
self.pop()
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.debughelpers
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Various helpers to make the development experience better.
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
|
||||
class DebugFilesKeyError(KeyError, AssertionError):
|
||||
"""Raised from request.files during debugging. The idea is that it can
|
||||
provide a better error message than just a generic KeyError/BadRequest.
|
||||
"""
|
||||
|
||||
def __init__(self, request, key):
|
||||
form_matches = request.form.getlist(key)
|
||||
buf = ['You tried to access the file "%s" in the request.files '
|
||||
'dictionary but it does not exist. The mimetype for the request '
|
||||
'is "%s" instead of "multipart/form-data" which means that no '
|
||||
'file contents were transmitted. To fix this error you should '
|
||||
'provide enctype="multipart/form-data" in your form.' %
|
||||
(key, request.mimetype)]
|
||||
if form_matches:
|
||||
buf.append('\n\nThe browser instead transmitted some file names. '
|
||||
'This was submitted: %s' % ', '.join('"%s"' % x
|
||||
for x in form_matches))
|
||||
self.msg = ''.join(buf).encode('utf-8')
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class FormDataRoutingRedirect(AssertionError):
|
||||
"""This exception is raised by Flask in debug mode if it detects a
|
||||
redirect caused by the routing system when the request method is not
|
||||
GET, HEAD or OPTIONS. Reasoning: form data will be dropped.
|
||||
"""
|
||||
|
||||
def __init__(self, request):
|
||||
exc = request.routing_exception
|
||||
buf = ['A request was sent to this URL (%s) but a redirect was '
|
||||
'issued automatically by the routing system to "%s".'
|
||||
% (request.url, exc.new_url)]
|
||||
|
||||
# In case just a slash was appended we can be extra helpful
|
||||
if request.base_url + '/' == exc.new_url.split('?')[0]:
|
||||
buf.append(' The URL was defined with a trailing slash so '
|
||||
'Flask will automatically redirect to the URL '
|
||||
'with the trailing slash if it was accessed '
|
||||
'without one.')
|
||||
|
||||
buf.append(' Make sure to directly send your %s-request to this URL '
|
||||
'since we can\'t make browsers or HTTP clients redirect '
|
||||
'with form data.' % request.method)
|
||||
buf.append('\n\nNote: this exception is only raised in debug mode')
|
||||
AssertionError.__init__(self, ''.join(buf).encode('utf-8'))
|
||||
|
||||
|
||||
def attach_enctype_error_multidict(request):
|
||||
"""Since Flask 0.8 we're monkeypatching the files object in case a
|
||||
request is detected that does not use multipart form data but the files
|
||||
object is accessed.
|
||||
"""
|
||||
oldcls = request.files.__class__
|
||||
class newcls(oldcls):
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
return oldcls.__getitem__(self, key)
|
||||
except KeyError, e:
|
||||
if key not in request.form:
|
||||
raise
|
||||
raise DebugFilesKeyError(request, key)
|
||||
newcls.__name__ = oldcls.__name__
|
||||
newcls.__module__ = oldcls.__module__
|
||||
request.files.__class__ = newcls
|
||||
@@ -11,7 +11,7 @@
|
||||
"""
|
||||
|
||||
from functools import partial
|
||||
from werkzeug import LocalStack, LocalProxy
|
||||
from werkzeug.local import LocalStack, LocalProxy
|
||||
|
||||
def _lookup_object(name):
|
||||
top = _request_ctx_stack.top
|
||||
|
||||
@@ -9,12 +9,15 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from __future__ import with_statement
|
||||
|
||||
import os
|
||||
import sys
|
||||
import posixpath
|
||||
import mimetypes
|
||||
from time import time
|
||||
from zlib import adler32
|
||||
from threading import RLock
|
||||
|
||||
# try to load the best simplejson implementation available. If JSON
|
||||
# is not installed, we add a failing class.
|
||||
@@ -33,9 +36,15 @@ except ImportError:
|
||||
json_available = False
|
||||
|
||||
|
||||
from werkzeug import Headers, wrap_file, cached_property
|
||||
from werkzeug.datastructures import Headers
|
||||
from werkzeug.exceptions import NotFound
|
||||
|
||||
# this was moved in 0.7
|
||||
try:
|
||||
from werkzeug.wsgi import wrap_file
|
||||
except ImportError:
|
||||
from werkzeug.utils import wrap_file
|
||||
|
||||
from jinja2 import FileSystemLoader
|
||||
|
||||
from .globals import session, _request_ctx_stack, current_app, request
|
||||
@@ -58,6 +67,10 @@ else:
|
||||
_tojson_filter = json.dumps
|
||||
|
||||
|
||||
# sentinel
|
||||
_missing = object()
|
||||
|
||||
|
||||
# what separators does this operating system provide that are not a slash?
|
||||
# this is used by the send_from_directory function to ensure that nobody is
|
||||
# able to access files from outside the filesystem.
|
||||
@@ -151,22 +164,16 @@ def make_response(*args):
|
||||
|
||||
def url_for(endpoint, **values):
|
||||
"""Generates a URL to the given endpoint with the method provided.
|
||||
The endpoint is relative to the active module if modules are in use.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
==================== ======================= =============================
|
||||
Active Module Target Endpoint Target Function
|
||||
==================== ======================= =============================
|
||||
`None` ``'index'`` `index` of the application
|
||||
`None` ``'.index'`` `index` of the application
|
||||
``'admin'`` ``'index'`` `index` of the `admin` module
|
||||
any ``'.index'`` `index` of the application
|
||||
any ``'admin.index'`` `index` of the `admin` module
|
||||
==================== ======================= =============================
|
||||
|
||||
Variable arguments that are unknown to the target endpoint are appended
|
||||
to the generated URL as query arguments.
|
||||
to the generated URL as query arguments. If the value of a query argument
|
||||
is `None`, the whole pair is skipped. In case blueprints are active
|
||||
you can shortcut references to the same blueprint by prefixing the
|
||||
local endpoint with a dot (``.``).
|
||||
|
||||
This will reference the index function local to the current blueprint::
|
||||
|
||||
url_for('.index')
|
||||
|
||||
For more information, head over to the :ref:`Quickstart <url-building>`.
|
||||
|
||||
@@ -175,13 +182,22 @@ def url_for(endpoint, **values):
|
||||
:param _external: if set to `True`, an absolute URL is generated.
|
||||
"""
|
||||
ctx = _request_ctx_stack.top
|
||||
if '.' not in endpoint:
|
||||
mod = ctx.request.module
|
||||
if mod is not None:
|
||||
endpoint = mod + '.' + endpoint
|
||||
elif endpoint.startswith('.'):
|
||||
endpoint = endpoint[1:]
|
||||
blueprint_name = request.blueprint
|
||||
if not ctx.request._is_old_module:
|
||||
if endpoint[:1] == '.':
|
||||
if blueprint_name is not None:
|
||||
endpoint = blueprint_name + endpoint
|
||||
else:
|
||||
endpoint = endpoint[1:]
|
||||
else:
|
||||
# TODO: get rid of this deprecated functionality in 1.0
|
||||
if '.' not in endpoint:
|
||||
if blueprint_name is not None:
|
||||
endpoint = blueprint_name + '.' + endpoint
|
||||
elif endpoint.startswith('.'):
|
||||
endpoint = endpoint[1:]
|
||||
external = values.pop('_external', False)
|
||||
ctx.app.inject_url_defaults(endpoint, values)
|
||||
return ctx.url_adapter.build(endpoint, values, force_external=external)
|
||||
|
||||
|
||||
@@ -248,7 +264,8 @@ def get_flashed_messages(with_categories=False):
|
||||
"""
|
||||
flashes = _request_ctx_stack.top.flashes
|
||||
if flashes is None:
|
||||
_request_ctx_stack.top.flashes = flashes = session.pop('_flashes', [])
|
||||
_request_ctx_stack.top.flashes = flashes = session.pop('_flashes') \
|
||||
if '_flashes' in session else []
|
||||
if not with_categories:
|
||||
return [x[1] for x in flashes]
|
||||
return flashes
|
||||
@@ -321,7 +338,7 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
|
||||
if not attachment_filename and not mimetype \
|
||||
and isinstance(filename, basestring):
|
||||
warn(DeprecationWarning('The filename support for file objects '
|
||||
'passed to send_file is not deprecated. Pass an '
|
||||
'passed to send_file is now deprecated. Pass an '
|
||||
'attach_filename if you want mimetypes to be guessed.'),
|
||||
stacklevel=2)
|
||||
if add_etags:
|
||||
@@ -377,7 +394,10 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
|
||||
rv.set_etag('flask-%s-%s-%s' % (
|
||||
os.path.getmtime(filename),
|
||||
os.path.getsize(filename),
|
||||
adler32(filename) & 0xffffffff
|
||||
adler32(
|
||||
filename.encode('utf8') if isinstance(filename, unicode)
|
||||
else filename
|
||||
) & 0xffffffff
|
||||
))
|
||||
if conditional:
|
||||
rv = rv.make_conditional(request)
|
||||
@@ -388,6 +408,31 @@ def send_file(filename_or_fp, mimetype=None, as_attachment=False,
|
||||
return rv
|
||||
|
||||
|
||||
def safe_join(directory, filename):
|
||||
"""Safely join `directory` and `filename`.
|
||||
|
||||
Example usage::
|
||||
|
||||
@app.route('/wiki/<path:filename>')
|
||||
def wiki_page(filename):
|
||||
filename = safe_join(app.config['WIKI_FOLDER'], filename)
|
||||
with open(filename, 'rb') as fd:
|
||||
content = fd.read() # Read and process the file content...
|
||||
|
||||
:param directory: the base directory.
|
||||
:param filename: the untrusted filename relative to that directory.
|
||||
:raises: :class:`~werkzeug.exceptions.NotFound` if the resulting path
|
||||
would fall out of `directory`.
|
||||
"""
|
||||
filename = posixpath.normpath(filename)
|
||||
for sep in _os_alt_seps:
|
||||
if sep in filename:
|
||||
raise NotFound()
|
||||
if os.path.isabs(filename) or filename.startswith('../'):
|
||||
raise NotFound()
|
||||
return os.path.join(directory, filename)
|
||||
|
||||
|
||||
def send_from_directory(directory, filename, **options):
|
||||
"""Send a file from a given directory with :func:`send_file`. This
|
||||
is a secure way to quickly expose static files from an upload folder
|
||||
@@ -415,35 +460,128 @@ def send_from_directory(directory, filename, **options):
|
||||
:param options: optional keyword arguments that are directly
|
||||
forwarded to :func:`send_file`.
|
||||
"""
|
||||
filename = posixpath.normpath(filename)
|
||||
for sep in _os_alt_seps:
|
||||
if sep in filename:
|
||||
raise NotFound()
|
||||
if os.path.isabs(filename) or filename.startswith('../'):
|
||||
raise NotFound()
|
||||
filename = os.path.join(directory, filename)
|
||||
filename = safe_join(directory, filename)
|
||||
if not os.path.isfile(filename):
|
||||
raise NotFound()
|
||||
return send_file(filename, conditional=True, **options)
|
||||
|
||||
|
||||
def _get_package_path(name):
|
||||
"""Returns the path to a package or cwd if that cannot be found."""
|
||||
def get_root_path(import_name):
|
||||
"""Returns the path to a package or cwd if that cannot be found. This
|
||||
returns the path of a package or the folder that contains a module.
|
||||
|
||||
Not to be confused with the package path returned by :func:`find_package`.
|
||||
"""
|
||||
__import__(import_name)
|
||||
try:
|
||||
return os.path.abspath(os.path.dirname(sys.modules[name].__file__))
|
||||
except (KeyError, AttributeError):
|
||||
directory = os.path.dirname(sys.modules[import_name].__file__)
|
||||
return os.path.abspath(directory)
|
||||
except AttributeError:
|
||||
return os.getcwd()
|
||||
|
||||
|
||||
def find_package(import_name):
|
||||
"""Finds a package and returns the prefix (or None if the package is
|
||||
not installed) as well as the folder that contains the package or
|
||||
module as a tuple. The package path returned is the module that would
|
||||
have to be added to the pythonpath in order to make it possible to
|
||||
import the module. The prefix is the path below which a UNIX like
|
||||
folder structure exists (lib, share etc.).
|
||||
"""
|
||||
__import__(import_name)
|
||||
root_mod = sys.modules[import_name.split('.')[0]]
|
||||
package_path = getattr(root_mod, '__file__', None)
|
||||
if package_path is None:
|
||||
package_path = os.getcwd()
|
||||
else:
|
||||
package_path = os.path.abspath(os.path.dirname(package_path))
|
||||
if hasattr(root_mod, '__path__'):
|
||||
package_path = os.path.dirname(package_path)
|
||||
|
||||
# leave the egg wrapper folder or the actual .egg on the filesystem
|
||||
test_package_path = package_path
|
||||
if os.path.basename(test_package_path).endswith('.egg'):
|
||||
test_package_path = os.path.dirname(test_package_path)
|
||||
|
||||
site_parent, site_folder = os.path.split(test_package_path)
|
||||
py_prefix = os.path.abspath(sys.prefix)
|
||||
if test_package_path.startswith(py_prefix):
|
||||
return py_prefix, package_path
|
||||
elif site_folder.lower() == 'site-packages':
|
||||
parent, folder = os.path.split(site_parent)
|
||||
# Windows like installations
|
||||
if folder.lower() == 'lib':
|
||||
base_dir = parent
|
||||
# UNIX like installations
|
||||
elif os.path.basename(parent).lower() == 'lib':
|
||||
base_dir = os.path.dirname(parent)
|
||||
else:
|
||||
base_dir = site_parent
|
||||
return base_dir, package_path
|
||||
return None, package_path
|
||||
|
||||
|
||||
class locked_cached_property(object):
|
||||
"""A decorator that converts a function into a lazy property. The
|
||||
function wrapped is called the first time to retrieve the result
|
||||
and then that calculated result is used the next time you access
|
||||
the value. Works like the one in Werkzeug but has a lock for
|
||||
thread safety.
|
||||
"""
|
||||
|
||||
def __init__(self, func, name=None, doc=None):
|
||||
self.__name__ = name or func.__name__
|
||||
self.__module__ = func.__module__
|
||||
self.__doc__ = doc or func.__doc__
|
||||
self.func = func
|
||||
self.lock = RLock()
|
||||
|
||||
def __get__(self, obj, type=None):
|
||||
if obj is None:
|
||||
return self
|
||||
with self.lock:
|
||||
value = obj.__dict__.get(self.__name__, _missing)
|
||||
if value is _missing:
|
||||
value = self.func(obj)
|
||||
obj.__dict__[self.__name__] = value
|
||||
return value
|
||||
|
||||
|
||||
class _PackageBoundObject(object):
|
||||
|
||||
def __init__(self, import_name):
|
||||
def __init__(self, import_name, template_folder=None):
|
||||
#: The name of the package or module. Do not change this once
|
||||
#: it was set by the constructor.
|
||||
self.import_name = import_name
|
||||
|
||||
#: location of the templates. `None` if templates should not be
|
||||
#: exposed.
|
||||
self.template_folder = template_folder
|
||||
|
||||
#: Where is the app root located?
|
||||
self.root_path = _get_package_path(self.import_name)
|
||||
self.root_path = get_root_path(self.import_name)
|
||||
|
||||
self._static_folder = None
|
||||
self._static_url_path = None
|
||||
|
||||
def _get_static_folder(self):
|
||||
if self._static_folder is not None:
|
||||
return os.path.join(self.root_path, self._static_folder)
|
||||
def _set_static_folder(self, value):
|
||||
self._static_folder = value
|
||||
static_folder = property(_get_static_folder, _set_static_folder)
|
||||
del _get_static_folder, _set_static_folder
|
||||
|
||||
def _get_static_url_path(self):
|
||||
if self._static_url_path is None:
|
||||
if self.static_folder is None:
|
||||
return None
|
||||
return '/' + os.path.basename(self.static_folder)
|
||||
return self._static_url_path
|
||||
def _set_static_url_path(self, value):
|
||||
self._static_url_path = value
|
||||
static_url_path = property(_get_static_url_path, _set_static_url_path)
|
||||
del _get_static_url_path, _set_static_url_path
|
||||
|
||||
@property
|
||||
def has_static_folder(self):
|
||||
@@ -452,15 +590,17 @@ class _PackageBoundObject(object):
|
||||
|
||||
.. versionadded:: 0.5
|
||||
"""
|
||||
return os.path.isdir(os.path.join(self.root_path, 'static'))
|
||||
return self.static_folder is not None
|
||||
|
||||
@cached_property
|
||||
@locked_cached_property
|
||||
def jinja_loader(self):
|
||||
"""The Jinja loader for this package bound object.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
"""
|
||||
return FileSystemLoader(os.path.join(self.root_path, 'templates'))
|
||||
if self.template_folder is not None:
|
||||
return FileSystemLoader(os.path.join(self.root_path,
|
||||
self.template_folder))
|
||||
|
||||
def send_static_file(self, filename):
|
||||
"""Function used internally to send static files from the static
|
||||
@@ -468,10 +608,11 @@ class _PackageBoundObject(object):
|
||||
|
||||
.. versionadded:: 0.5
|
||||
"""
|
||||
return send_from_directory(os.path.join(self.root_path, 'static'),
|
||||
filename)
|
||||
if not self.has_static_folder:
|
||||
raise RuntimeError('No static folder for this object')
|
||||
return send_from_directory(self.static_folder, filename)
|
||||
|
||||
def open_resource(self, resource):
|
||||
def open_resource(self, resource, mode='rb'):
|
||||
"""Opens a resource from the application's resource folder. To see
|
||||
how this works, consider the following folder structure::
|
||||
|
||||
@@ -493,4 +634,6 @@ class _PackageBoundObject(object):
|
||||
:param resource: the name of the resource. To access resources within
|
||||
subfolders use forward slashes as separator.
|
||||
"""
|
||||
return open(os.path.join(self.root_path, resource), 'rb')
|
||||
if mode not in ('r', 'rb'):
|
||||
raise ValueError('Resources can only be opened for reading')
|
||||
return open(os.path.join(self.root_path, resource), mode)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from logging import getLogger, StreamHandler, Formatter, Logger, DEBUG
|
||||
from logging import getLogger, StreamHandler, Formatter, getLoggerClass, DEBUG
|
||||
|
||||
|
||||
def create_logger(app):
|
||||
@@ -21,6 +21,7 @@ def create_logger(app):
|
||||
function also removes all attached handlers in case there was a
|
||||
logger with the log name before.
|
||||
"""
|
||||
Logger = getLoggerClass()
|
||||
|
||||
class DebugLogger(Logger):
|
||||
def getEffectiveLevel(x):
|
||||
|
||||
@@ -9,109 +9,24 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from .helpers import _PackageBoundObject, _endpoint_from_view_func
|
||||
import os
|
||||
|
||||
from .blueprints import Blueprint
|
||||
|
||||
|
||||
def _register_module(module, static_path):
|
||||
"""Internal helper function that returns a function for recording
|
||||
that registers the `send_static_file` function for the module on
|
||||
the application if necessary. It also registers the module on
|
||||
the application.
|
||||
"""
|
||||
def _register(state):
|
||||
state.app.modules[module.name] = module
|
||||
# do not register the rule if the static folder of the
|
||||
# module is the same as the one from the application.
|
||||
if state.app.root_path == module.root_path:
|
||||
return
|
||||
path = static_path
|
||||
if path is None:
|
||||
path = state.app.static_path
|
||||
if state.url_prefix:
|
||||
path = state.url_prefix + path
|
||||
state.app.add_url_rule(path + '/<path:filename>',
|
||||
endpoint='%s.static' % module.name,
|
||||
view_func=module.send_static_file,
|
||||
subdomain=state.subdomain)
|
||||
return _register
|
||||
def blueprint_is_module(bp):
|
||||
"""Used to figure out if something is actually a module"""
|
||||
return isinstance(bp, Module)
|
||||
|
||||
|
||||
class _ModuleSetupState(object):
|
||||
class Module(Blueprint):
|
||||
"""Deprecated module support. Until Flask 0.6 modules were a different
|
||||
name of the concept now available as blueprints in Flask. They are
|
||||
essentially doing the same but have some bad semantics for templates and
|
||||
static files that were fixed with blueprints.
|
||||
|
||||
def __init__(self, app, url_prefix=None, subdomain=None):
|
||||
self.app = app
|
||||
self.url_prefix = url_prefix
|
||||
self.subdomain = subdomain
|
||||
|
||||
|
||||
class Module(_PackageBoundObject):
|
||||
"""Container object that enables pluggable applications. A module can
|
||||
be used to organize larger applications. They represent blueprints that,
|
||||
in combination with a :class:`Flask` object are used to create a large
|
||||
application.
|
||||
|
||||
A module is like an application bound to an `import_name`. Multiple
|
||||
modules can share the same import names, but in that case a `name` has
|
||||
to be provided to keep them apart. If different import names are used,
|
||||
the rightmost part of the import name is used as name.
|
||||
|
||||
Here's an example structure for a larger application::
|
||||
|
||||
/myapplication
|
||||
/__init__.py
|
||||
/views
|
||||
/__init__.py
|
||||
/admin.py
|
||||
/frontend.py
|
||||
|
||||
The `myapplication/__init__.py` can look like this::
|
||||
|
||||
from flask import Flask
|
||||
from myapplication.views.admin import admin
|
||||
from myapplication.views.frontend import frontend
|
||||
|
||||
app = Flask(__name__)
|
||||
app.register_module(admin, url_prefix='/admin')
|
||||
app.register_module(frontend)
|
||||
|
||||
And here's an example view module (`myapplication/views/admin.py`)::
|
||||
|
||||
from flask import Module
|
||||
|
||||
admin = Module(__name__)
|
||||
|
||||
@admin.route('/')
|
||||
def index():
|
||||
pass
|
||||
|
||||
@admin.route('/login')
|
||||
def login():
|
||||
pass
|
||||
|
||||
For a gentle introduction into modules, checkout the
|
||||
:ref:`working-with-modules` section.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
The `static_path` parameter was added and it's now possible for
|
||||
modules to refer to their own templates and static files. See
|
||||
:ref:`modules-and-resources` for more information.
|
||||
|
||||
.. versionadded:: 0.6
|
||||
The `subdomain` parameter was added.
|
||||
|
||||
:param import_name: the name of the Python package or module
|
||||
implementing this :class:`Module`.
|
||||
:param name: the internal short name for the module. Unless specified
|
||||
the rightmost part of the import name
|
||||
:param url_prefix: an optional string that is used to prefix all the
|
||||
URL rules of this module. This can also be specified
|
||||
when registering the module with the application.
|
||||
:param subdomain: used to set the subdomain setting for URL rules that
|
||||
do not have a subdomain setting set.
|
||||
:param static_path: can be used to specify a different path for the
|
||||
static files on the web. Defaults to ``/static``.
|
||||
This does not affect the folder the files are served
|
||||
*from*.
|
||||
.. versionchanged:: 0.7
|
||||
Modules were deprecated in favor for blueprints.
|
||||
"""
|
||||
|
||||
def __init__(self, import_name, name=None, url_prefix=None,
|
||||
@@ -120,111 +35,8 @@ class Module(_PackageBoundObject):
|
||||
assert '.' in import_name, 'name required if package name ' \
|
||||
'does not point to a submodule'
|
||||
name = import_name.rsplit('.', 1)[1]
|
||||
_PackageBoundObject.__init__(self, import_name)
|
||||
self.name = name
|
||||
self.url_prefix = url_prefix
|
||||
self.subdomain = subdomain
|
||||
self.view_functions = {}
|
||||
self._register_events = [_register_module(self, static_path)]
|
||||
Blueprint.__init__(self, name, import_name, url_prefix=url_prefix,
|
||||
subdomain=subdomain, template_folder='templates')
|
||||
|
||||
def route(self, rule, **options):
|
||||
"""Like :meth:`Flask.route` but for a module. The endpoint for the
|
||||
:func:`url_for` function is prefixed with the name of the module.
|
||||
"""
|
||||
def decorator(f):
|
||||
self.add_url_rule(rule, f.__name__, f, **options)
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def add_url_rule(self, rule, endpoint=None, view_func=None, **options):
|
||||
"""Like :meth:`Flask.add_url_rule` but for a module. The endpoint for
|
||||
the :func:`url_for` function is prefixed with the name of the module.
|
||||
|
||||
.. versionchanged:: 0.6
|
||||
The `endpoint` argument is now optional and will default to the
|
||||
function name to consistent with the function of the same name
|
||||
on the application object.
|
||||
"""
|
||||
def register_rule(state):
|
||||
the_rule = rule
|
||||
if state.url_prefix:
|
||||
the_rule = state.url_prefix + rule
|
||||
options.setdefault('subdomain', state.subdomain)
|
||||
the_endpoint = endpoint
|
||||
if the_endpoint is None:
|
||||
the_endpoint = _endpoint_from_view_func(view_func)
|
||||
state.app.add_url_rule(the_rule, '%s.%s' % (self.name,
|
||||
the_endpoint),
|
||||
view_func, **options)
|
||||
self._record(register_rule)
|
||||
|
||||
def endpoint(self, endpoint):
|
||||
"""Like :meth:`Flask.endpoint` but for a module."""
|
||||
def decorator(f):
|
||||
self.view_functions[endpoint] = f
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def before_request(self, f):
|
||||
"""Like :meth:`Flask.before_request` but for a module. This function
|
||||
is only executed before each request that is handled by a function of
|
||||
that module.
|
||||
"""
|
||||
self._record(lambda s: s.app.before_request_funcs
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def before_app_request(self, f):
|
||||
"""Like :meth:`Flask.before_request`. Such a function is executed
|
||||
before each request, even if outside of a module.
|
||||
"""
|
||||
self._record(lambda s: s.app.before_request_funcs
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def after_request(self, f):
|
||||
"""Like :meth:`Flask.after_request` but for a module. This function
|
||||
is only executed after each request that is handled by a function of
|
||||
that module.
|
||||
"""
|
||||
self._record(lambda s: s.app.after_request_funcs
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def after_app_request(self, f):
|
||||
"""Like :meth:`Flask.after_request` but for a module. Such a function
|
||||
is executed after each request, even if outside of the module.
|
||||
"""
|
||||
self._record(lambda s: s.app.after_request_funcs
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def context_processor(self, f):
|
||||
"""Like :meth:`Flask.context_processor` but for a module. This
|
||||
function is only executed for requests handled by a module.
|
||||
"""
|
||||
self._record(lambda s: s.app.template_context_processors
|
||||
.setdefault(self.name, []).append(f))
|
||||
return f
|
||||
|
||||
def app_context_processor(self, f):
|
||||
"""Like :meth:`Flask.context_processor` but for a module. Such a
|
||||
function is executed each request, even if outside of the module.
|
||||
"""
|
||||
self._record(lambda s: s.app.template_context_processors
|
||||
.setdefault(None, []).append(f))
|
||||
return f
|
||||
|
||||
def app_errorhandler(self, code):
|
||||
"""Like :meth:`Flask.errorhandler` but for a module. This
|
||||
handler is used for all requests, even if outside of the module.
|
||||
|
||||
.. versionadded:: 0.4
|
||||
"""
|
||||
def decorator(f):
|
||||
self._record(lambda s: s.app.errorhandler(code)(f))
|
||||
return f
|
||||
return decorator
|
||||
|
||||
def _record(self, func):
|
||||
self._register_events.append(func)
|
||||
if os.path.isdir(os.path.join(self.root_path, 'static')):
|
||||
self._static_folder = 'static'
|
||||
|
||||
@@ -3,41 +3,17 @@
|
||||
flask.session
|
||||
~~~~~~~~~~~~~
|
||||
|
||||
Implements cookie based sessions based on Werkzeug's secure cookie
|
||||
system.
|
||||
This module used to flask with the session global so we moved it
|
||||
over to flask.sessions
|
||||
|
||||
:copyright: (c) 2010 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from werkzeug.contrib.securecookie import SecureCookie
|
||||
from warnings import warn
|
||||
warn(DeprecationWarning('please use flask.sessions instead'))
|
||||
|
||||
from .sessions import *
|
||||
|
||||
class Session(SecureCookie):
|
||||
"""Expands the session with support for switching between permanent
|
||||
and non-permanent sessions.
|
||||
"""
|
||||
|
||||
def _get_permanent(self):
|
||||
return self.get('_permanent', False)
|
||||
|
||||
def _set_permanent(self, value):
|
||||
self['_permanent'] = bool(value)
|
||||
|
||||
permanent = property(_get_permanent, _set_permanent)
|
||||
del _get_permanent, _set_permanent
|
||||
|
||||
|
||||
class _NullSession(Session):
|
||||
"""Class used to generate nicer error messages if sessions are not
|
||||
available. Will still allow read-only access to the empty session
|
||||
but fail on setting.
|
||||
"""
|
||||
|
||||
def _fail(self, *args, **kwargs):
|
||||
raise RuntimeError('the session is unavailable because no secret '
|
||||
'key was set. Set the secret_key on the '
|
||||
'application to something unique and secret.')
|
||||
__setitem__ = __delitem__ = clear = pop = popitem = \
|
||||
update = setdefault = _fail
|
||||
del _fail
|
||||
Session = SecureCookieSession
|
||||
_NullSession = NullSession
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.sessions
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Implements cookie based sessions based on Werkzeug's secure cookie
|
||||
system.
|
||||
|
||||
:copyright: (c) 2010 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from werkzeug.contrib.securecookie import SecureCookie
|
||||
|
||||
|
||||
class SessionMixin(object):
|
||||
"""Expands a basic dictionary with an accessors that are expected
|
||||
by Flask extensions and users for the session.
|
||||
"""
|
||||
|
||||
def _get_permanent(self):
|
||||
return self.get('_permanent', False)
|
||||
|
||||
def _set_permanent(self, value):
|
||||
self['_permanent'] = bool(value)
|
||||
|
||||
#: this reflects the ``'_permanent'`` key in the dict.
|
||||
permanent = property(_get_permanent, _set_permanent)
|
||||
del _get_permanent, _set_permanent
|
||||
|
||||
#: some session backends can tell you if a session is new, but that is
|
||||
#: not necessarily guaranteed. Use with caution. The default mixin
|
||||
#: implementation just hardcodes `False` in.
|
||||
new = False
|
||||
|
||||
#: for some backends this will always be `True`, but some backends will
|
||||
#: default this to false and detect changes in the dictionary for as
|
||||
#: long as changes do not happen on mutable structures in the session.
|
||||
#: The default mixin implementation just hardcodes `True` in.
|
||||
modified = True
|
||||
|
||||
|
||||
class SecureCookieSession(SecureCookie, SessionMixin):
|
||||
"""Expands the session with support for switching between permanent
|
||||
and non-permanent sessions.
|
||||
"""
|
||||
|
||||
|
||||
class NullSession(SecureCookieSession):
|
||||
"""Class used to generate nicer error messages if sessions are not
|
||||
available. Will still allow read-only access to the empty session
|
||||
but fail on setting.
|
||||
"""
|
||||
|
||||
def _fail(self, *args, **kwargs):
|
||||
raise RuntimeError('the session is unavailable because no secret '
|
||||
'key was set. Set the secret_key on the '
|
||||
'application to something unique and secret.')
|
||||
__setitem__ = __delitem__ = clear = pop = popitem = \
|
||||
update = setdefault = _fail
|
||||
del _fail
|
||||
|
||||
|
||||
class SessionInterface(object):
|
||||
"""The basic interface you have to implement in order to replace the
|
||||
default session interface which uses werkzeug's securecookie
|
||||
implementation. The only methods you have to implement are
|
||||
:meth:`open_session` and :meth:`save_session`, the others have
|
||||
useful defaults which you don't need to change.
|
||||
|
||||
The session object returned by the :meth:`open_session` method has to
|
||||
provide a dictionary like interface plus the properties and methods
|
||||
from the :class:`SessionMixin`. We recommend just subclassing a dict
|
||||
and adding that mixin::
|
||||
|
||||
class Session(dict, SessionMixin):
|
||||
pass
|
||||
|
||||
If :meth:`open_session` returns `None` Flask will call into
|
||||
:meth:`make_null_session` to create a session that acts as replacement
|
||||
if the session support cannot work because some requirement is not
|
||||
fulfilled. The default :class:`NullSession` class that is created
|
||||
will complain that the secret key was not set.
|
||||
|
||||
To replace the session interface on an application all you have to do
|
||||
is to assign :attr:`flask.Flask.session_interface`::
|
||||
|
||||
app = Flask(__name__)
|
||||
app.session_interface = MySessionInterface()
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
|
||||
#: :meth:`make_null_session` will look here for the class that should
|
||||
#: be created when a null session is requested. Likewise the
|
||||
#: :meth:`is_null_session` method will perform a typecheck against
|
||||
#: this type.
|
||||
null_session_class = NullSession
|
||||
|
||||
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
|
||||
error. This mainly aids the user experience because the job of the
|
||||
null session is to still support lookup without complaining but
|
||||
modifications are answered with a helpful error message of what
|
||||
failed.
|
||||
|
||||
This creates an instance of :attr:`null_session_class` by default.
|
||||
"""
|
||||
return self.null_session_class()
|
||||
|
||||
def is_null_session(self, obj):
|
||||
"""Checks if a given object is a null session. Null sessions are
|
||||
not asked to be saved.
|
||||
|
||||
This checks if the object is an instance of :attr:`null_session_class`
|
||||
by default.
|
||||
"""
|
||||
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.
|
||||
"""
|
||||
if app.config['SERVER_NAME'] is not None:
|
||||
# chop of the port which is usually not supported by browsers
|
||||
return '.' + app.config['SERVER_NAME'].rsplit(':', 1)[0]
|
||||
|
||||
def get_expiration_time(self, app, session):
|
||||
"""A helper method that returns an expiration date for the session
|
||||
or `None` if the session is linked to the browser session. The
|
||||
default implementation returns now + the permanent session
|
||||
lifetime configured on the application.
|
||||
"""
|
||||
if session.permanent:
|
||||
return datetime.utcnow() + app.permanent_session_lifetime
|
||||
|
||||
def open_session(self, app, request):
|
||||
"""This method has to be implemented and must either return `None`
|
||||
in case the loading failed because of a configuration error or an
|
||||
instance of a session object which implements a dictionary like
|
||||
interface + the methods and attributes on :class:`SessionMixin`.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def save_session(self, app, session, response):
|
||||
"""This is called for actual sessions returned by :meth:`open_session`
|
||||
at the end of the request. This is still called during a request
|
||||
context so if you absolutely need access to the request you can do
|
||||
that.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class SecureCookieSessionInterface(SessionInterface):
|
||||
"""The cookie session interface that uses the Werkzeug securecookie
|
||||
as client side session backend.
|
||||
"""
|
||||
session_class = SecureCookieSession
|
||||
|
||||
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)
|
||||
|
||||
def save_session(self, app, session, response):
|
||||
expires = self.get_expiration_time(app, session)
|
||||
domain = self.get_cookie_domain(app)
|
||||
if session.modified and not session:
|
||||
response.delete_cookie(app.session_cookie_name,
|
||||
domain=domain)
|
||||
else:
|
||||
session.save_cookie(response, app.session_cookie_name,
|
||||
expires=expires, httponly=True, domain=domain)
|
||||
@@ -47,4 +47,5 @@ _signals = Namespace()
|
||||
template_rendered = _signals.signal('template-rendered')
|
||||
request_started = _signals.signal('request-started')
|
||||
request_finished = _signals.signal('request-finished')
|
||||
request_tearing_down = _signals.signal('request-tearing-down')
|
||||
got_request_exception = _signals.signal('got-request-exception')
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import posixpath
|
||||
from jinja2 import BaseLoader, TemplateNotFound
|
||||
from jinja2 import BaseLoader, Environment as BaseEnvironment, \
|
||||
TemplateNotFound
|
||||
|
||||
from .globals import _request_ctx_stack
|
||||
from .signals import template_rendered
|
||||
from .module import blueprint_is_module
|
||||
|
||||
|
||||
def _default_template_ctx_processor():
|
||||
@@ -28,40 +30,76 @@ def _default_template_ctx_processor():
|
||||
)
|
||||
|
||||
|
||||
class _DispatchingJinjaLoader(BaseLoader):
|
||||
class Environment(BaseEnvironment):
|
||||
"""Works like a regular Jinja2 environment but has some additional
|
||||
knowledge of how Flask's blueprint works so that it can prepend the
|
||||
name of the blueprint to referenced templates if necessary.
|
||||
"""
|
||||
|
||||
def __init__(self, app, **options):
|
||||
if 'loader' not in options:
|
||||
options['loader'] = app.create_global_jinja_loader()
|
||||
BaseEnvironment.__init__(self, **options)
|
||||
self.app = app
|
||||
|
||||
|
||||
class DispatchingJinjaLoader(BaseLoader):
|
||||
"""A loader that looks for templates in the application and all
|
||||
the module folders.
|
||||
the blueprint folders.
|
||||
"""
|
||||
|
||||
def __init__(self, app):
|
||||
self.app = app
|
||||
|
||||
def get_source(self, environment, template):
|
||||
template = posixpath.normpath(template)
|
||||
if template.startswith('../'):
|
||||
raise TemplateNotFound(template)
|
||||
loader = None
|
||||
try:
|
||||
module, name = template.split('/', 1)
|
||||
loader = self.app.modules[module].jinja_loader
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
# if there was a module and it has a loader, try this first
|
||||
if loader is not None:
|
||||
for loader, local_name in self._iter_loaders(template):
|
||||
try:
|
||||
return loader.get_source(environment, name)
|
||||
return loader.get_source(environment, local_name)
|
||||
except TemplateNotFound:
|
||||
pass
|
||||
# fall back to application loader if module failed
|
||||
return self.app.jinja_loader.get_source(environment, template)
|
||||
|
||||
raise TemplateNotFound(template)
|
||||
|
||||
def _iter_loaders(self, template):
|
||||
loader = self.app.jinja_loader
|
||||
if loader is not None:
|
||||
yield loader, template
|
||||
|
||||
# old style module based loaders in case we are dealing with a
|
||||
# blueprint that is an old style module
|
||||
try:
|
||||
module, local_name = posixpath.normpath(template).split('/', 1)
|
||||
blueprint = self.app.blueprints[module]
|
||||
if blueprint_is_module(blueprint):
|
||||
loader = blueprint.jinja_loader
|
||||
if loader is not None:
|
||||
yield loader, local_name
|
||||
except (ValueError, KeyError):
|
||||
pass
|
||||
|
||||
for blueprint in self.app.blueprints.itervalues():
|
||||
if blueprint_is_module(blueprint):
|
||||
continue
|
||||
loader = blueprint.jinja_loader
|
||||
if loader is not None:
|
||||
yield loader, template
|
||||
|
||||
def list_templates(self):
|
||||
result = self.app.jinja_loader.list_templates()
|
||||
for name, module in self.app.modules.iteritems():
|
||||
if module.jinja_loader is not None:
|
||||
for template in module.jinja_loader.list_templates():
|
||||
result.append('%s/%s' % (name, template))
|
||||
return result
|
||||
result = set()
|
||||
loader = self.app.jinja_loader
|
||||
if loader is not None:
|
||||
result.update(loader.list_templates())
|
||||
|
||||
for name, blueprint in self.app.blueprints.iteritems():
|
||||
loader = blueprint.jinja_loader
|
||||
if loader is not None:
|
||||
for template in loader.list_templates():
|
||||
prefix = ''
|
||||
if blueprint_is_module(blueprint):
|
||||
prefix = name + '/'
|
||||
result.add(prefix + template)
|
||||
|
||||
return list(result)
|
||||
|
||||
|
||||
def _render(template, context, app):
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from werkzeug import Client, EnvironBuilder
|
||||
from werkzeug.test import Client, EnvironBuilder
|
||||
from flask import _request_ctx_stack
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
flask.views
|
||||
~~~~~~~~~~~
|
||||
|
||||
This module provides class based views inspired by the ones in Django.
|
||||
|
||||
:copyright: (c) 2011 by Armin Ronacher.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from .globals import request
|
||||
|
||||
|
||||
http_method_funcs = frozenset(['get', 'post', 'head', 'options',
|
||||
'delete', 'put', 'trace'])
|
||||
|
||||
|
||||
|
||||
class View(object):
|
||||
"""Alternative way to use view functions. A subclass has to implement
|
||||
:meth:`dispatch_request` which is called with the view arguments from
|
||||
the URL routing system. If :attr:`methods` is provided the methods
|
||||
do not have to be passed to the :meth:`~flask.Flask.add_url_rule`
|
||||
method explicitly::
|
||||
|
||||
class MyView(View):
|
||||
methods = ['GET']
|
||||
|
||||
def dispatch_request(self, name):
|
||||
return 'Hello %s!' % name
|
||||
|
||||
app.add_url_rule('/hello/<name>', view_func=MyView.as_view('myview'))
|
||||
"""
|
||||
|
||||
methods = None
|
||||
|
||||
def dispatch_request(self):
|
||||
"""Subclasses have to override this method to implement the
|
||||
actual view functionc ode. This method is called with all
|
||||
the arguments from the URL rule.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def as_view(cls, name, *class_args, **class_kwargs):
|
||||
"""Converts the class into an actual view function that can be
|
||||
used with the routing system. What it does internally is generating
|
||||
a function on the fly that will instanciate the :class:`View`
|
||||
on each request and call the :meth:`dispatch_request` method on it.
|
||||
|
||||
The arguments passed to :meth:`as_view` are forwarded to the
|
||||
constructor of the class.
|
||||
"""
|
||||
def view(*args, **kwargs):
|
||||
self = view.view_class(*class_args, **class_kwargs)
|
||||
return self.dispatch_request(*args, **kwargs)
|
||||
# we attach the view class to the view function for two reasons:
|
||||
# first of all it allows us to easily figure out what class based
|
||||
# view this thing came from, secondly it's also used for instanciating
|
||||
# the view class so you can actually replace it with something else
|
||||
# for testing purposes and debugging.
|
||||
view.view_class = cls
|
||||
view.__name__ = name
|
||||
view.__doc__ = cls.__doc__
|
||||
view.__module__ = cls.__module__
|
||||
view.methods = cls.methods
|
||||
return view
|
||||
|
||||
|
||||
class MethodViewType(type):
|
||||
|
||||
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, value in d.iteritems():
|
||||
if key in http_method_funcs:
|
||||
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 baseclass or another subclass of a base method view
|
||||
# that does not introduce new methods).
|
||||
if methods:
|
||||
rv.methods = sorted(methods)
|
||||
return rv
|
||||
|
||||
|
||||
class MethodView(View):
|
||||
"""Like a regular class based view but that dispatches requests to
|
||||
particular methods. For instance if you implement a method called
|
||||
:meth:`get` it means you will response to ``'GET'`` requests and
|
||||
the :meth:`dispatch_request` implementation will automatically
|
||||
forward your request to that. Also :attr:`options` is set for you
|
||||
automatically::
|
||||
|
||||
class CounterAPI(MethodView):
|
||||
|
||||
def get(self):
|
||||
return session.get('counter', 0)
|
||||
|
||||
def post(self):
|
||||
session['counter'] = session.get('counter', 0) + 1
|
||||
return 'OK'
|
||||
|
||||
app.add_url_rule('/counter', view_func=CounterAPI.as_view('counter'))
|
||||
"""
|
||||
__metaclass__ = MethodViewType
|
||||
|
||||
def dispatch_request(self, *args, **kwargs):
|
||||
meth = getattr(self, request.method.lower(), None)
|
||||
assert meth is not None, 'Not implemented method'
|
||||
return meth(*args, **kwargs)
|
||||
@@ -9,9 +9,11 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
from werkzeug import Request as RequestBase, Response as ResponseBase, \
|
||||
cached_property
|
||||
from werkzeug.wrappers import Request as RequestBase, Response as ResponseBase
|
||||
from werkzeug.exceptions import BadRequest
|
||||
from werkzeug.utils import cached_property
|
||||
|
||||
from .debughelpers import attach_enctype_error_multidict
|
||||
from .helpers import json, _assert_have_json
|
||||
from .globals import _request_ctx_stack
|
||||
|
||||
@@ -23,6 +25,10 @@ class Request(RequestBase):
|
||||
It is what ends up as :class:`~flask.request`. If you want to replace
|
||||
the request object used you can subclass this and set
|
||||
:attr:`~flask.Flask.request_class` to your subclass.
|
||||
|
||||
The request object is a :class:`~werkzeug.wrappers.Request` subclass and
|
||||
provides all of the attributes Werkzeug defines plus a few Flask
|
||||
specific ones.
|
||||
"""
|
||||
|
||||
#: the internal URL rule that matched the request. This can be
|
||||
@@ -42,6 +48,10 @@ class Request(RequestBase):
|
||||
#: something similar.
|
||||
routing_exception = None
|
||||
|
||||
# switched by the request context until 1.0 to opt in deprecated
|
||||
# module functionality
|
||||
_is_old_module = False
|
||||
|
||||
@property
|
||||
def max_content_length(self):
|
||||
"""Read-only view of the `MAX_CONTENT_LENGTH` config key."""
|
||||
@@ -61,19 +71,59 @@ class Request(RequestBase):
|
||||
|
||||
@property
|
||||
def module(self):
|
||||
"""The name of the current module"""
|
||||
"""The name of the current module if the request was dispatched
|
||||
to an actual module. This is deprecated functionality, use blueprints
|
||||
instead.
|
||||
"""
|
||||
from warnings import warn
|
||||
warn(DeprecationWarning('modules were deprecated in favor of '
|
||||
'blueprints. Use request.blueprint '
|
||||
'instead.'), stacklevel=2)
|
||||
if self._is_old_module:
|
||||
return self.blueprint
|
||||
|
||||
@property
|
||||
def blueprint(self):
|
||||
"""The name of the current blueprint"""
|
||||
if self.url_rule and '.' in self.url_rule.endpoint:
|
||||
return self.url_rule.endpoint.rsplit('.', 1)[0]
|
||||
|
||||
@cached_property
|
||||
def json(self):
|
||||
"""If the mimetype is `application/json` this will contain the
|
||||
parsed JSON data.
|
||||
parsed JSON data. Otherwise this will be `None`.
|
||||
|
||||
This requires Python 2.6 or an installed version of simplejson.
|
||||
"""
|
||||
if __debug__:
|
||||
_assert_have_json()
|
||||
if self.mimetype == 'application/json':
|
||||
return json.loads(self.data)
|
||||
request_charset = self.mimetype_params.get('charset')
|
||||
try:
|
||||
if request_charset is not None:
|
||||
return json.loads(self.data, encoding=request_charset)
|
||||
return json.loads(self.data)
|
||||
except ValueError, e:
|
||||
return self.on_json_loading_failed(e)
|
||||
|
||||
def on_json_loading_failed(self, e):
|
||||
"""Called if decoding of the JSON data failed. The return value of
|
||||
this method is used by :attr:`json` when an error ocurred. The
|
||||
default implementation raises a :class:`~werkzeug.exceptions.BadRequest`.
|
||||
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
raise BadRequest()
|
||||
|
||||
def _load_form_data(self):
|
||||
RequestBase._load_form_data(self)
|
||||
|
||||
# in debug mode we're replacing the files multidict with an ad-hoc
|
||||
# subclass that raises a different error for key errors.
|
||||
ctx = _request_ctx_stack.top
|
||||
if ctx is not None and ctx.app.debug and \
|
||||
self.mimetype != 'multipart/form-data' and not self.files:
|
||||
attach_enctype_error_multidict(self)
|
||||
|
||||
|
||||
class Response(ResponseBase):
|
||||
|
||||
@@ -27,11 +27,7 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
__docformat__ = 'restructuredtext en'
|
||||
try:
|
||||
__version__ = __import__('pkg_resources') \
|
||||
.get_distribution('Jinja2').version
|
||||
except Exception:
|
||||
__version__ = 'unknown'
|
||||
__version__ = '2.7-dev'
|
||||
|
||||
# high level interface
|
||||
from jinja2.environment import Environment, Template
|
||||
|
||||
@@ -20,7 +20,6 @@ import marshal
|
||||
import tempfile
|
||||
import cPickle as pickle
|
||||
import fnmatch
|
||||
from cStringIO import StringIO
|
||||
try:
|
||||
from hashlib import sha1
|
||||
except ImportError:
|
||||
@@ -28,6 +27,26 @@ except ImportError:
|
||||
from jinja2.utils import open_if_exists
|
||||
|
||||
|
||||
# marshal works better on 3.x, one hack less required
|
||||
if sys.version_info > (3, 0):
|
||||
from io import BytesIO
|
||||
marshal_dump = marshal.dump
|
||||
marshal_load = marshal.load
|
||||
else:
|
||||
from cStringIO import StringIO as BytesIO
|
||||
|
||||
def marshal_dump(code, f):
|
||||
if isinstance(f, file):
|
||||
marshal.dump(code, f)
|
||||
else:
|
||||
f.write(marshal.dumps(code))
|
||||
|
||||
def marshal_load(f):
|
||||
if isinstance(f, file):
|
||||
return marshal.load(f)
|
||||
return marshal.loads(f.read())
|
||||
|
||||
|
||||
bc_version = 2
|
||||
|
||||
# magic version used to only change with new jinja versions. With 2.6
|
||||
@@ -71,12 +90,7 @@ class Bucket(object):
|
||||
if self.checksum != checksum:
|
||||
self.reset()
|
||||
return
|
||||
# now load the code. Because marshal is not able to load
|
||||
# from arbitrary streams we have to work around that
|
||||
if isinstance(f, file):
|
||||
self.code = marshal.load(f)
|
||||
else:
|
||||
self.code = marshal.loads(f.read())
|
||||
self.code = marshal_load(f)
|
||||
|
||||
def write_bytecode(self, f):
|
||||
"""Dump the bytecode into the file or file like object passed."""
|
||||
@@ -84,18 +98,15 @@ class Bucket(object):
|
||||
raise TypeError('can\'t write empty bucket')
|
||||
f.write(bc_magic)
|
||||
pickle.dump(self.checksum, f, 2)
|
||||
if isinstance(f, file):
|
||||
marshal.dump(self.code, f)
|
||||
else:
|
||||
f.write(marshal.dumps(self.code))
|
||||
marshal_dump(self.code, f)
|
||||
|
||||
def bytecode_from_string(self, string):
|
||||
"""Load bytecode from a string."""
|
||||
self.load_bytecode(StringIO(string))
|
||||
self.load_bytecode(BytesIO(string))
|
||||
|
||||
def bytecode_to_string(self):
|
||||
"""Return the bytecode as string."""
|
||||
out = StringIO()
|
||||
out = BytesIO()
|
||||
self.write_bytecode(out)
|
||||
return out.getvalue()
|
||||
|
||||
@@ -153,9 +164,10 @@ class BytecodeCache(object):
|
||||
"""Returns the unique hash key for this template name."""
|
||||
hash = sha1(name.encode('utf-8'))
|
||||
if filename is not None:
|
||||
filename = '|' + filename
|
||||
if isinstance(filename, unicode):
|
||||
filename = filename.encode('utf-8')
|
||||
hash.update('|' + filename)
|
||||
hash.update(filename)
|
||||
return hash.hexdigest()
|
||||
|
||||
def get_source_checksum(self, source):
|
||||
|
||||
@@ -127,12 +127,10 @@ class Identifiers(object):
|
||||
self.undeclared.discard(name)
|
||||
self.declared.add(name)
|
||||
|
||||
def is_declared(self, name, local_only=False):
|
||||
def is_declared(self, name):
|
||||
"""Check if a name is declared in this or an outer scope."""
|
||||
if name in self.declared_locally or name in self.declared_parameter:
|
||||
return True
|
||||
if local_only:
|
||||
return False
|
||||
return name in self.declared
|
||||
|
||||
def copy(self):
|
||||
@@ -193,12 +191,12 @@ class Frame(object):
|
||||
rv.identifiers.__dict__.update(self.identifiers.__dict__)
|
||||
return rv
|
||||
|
||||
def inspect(self, nodes, hard_scope=False):
|
||||
def inspect(self, nodes):
|
||||
"""Walk the node and check for identifiers. If the scope is hard (eg:
|
||||
enforce on a python level) overrides from outer scopes are tracked
|
||||
differently.
|
||||
"""
|
||||
visitor = FrameIdentifierVisitor(self.identifiers, hard_scope)
|
||||
visitor = FrameIdentifierVisitor(self.identifiers)
|
||||
for node in nodes:
|
||||
visitor.visit(node)
|
||||
|
||||
@@ -275,9 +273,8 @@ class UndeclaredNameVisitor(NodeVisitor):
|
||||
class FrameIdentifierVisitor(NodeVisitor):
|
||||
"""A visitor for `Frame.inspect`."""
|
||||
|
||||
def __init__(self, identifiers, hard_scope):
|
||||
def __init__(self, identifiers):
|
||||
self.identifiers = identifiers
|
||||
self.hard_scope = hard_scope
|
||||
|
||||
def visit_Name(self, node):
|
||||
"""All assignments to names go through this function."""
|
||||
@@ -286,7 +283,7 @@ class FrameIdentifierVisitor(NodeVisitor):
|
||||
elif node.ctx == 'param':
|
||||
self.identifiers.declared_parameter.add(node.name)
|
||||
elif node.ctx == 'load' and not \
|
||||
self.identifiers.is_declared(node.name, self.hard_scope):
|
||||
self.identifiers.is_declared(node.name):
|
||||
self.identifiers.undeclared.add(node.name)
|
||||
|
||||
def visit_If(self, node):
|
||||
@@ -658,7 +655,7 @@ class CodeGenerator(NodeVisitor):
|
||||
children = node.iter_child_nodes()
|
||||
children = list(children)
|
||||
func_frame = frame.inner()
|
||||
func_frame.inspect(children, hard_scope=True)
|
||||
func_frame.inspect(children)
|
||||
|
||||
# variables that are undeclared (accessed before declaration) and
|
||||
# declared locally *and* part of an outside scope raise a template
|
||||
|
||||
@@ -45,7 +45,13 @@ class TracebackFrameProxy(object):
|
||||
|
||||
def set_next(self, next):
|
||||
if tb_set_next is not None:
|
||||
tb_set_next(self.tb, next and next.tb or None)
|
||||
try:
|
||||
tb_set_next(self.tb, next and next.tb or None)
|
||||
except Exception:
|
||||
# this function can fail due to all the hackery it does
|
||||
# on various python implementations. We just catch errors
|
||||
# down and ignore them if necessary.
|
||||
pass
|
||||
self._tb_next = next
|
||||
|
||||
@property
|
||||
|
||||
@@ -53,7 +53,7 @@ def make_attrgetter(environment, attribute):
|
||||
passed object with the rules of the environment. Dots are allowed
|
||||
to access attributes of attributes.
|
||||
"""
|
||||
if '.' not in attribute:
|
||||
if not isinstance(attribute, basestring) or '.' not in attribute:
|
||||
return lambda x: environment.getitem(x, attribute)
|
||||
attribute = attribute.split('.')
|
||||
def attrgetter(item):
|
||||
@@ -338,21 +338,33 @@ def do_random(environment, seq):
|
||||
|
||||
|
||||
def do_filesizeformat(value, binary=False):
|
||||
"""Format the value like a 'human-readable' file size (i.e. 13 KB,
|
||||
4.1 MB, 102 bytes, etc). Per default decimal prefixes are used (mega,
|
||||
giga, etc.), if the second parameter is set to `True` the binary
|
||||
prefixes are used (mebi, gibi).
|
||||
"""Format the value like a 'human-readable' file size (i.e. 13 kB,
|
||||
4.1 MB, 102 Bytes, etc). Per default decimal prefixes are used (Mega,
|
||||
Giga, etc.), if the second parameter is set to `True` the binary
|
||||
prefixes are used (Mebi, Gibi).
|
||||
"""
|
||||
bytes = float(value)
|
||||
base = binary and 1024 or 1000
|
||||
middle = binary and 'i' or ''
|
||||
if bytes < base:
|
||||
return "%d Byte%s" % (bytes, bytes != 1 and 's' or '')
|
||||
elif bytes < base * base:
|
||||
return "%.1f K%sB" % (bytes / base, middle)
|
||||
elif bytes < base * base * base:
|
||||
return "%.1f M%sB" % (bytes / (base * base), middle)
|
||||
return "%.1f G%sB" % (bytes / (base * base * base), middle)
|
||||
prefixes = [
|
||||
(binary and "KiB" or "kB"),
|
||||
(binary and "MiB" or "MB"),
|
||||
(binary and "GiB" or "GB"),
|
||||
(binary and "TiB" or "TB"),
|
||||
(binary and "PiB" or "PB"),
|
||||
(binary and "EiB" or "EB"),
|
||||
(binary and "ZiB" or "ZB"),
|
||||
(binary and "YiB" or "YB")
|
||||
]
|
||||
if bytes == 1:
|
||||
return "1 Byte"
|
||||
elif bytes < base:
|
||||
return "%d Bytes" % bytes
|
||||
else:
|
||||
for i, prefix in enumerate(prefixes):
|
||||
unit = base * base ** (i + 1)
|
||||
if bytes < unit:
|
||||
return "%.1f %s" % ((bytes / unit), prefix)
|
||||
return "%.1f %s" % ((bytes / unit), prefix)
|
||||
|
||||
|
||||
def do_pprint(value, verbose=False):
|
||||
@@ -431,8 +443,8 @@ def do_truncate(s, length=255, killwords=False, end='...'):
|
||||
result.append(end)
|
||||
return u' '.join(result)
|
||||
|
||||
|
||||
def do_wordwrap(s, width=79, break_long_words=True):
|
||||
@environmentfilter
|
||||
def do_wordwrap(environment, s, width=79, break_long_words=True):
|
||||
"""
|
||||
Return a copy of the string passed to the filter wrapped after
|
||||
``79`` characters. You can override this default using the first
|
||||
@@ -440,7 +452,7 @@ def do_wordwrap(s, width=79, break_long_words=True):
|
||||
split words apart if they are longer than `width`.
|
||||
"""
|
||||
import textwrap
|
||||
return u'\n'.join(textwrap.wrap(s, width=width, expand_tabs=False,
|
||||
return environment.newline_sequence.join(textwrap.wrap(s, width=width, expand_tabs=False,
|
||||
replace_whitespace=False,
|
||||
break_long_words=break_long_words))
|
||||
|
||||
|
||||
@@ -251,8 +251,7 @@ class PackageLoader(BaseLoader):
|
||||
for filename in self.provider.resource_listdir(path):
|
||||
fullname = path + '/' + filename
|
||||
if self.provider.resource_isdir(fullname):
|
||||
for item in _walk(fullname):
|
||||
results.append(item)
|
||||
_walk(fullname)
|
||||
else:
|
||||
results.append(fullname[offset:].lstrip('/'))
|
||||
_walk(path)
|
||||
|
||||
@@ -75,8 +75,6 @@ class TemplateReference(object):
|
||||
|
||||
def __getitem__(self, name):
|
||||
blocks = self.__context.blocks[name]
|
||||
wrap = self.__context.eval_ctx.autoescape and \
|
||||
Markup or (lambda x: x)
|
||||
return BlockReference(name, self.__context, blocks, 0)
|
||||
|
||||
def __repr__(self):
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
import re
|
||||
from jinja2.runtime import Undefined
|
||||
|
||||
try:
|
||||
from collections import Mapping as MappingType
|
||||
except ImportError:
|
||||
import UserDict
|
||||
MappingType = (UserDict.UserDict, UserDict.DictMixin, dict)
|
||||
|
||||
# nose, nothing here to test
|
||||
__test__ = False
|
||||
|
||||
@@ -83,6 +89,14 @@ def test_string(value):
|
||||
return isinstance(value, basestring)
|
||||
|
||||
|
||||
def test_mapping(value):
|
||||
"""Return true if the object is a mapping (dict etc.).
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
return isinstance(value, MappingType)
|
||||
|
||||
|
||||
def test_number(value):
|
||||
"""Return true if the variable is a number."""
|
||||
return isinstance(value, (int, long, float, complex))
|
||||
@@ -137,6 +151,7 @@ TESTS = {
|
||||
'lower': test_lower,
|
||||
'upper': test_upper,
|
||||
'string': test_string,
|
||||
'mapping': test_mapping,
|
||||
'number': test_number,
|
||||
'sequence': test_sequence,
|
||||
'iterable': test_iterable,
|
||||
|
||||
@@ -85,8 +85,8 @@ class FilterTestCase(JinjaTestCase):
|
||||
)
|
||||
out = tmpl.render()
|
||||
assert out == (
|
||||
'100 Bytes|1.0 KB|1.0 MB|1.0 GB|1000.0 GB|'
|
||||
'100 Bytes|1000 Bytes|976.6 KiB|953.7 MiB|931.3 GiB'
|
||||
'100 Bytes|0.0 kB|0.0 MB|0.0 GB|0.0 TB|100 Bytes|'
|
||||
'1000 Bytes|1.0 KiB|0.9 MiB|0.9 GiB'
|
||||
)
|
||||
|
||||
def test_first(self):
|
||||
@@ -288,6 +288,13 @@ class FilterTestCase(JinjaTestCase):
|
||||
""
|
||||
]
|
||||
|
||||
def test_groupby_tuple_index(self):
|
||||
tmpl = env.from_string('''
|
||||
{%- for grouper, list in [('a', 1), ('a', 2), ('b', 1)]|groupby(0) -%}
|
||||
{{ grouper }}{% for x in list %}:{{ x.1 }}{% endfor %}|
|
||||
{%- endfor %}''')
|
||||
assert tmpl.render() == 'a:1:2|b:1|'
|
||||
|
||||
def test_groupby_multidot(self):
|
||||
class Date(object):
|
||||
def __init__(self, day, month, year):
|
||||
|
||||
@@ -48,10 +48,16 @@ class TestsTestCase(JinjaTestCase):
|
||||
{{ range is callable }}
|
||||
{{ 42 is callable }}
|
||||
{{ range(5) is iterable }}
|
||||
{{ {} is mapping }}
|
||||
{{ mydict is mapping }}
|
||||
{{ [] is mapping }}
|
||||
''')
|
||||
assert tmpl.render().split() == [
|
||||
class MyDict(dict):
|
||||
pass
|
||||
assert tmpl.render(mydict=MyDict()).split() == [
|
||||
'False', 'True', 'False', 'True', 'True', 'False',
|
||||
'True', 'True', 'True', 'True', 'False', 'True'
|
||||
'True', 'True', 'True', 'True', 'False', 'True',
|
||||
'True', 'True', 'False'
|
||||
]
|
||||
|
||||
def test_sequence(self):
|
||||
|
||||
@@ -11,12 +11,17 @@
|
||||
library.
|
||||
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from types import ModuleType
|
||||
import sys
|
||||
|
||||
|
||||
# the version. Usually set automatically by a script.
|
||||
__version__ = '0.8-dev'
|
||||
|
||||
|
||||
# This import magic raises concerns quite often which is why the implementation
|
||||
# and motivation is explained here in detail now.
|
||||
#
|
||||
@@ -106,11 +111,6 @@ for module, items in all_by_module.iteritems():
|
||||
object_origins[item] = module
|
||||
|
||||
|
||||
#: the cached version of the library. We get the distribution from
|
||||
#: pkg_resources the first time this attribute is accessed. Because
|
||||
#: this operation is quite slow it speeds up importing a lot.
|
||||
version = None
|
||||
|
||||
class module(ModuleType):
|
||||
"""Automatically import objects from the modules."""
|
||||
|
||||
@@ -132,17 +132,6 @@ class module(ModuleType):
|
||||
'__package__', '__version__'))
|
||||
return result
|
||||
|
||||
@property
|
||||
def __version__(self):
|
||||
global version
|
||||
if version is None:
|
||||
try:
|
||||
version = __import__('pkg_resources') \
|
||||
.get_distribution('Werkzeug').version
|
||||
except Exception:
|
||||
version = 'unknown'
|
||||
return version
|
||||
|
||||
# keep a reference to this module so that it's not garbage collected
|
||||
old_module = sys.modules['werkzeug']
|
||||
|
||||
@@ -154,6 +143,7 @@ new_module.__dict__.update({
|
||||
'__package__': 'werkzeug',
|
||||
'__path__': __path__,
|
||||
'__doc__': __doc__,
|
||||
'__version__': __version__,
|
||||
'__all__': tuple(object_origins) + tuple(attribute_modules),
|
||||
'__docformat__': 'restructuredtext en'
|
||||
})
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This module provides internally used helpers and constants.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import inspect
|
||||
@@ -204,7 +204,7 @@ def _decode_unicode(value, charset, errors):
|
||||
return value.decode(charset, errors)
|
||||
except UnicodeError, e:
|
||||
if fallback is not None:
|
||||
return value.decode(fallback, 'ignore')
|
||||
return value.decode(fallback, 'replace')
|
||||
from werkzeug.exceptions import HTTPUnicodeError
|
||||
raise HTTPUnicodeError(str(e))
|
||||
|
||||
@@ -383,8 +383,11 @@ mj2Z/FM1vQWgDynsRwNvrWnJHlespkrp8+vO1jNaibm+PhqXPPv30YwDZ6jApe3wUjFQobghvW9p
|
||||
if environ.get('QUERY_STRING') != 'macgybarchakku':
|
||||
return app(environ, injecting_start_response)
|
||||
injecting_start_response('200 OK', [('Content-Type', 'text/html')])
|
||||
return ['''<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
|
||||
<title>About Werkzeug</>
|
||||
return ['''
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>About Werkzeug</title>
|
||||
<style type="text/css">
|
||||
body { font: 15px Georgia, serif; text-align: center; }
|
||||
a { color: #333; text-decoration: none; }
|
||||
@@ -392,7 +395,11 @@ mj2Z/FM1vQWgDynsRwNvrWnJHlespkrp8+vO1jNaibm+PhqXPPv30YwDZ6jApe3wUjFQobghvW9p
|
||||
p { margin: 0 0 30px 0; }
|
||||
pre { font: 11px 'Consolas', 'Monaco', monospace; line-height: 0.95; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1><a href="http://werkzeug.pocoo.org/">Werkzeug</a></h1>
|
||||
<p>the Swiss Army knife of Python web development.
|
||||
<pre>%s\n\n\n</>''' % gyver]
|
||||
<p>the Swiss Army knife of Python web development.</p>
|
||||
<pre>%s\n\n\n</pre>
|
||||
</body>
|
||||
</html>''' % gyver]
|
||||
return easteregged
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
This file itself is mostly for informational purposes and to tell the
|
||||
Python interpreter that `contrib` is a package.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
updated=post.last_update, published=post.pub_date)
|
||||
return feed.get_response()
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from datetime import datetime
|
||||
@@ -152,7 +152,7 @@ class AtomFeed(object):
|
||||
# atom demands either an author element in every entry or a global one
|
||||
if not self.author:
|
||||
if False in map(lambda e: bool(e.author), self.entries):
|
||||
self.author = ({'name': u'unbekannter Autor'},)
|
||||
self.author = ({'name': 'Unknown author'},)
|
||||
|
||||
if not self.updated:
|
||||
dates = sorted([entry.updated for entry in self.entries])
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
you have access to it (either as a module global you can import or you just
|
||||
put it into your WSGI application).
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
@@ -68,6 +68,19 @@ from time import time
|
||||
from cPickle import loads, dumps, load, dump, HIGHEST_PROTOCOL
|
||||
from werkzeug.posixemulation import rename
|
||||
|
||||
def _items(mappingorseq):
|
||||
"""Wrapper for efficient iteration over mappings represented by dicts
|
||||
or sequences::
|
||||
|
||||
>>> for k, v in _items((i, i*i) for i in xrange(5)):
|
||||
... assert k*k == v
|
||||
|
||||
>>> for k, v in _items(dict((i, i*i) for i in xrange(5))):
|
||||
... assert k*k == v
|
||||
|
||||
"""
|
||||
return mappingorseq.iteritems() if hasattr(mappingorseq, 'iteritems') \
|
||||
else mappingorseq
|
||||
|
||||
class BaseCache(object):
|
||||
"""Baseclass for the cache systems. All the cache systems implement this
|
||||
@@ -145,13 +158,13 @@ class BaseCache(object):
|
||||
pass
|
||||
|
||||
def set_many(self, mapping, timeout=None):
|
||||
"""Sets multiple keys and values from a dict.
|
||||
"""Sets multiple keys and values from a mapping.
|
||||
|
||||
:param mapping: a dict with the keys/values to set.
|
||||
:param mapping: a mapping with the keys/values to set.
|
||||
:param timeout: the cache timeout for the key (if not specified,
|
||||
it uses the default timeout).
|
||||
"""
|
||||
for key, value in mapping.iteritems():
|
||||
for key, value in _items(mapping):
|
||||
self.set(key, value, timeout)
|
||||
|
||||
def delete_many(self, *keys):
|
||||
@@ -279,35 +292,40 @@ class MemcachedCache(BaseCache):
|
||||
def __init__(self, servers, default_timeout=300, key_prefix=None):
|
||||
BaseCache.__init__(self, default_timeout)
|
||||
if isinstance(servers, (list, tuple)):
|
||||
is_cmemcached = is_cmemcache = is_pylibmc = False
|
||||
try:
|
||||
import cmemcache as memcache
|
||||
is_cmemcache = True
|
||||
import cmemcached as memcache
|
||||
is_cmemcached = True
|
||||
except ImportError:
|
||||
try:
|
||||
import memcache
|
||||
is_cmemcache = False
|
||||
is_pylibmc = False
|
||||
import cmemcache as memcache
|
||||
is_cmemcache = True
|
||||
except ImportError:
|
||||
try:
|
||||
import pylibmc as memcache
|
||||
import memcache
|
||||
is_cmemcache = False
|
||||
is_pylibmc = True
|
||||
is_pylibmc = False
|
||||
except ImportError:
|
||||
raise RuntimeError('no memcache module found')
|
||||
try:
|
||||
import pylibmc as memcache
|
||||
is_cmemcache = False
|
||||
is_pylibmc = True
|
||||
except ImportError:
|
||||
raise RuntimeError('no memcache module found')
|
||||
|
||||
# cmemcache has a bug that debuglog is not defined for the
|
||||
# client. Whenever pickle fails you get a weird AttributeError.
|
||||
if is_cmemcache:
|
||||
# cmemcache has a bug that debuglog is not defined for the
|
||||
# client. Whenever pickle fails you get a weird
|
||||
# AttributeError.
|
||||
client = memcache.Client(map(str, servers))
|
||||
try:
|
||||
client.debuglog = lambda *a: None
|
||||
except Exception:
|
||||
pass
|
||||
elif is_pylibmc or is_cmemcached:
|
||||
client = memcache.Client(servers, False)
|
||||
else:
|
||||
if is_pylibmc:
|
||||
client = memcache.Client(servers, False)
|
||||
else:
|
||||
client = memcache.Client(servers, False, HIGHEST_PROTOCOL)
|
||||
client = memcache.Client(servers, False, HIGHEST_PROTOCOL)
|
||||
else:
|
||||
client = servers
|
||||
|
||||
@@ -378,7 +396,7 @@ class MemcachedCache(BaseCache):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
new_mapping = {}
|
||||
for key, value in mapping.iteritems():
|
||||
for key, value in _items(mapping):
|
||||
if isinstance(key, unicode):
|
||||
key = key.encode('utf-8')
|
||||
if self.key_prefix:
|
||||
@@ -441,6 +459,77 @@ class GAEMemcachedCache(MemcachedCache):
|
||||
default_timeout, key_prefix)
|
||||
|
||||
|
||||
class RedisCache(BaseCache):
|
||||
"""Uses the Redis key-value store as a cache backend.
|
||||
|
||||
The first argument can be either a string denoting address of the Redis
|
||||
server or an object resembling an instance of a redis.Redis class.
|
||||
|
||||
Note: Python Redis API already takes care of encoding unicode strings on
|
||||
the fly.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
|
||||
:param host: address of the Redis server or an object which API is
|
||||
compatible with the official Python Redis client (redis-py).
|
||||
:param port: port number on which Redis server listens for connections
|
||||
:param default_timeout: the default timeout that is used if no timeout is
|
||||
specified on :meth:`~BaseCache.set`.
|
||||
|
||||
"""
|
||||
def __init__(self, host='localhost', port=6379, default_timeout=300):
|
||||
BaseCache.__init__(self, default_timeout)
|
||||
if isinstance(host, basestring):
|
||||
try:
|
||||
import redis
|
||||
except ImportError:
|
||||
raise RuntimeError('no redis module found')
|
||||
self._client = redis.Redis(host=host, port=port)
|
||||
else:
|
||||
self._client = host
|
||||
|
||||
def get(self, key):
|
||||
return self._client.get(key)
|
||||
|
||||
def get_many(self, *keys):
|
||||
return self._client.mget(keys)
|
||||
|
||||
def set(self, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
self._client.setex(key, value, timeout)
|
||||
|
||||
def add(self, key, value, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
added = self._client.setnx(key, value)
|
||||
if added:
|
||||
self._client.expire(key, timeout)
|
||||
|
||||
def set_many(self, mapping, timeout=None):
|
||||
if timeout is None:
|
||||
timeout = self.default_timeout
|
||||
pipe = self._client.pipeline()
|
||||
for key, value in _items(mapping):
|
||||
pipe.setex(key, value, timeout)
|
||||
pipe.execute()
|
||||
|
||||
def delete(self, key):
|
||||
self._client.delete(key)
|
||||
|
||||
def delete_many(self, *keys):
|
||||
self._client.delete(*keys)
|
||||
|
||||
def clear(self):
|
||||
self._client.flushdb()
|
||||
|
||||
def inc(self, key, delta=1):
|
||||
return self._client.incr(key, delta)
|
||||
|
||||
def dec(self, key, delta=1):
|
||||
return self._client.decr(key, delta)
|
||||
|
||||
|
||||
class FileSystemCache(BaseCache):
|
||||
"""A cache that stores the items on the file system. This cache depends
|
||||
on being the only user of the `cache_dir`. Make absolutely sure that
|
||||
|
||||
@@ -17,9 +17,8 @@
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from urllib import unquote
|
||||
from werkzeug.wrappers import BaseRequest
|
||||
from werkzeug.http import parse_options_header, parse_cache_control_header, \
|
||||
parse_set_header, dump_header
|
||||
parse_set_header
|
||||
from werkzeug.useragents import UserAgent
|
||||
from werkzeug.datastructures import Headers, ResponseCacheControl
|
||||
|
||||
@@ -35,8 +34,13 @@ class LighttpdCGIRootFix(object):
|
||||
self.app = app
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
#only set PATH_INFO for older versions of Lighty:
|
||||
if environ['SERVER_SOFTWARE'] < 'lighttpd/1.4.28':
|
||||
# only set PATH_INFO for older versions of Lighty or if no
|
||||
# server software is provided. That's because the test was
|
||||
# added in newer Werkzeug versions and we don't want to break
|
||||
# people's code if they are using this fixer in a test that
|
||||
# does not set the SERVER_SOFTWARE key.
|
||||
if 'SERVER_SOFTWARE' not in environ or \
|
||||
environ['SERVER_SOFTWARE'] < 'lighttpd/1.4.28':
|
||||
environ['PATH_INFO'] = environ.get('SCRIPT_NAME', '') + \
|
||||
environ.get('PATH_INFO', '')
|
||||
environ['SCRIPT_NAME'] = ''
|
||||
@@ -79,9 +83,6 @@ class ProxyFix(object):
|
||||
application that was not designed with HTTP proxies in mind. It
|
||||
sets `REMOTE_ADDR`, `HTTP_HOST` from `X-Forwarded` headers.
|
||||
|
||||
Werkzeug wrappers have builtin support for this by setting the
|
||||
:attr:`~BaseRequest.is_behind_proxy` attribute to `True`.
|
||||
|
||||
Do not use this middleware in non-proxy setups for security reasons.
|
||||
|
||||
The original values of `REMOTE_ADDR` and `HTTP_HOST` are stored in
|
||||
|
||||
@@ -36,11 +36,11 @@ r"""
|
||||
|
||||
.. _greenlet: http://codespeak.net/py/dist/greenlet.html
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
try:
|
||||
from py.magic import greenlet
|
||||
import greenlet
|
||||
except ImportError:
|
||||
greenlet = None
|
||||
|
||||
|
||||
@@ -6,29 +6,31 @@
|
||||
Addon module that allows to create a JavaScript function from a map
|
||||
that generates rules.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
try:
|
||||
from simplejson import dumps
|
||||
except ImportError:
|
||||
def dumps(*args):
|
||||
raise RuntimeError('simplejson required for jsrouting')
|
||||
try:
|
||||
from json import dumps
|
||||
except ImportError:
|
||||
def dumps(*args):
|
||||
raise RuntimeError('simplejson required for jsrouting')
|
||||
|
||||
from inspect import getmro
|
||||
from werkzeug.templates import Template
|
||||
from werkzeug.routing import NumberConverter
|
||||
|
||||
|
||||
_javascript_routing_template = Template(u'''\
|
||||
<% if name_parts %>\
|
||||
<% for idx in xrange(0, len(name_parts) - 1) %>\
|
||||
if (typeof ${'.'.join(name_parts[:idx + 1])} === 'undefined') \
|
||||
${'.'.join(name_parts[:idx + 1])} = {};
|
||||
<% endfor %>\
|
||||
${'.'.join(name_parts)} = <% endif %>\
|
||||
(function (server_name, script_name, subdomain, url_scheme) {
|
||||
var converters = ${', '.join(converters)};
|
||||
def render_template(name_parts, rules, converters):
|
||||
result = u''
|
||||
if name_parts:
|
||||
for idx in xrange(0, len(name_parts) - 1):
|
||||
name = u'.'.join(name_parts[:idx + 1])
|
||||
result += u"if (typeof %s === 'undefined') %s = {}\n" % (name, name)
|
||||
result += '%s = ' % '.'.join(name_parts)
|
||||
result += """(function (server_name, script_name, subdomain, url_scheme) {
|
||||
var converters = %(converters)s;
|
||||
var rules = $rules;
|
||||
function in_array(array, value) {
|
||||
if (array.indexOf != undefined) {
|
||||
@@ -160,7 +162,8 @@ ${'.'.join(name_parts)} = <% endif %>\
|
||||
+ '/' + lstrip(rv.path, '/');
|
||||
}
|
||||
};
|
||||
})''')
|
||||
})""" % {'converters': u', '.join(converters)}
|
||||
return result
|
||||
|
||||
|
||||
def generate_map(map, name='url_map'):
|
||||
@@ -203,11 +206,9 @@ def generate_map(map, name='url_map'):
|
||||
u'defaults': rule.defaults
|
||||
})
|
||||
|
||||
return _javascript_routing_template.render({
|
||||
'name_parts': name and name.split('.') or [],
|
||||
'rules': dumps(rules),
|
||||
'converters': converters
|
||||
})
|
||||
return render_template(name_parts=name and name.split('.') or [],
|
||||
rules=dumps(rules),
|
||||
converters=converters)
|
||||
|
||||
|
||||
def generate_adapter(adapter, name='url_for', map_name='url_map'):
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
.. _Genshi: http://genshi.edgewall.org/
|
||||
.. _Django: http://www.djangoproject.com/
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from os import path
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
.. _Trac: http://trac.edgewall.org/
|
||||
.. _Django: http://www.djangoproject.com/
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from warnings import warn
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
It's strongly recommended to use it during development.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from urlparse import urlparse
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
from werkzeug.contrib.profiler import ProfilerMiddleware
|
||||
app = ProfilerMiddleware(app)
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
|
||||
@@ -85,17 +85,17 @@ r"""
|
||||
request.client_session.save_cookie(response)
|
||||
return response(environ, start_response)
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
import cPickle as pickle
|
||||
from hmac import new as hmac
|
||||
from itertools import izip
|
||||
from time import time
|
||||
from werkzeug.urls import url_quote_plus, url_unquote_plus
|
||||
from werkzeug._internal import _date_to_unix
|
||||
from werkzeug.contrib.sessions import ModificationTrackingDict
|
||||
from werkzeug.security import safe_str_cmp
|
||||
|
||||
|
||||
# rather ugly way to import the correct hash method. Because
|
||||
@@ -115,18 +115,6 @@ if _default_hash is None:
|
||||
import sha as _default_hash
|
||||
|
||||
|
||||
def safe_str_cmp(a, b):
|
||||
"""This function compares strings in somewhat constant time. In case
|
||||
someone actually finds a way to measure that over the network which
|
||||
I strongly doubt."""
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
rv = 0
|
||||
for x, y in izip(a, b):
|
||||
rv |= ord(x) ^ ord(y)
|
||||
return rv == 0
|
||||
|
||||
|
||||
class UnquoteError(Exception):
|
||||
"""Internal exception used to signal failures on quoting."""
|
||||
|
||||
@@ -261,7 +249,7 @@ class SecureCookie(ModificationTrackingDict):
|
||||
:return: a new :class:`SecureCookie`.
|
||||
"""
|
||||
if isinstance(string, unicode):
|
||||
string = string.encode('utf-8', 'ignore')
|
||||
string = string.encode('utf-8', 'replace')
|
||||
try:
|
||||
base64_hash, data = string.split('?', 1)
|
||||
except (ValueError, IndexError):
|
||||
|
||||
@@ -48,7 +48,7 @@ r"""
|
||||
response.set_cookie('cookie_name', request.session.sid)
|
||||
return response(environ, start_response)
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
A response wrapper which adds various cached attributes for
|
||||
simplified assertions on various content types.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
from werkzeug.utils import cached_property, import_string
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
Afterwards this request object provides the extra functionality of the
|
||||
:class:`JSONRequestMixin`.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import codecs
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This module provides mixins and classes with an immutable interface.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
@@ -291,11 +291,6 @@ class MultiDict(TypeConversionDict):
|
||||
or `None`.
|
||||
"""
|
||||
|
||||
# the key error this class raises. Because of circular dependencies
|
||||
# with the http exception module this class is created at the end of
|
||||
# this module.
|
||||
KeyError = None
|
||||
|
||||
def __init__(self, mapping=None):
|
||||
if isinstance(mapping, MultiDict):
|
||||
dict.__init__(self, ((k, l[:]) for k, l in mapping.iterlists()))
|
||||
@@ -333,7 +328,7 @@ class MultiDict(TypeConversionDict):
|
||||
"""
|
||||
if key in self:
|
||||
return dict.__getitem__(self, key)[0]
|
||||
raise self.KeyError(key)
|
||||
raise BadRequestKeyError(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Like :meth:`add` but removes an existing key first.
|
||||
@@ -539,7 +534,7 @@ class MultiDict(TypeConversionDict):
|
||||
except KeyError, e:
|
||||
if default is not _missing:
|
||||
return default
|
||||
raise self.KeyError(str(e))
|
||||
raise BadRequestKeyError(str(e))
|
||||
|
||||
def popitem(self):
|
||||
"""Pop an item from the dict."""
|
||||
@@ -547,7 +542,7 @@ class MultiDict(TypeConversionDict):
|
||||
item = dict.popitem(self)
|
||||
return (item[0], item[1][0])
|
||||
except KeyError, e:
|
||||
raise self.KeyError(str(e))
|
||||
raise BadRequestKeyError(str(e))
|
||||
|
||||
def poplist(self, key):
|
||||
"""Pop the list for a key from the dict. If the key is not in the dict
|
||||
@@ -564,7 +559,7 @@ class MultiDict(TypeConversionDict):
|
||||
try:
|
||||
return dict.popitem(self)
|
||||
except KeyError, e:
|
||||
raise self.KeyError(str(e))
|
||||
raise BadRequestKeyError(str(e))
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__class__.__name__, self.items(multi=True))
|
||||
@@ -617,11 +612,6 @@ class OrderedMultiDict(MultiDict):
|
||||
the internal bucket objects are exposed.
|
||||
"""
|
||||
|
||||
# the key error this class raises. Because of circular dependencies
|
||||
# with the http exception module this class is created at the end of
|
||||
# this module.
|
||||
KeyError = None
|
||||
|
||||
def __init__(self, mapping=None):
|
||||
dict.__init__(self)
|
||||
self._first_bucket = self._last_bucket = None
|
||||
@@ -670,7 +660,7 @@ class OrderedMultiDict(MultiDict):
|
||||
def __getitem__(self, key):
|
||||
if key in self:
|
||||
return dict.__getitem__(self, key)[0].value
|
||||
raise self.KeyError(key)
|
||||
raise BadRequestKeyError(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.poplist(key)
|
||||
@@ -755,7 +745,7 @@ class OrderedMultiDict(MultiDict):
|
||||
except KeyError, e:
|
||||
if default is not _missing:
|
||||
return default
|
||||
raise self.KeyError(str(e))
|
||||
raise BadRequestKeyError(str(e))
|
||||
for bucket in buckets:
|
||||
bucket.unlink(self)
|
||||
return buckets[0].value
|
||||
@@ -764,7 +754,7 @@ class OrderedMultiDict(MultiDict):
|
||||
try:
|
||||
key, buckets = dict.popitem(self)
|
||||
except KeyError, e:
|
||||
raise self.KeyError(str(e))
|
||||
raise BadRequestKeyError(str(e))
|
||||
for bucket in buckets:
|
||||
bucket.unlink(self)
|
||||
return key, buckets[0].value
|
||||
@@ -773,7 +763,7 @@ class OrderedMultiDict(MultiDict):
|
||||
try:
|
||||
key, buckets = dict.popitem(self)
|
||||
except KeyError, e:
|
||||
raise self.KeyError(str(e))
|
||||
raise BadRequestKeyError(str(e))
|
||||
for bucket in buckets:
|
||||
bucket.unlink(self)
|
||||
return key, [x.value for x in buckets]
|
||||
@@ -810,11 +800,6 @@ class Headers(object):
|
||||
:param defaults: The list of default values for the :class:`Headers`.
|
||||
"""
|
||||
|
||||
# the key error this class raises. Because of circular dependencies
|
||||
# with the http exception module this class is created at the end of
|
||||
# this module.
|
||||
KeyError = None
|
||||
|
||||
def __init__(self, defaults=None, _list=None):
|
||||
if _list is None:
|
||||
_list = []
|
||||
@@ -856,7 +841,7 @@ class Headers(object):
|
||||
# key error instead of our special one.
|
||||
if _get_mode:
|
||||
raise KeyError()
|
||||
raise self.KeyError(key)
|
||||
raise BadRequestKeyError(key)
|
||||
|
||||
def __eq__(self, other):
|
||||
return other.__class__ is self.__class__ and \
|
||||
@@ -1267,7 +1252,7 @@ class CombinedMultiDict(ImmutableMultiDictMixin, MultiDict):
|
||||
for d in self.dicts:
|
||||
if key in d:
|
||||
return d[key]
|
||||
raise self.KeyError(key)
|
||||
raise BadRequestKeyError(key)
|
||||
|
||||
def get(self, key, default=None, type=None):
|
||||
for d in self.dicts:
|
||||
@@ -1385,16 +1370,18 @@ class FileMultiDict(MultiDict):
|
||||
:param content_type: an optional content type
|
||||
"""
|
||||
if isinstance(file, FileStorage):
|
||||
self[name] = file
|
||||
return
|
||||
if isinstance(file, basestring):
|
||||
if filename is None:
|
||||
filename = file
|
||||
file = open(file, 'rb')
|
||||
if filename and content_type is None:
|
||||
content_type = mimetypes.guess_type(filename)[0] or \
|
||||
'application/octet-stream'
|
||||
self[name] = FileStorage(file, filename, name, content_type)
|
||||
value = file
|
||||
else:
|
||||
if isinstance(file, basestring):
|
||||
if filename is None:
|
||||
filename = file
|
||||
file = open(file, 'rb')
|
||||
if filename and content_type is None:
|
||||
content_type = mimetypes.guess_type(filename)[0] or \
|
||||
'application/octet-stream'
|
||||
value = FileStorage(file, filename, name, content_type)
|
||||
|
||||
self.add(name, value)
|
||||
|
||||
|
||||
class ImmutableDict(ImmutableDictMixin, dict):
|
||||
@@ -1651,6 +1638,11 @@ class MIMEAccept(Accept):
|
||||
'application/xml' in self
|
||||
)
|
||||
|
||||
@property
|
||||
def accept_json(self):
|
||||
"""True if this object accepts JSON."""
|
||||
return 'application/json' in self
|
||||
|
||||
|
||||
class LanguageAccept(Accept):
|
||||
"""Like :class:`Accept` but with normalization for languages."""
|
||||
@@ -2068,6 +2060,165 @@ class ETags(object):
|
||||
return '<%s %r>' % (self.__class__.__name__, str(self))
|
||||
|
||||
|
||||
class IfRange(object):
|
||||
"""Very simple object that represents the `If-Range` header in parsed
|
||||
form. It will either have neither a etag or date or one of either but
|
||||
never both.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
|
||||
def __init__(self, etag=None, date=None):
|
||||
#: The etag parsed and unquoted. Ranges always operate on strong
|
||||
#: etags so the weakness information is not necessary.
|
||||
self.etag = etag
|
||||
#: The date in parsed format or `None`.
|
||||
self.date = date
|
||||
|
||||
def to_header(self):
|
||||
"""Converts the object back into an HTTP header."""
|
||||
if self.date is not None:
|
||||
return http_date(self.date)
|
||||
if self.etag is not None:
|
||||
return quote_etag(self.etag)
|
||||
return ''
|
||||
|
||||
def __str__(self):
|
||||
return self.to_header()
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %r>' % (self.__class__.__name__, str(self))
|
||||
|
||||
|
||||
class Range(object):
|
||||
"""Represents a range header. All the methods are only supporting bytes
|
||||
as unit. It does store multiple ranges but :meth:`range_for_length` will
|
||||
only work if only one range is provided.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
|
||||
def __init__(self, units, ranges):
|
||||
#: The units of this range. Usually "bytes".
|
||||
self.units = units
|
||||
#: A list of ``(begin, end)`` tuples for the range header provided.
|
||||
#: The ranges are non-inclusive.
|
||||
self.ranges = ranges
|
||||
|
||||
def range_for_length(self, length):
|
||||
"""If the range is for bytes, the length is not None and there is
|
||||
exactly one range and it is satisfiable it returns a ``(start, stop)``
|
||||
tuple, otherwise `None`.
|
||||
"""
|
||||
if self.units != 'bytes' or length is None or len(self.ranges) != 1:
|
||||
return None
|
||||
start, end = self.ranges[0]
|
||||
if end is None:
|
||||
end = length
|
||||
if start < 0:
|
||||
start += length
|
||||
if is_byte_range_valid(start, end, length):
|
||||
return start, min(end, length)
|
||||
|
||||
def make_content_range(self, length):
|
||||
"""Creates a :class:`~werkzeug.datastructures.ContentRange` object
|
||||
from the current range and given content length.
|
||||
"""
|
||||
rng = self.range_for_length(length)
|
||||
if rng is not None:
|
||||
return ContentRange(self.units, rng[0], rng[1], length)
|
||||
|
||||
def to_header(self):
|
||||
"""Converts the object back into an HTTP header."""
|
||||
ranges = []
|
||||
for begin, end in self.ranges:
|
||||
if end is None:
|
||||
ranges.append(begin >= 0 and '%s-' % begin or str(begin))
|
||||
else:
|
||||
ranges.append('%s-%s' % (begin, end - 1))
|
||||
return '%s=%s' % (self.units, ','.join(ranges))
|
||||
|
||||
def __str__(self):
|
||||
return self.to_header()
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %r>' % (self.__class__.__name__, str(self))
|
||||
|
||||
|
||||
class ContentRange(object):
|
||||
"""Represents the content range header.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
|
||||
def __init__(self, units, start, stop, length=None, on_update=None):
|
||||
assert is_byte_range_valid(start, stop, length), \
|
||||
'Bad range provided'
|
||||
self.on_update = on_update
|
||||
self.set(start, stop, length, units)
|
||||
|
||||
def _callback_property(name):
|
||||
def fget(self):
|
||||
return getattr(self, name)
|
||||
def fset(self, value):
|
||||
setattr(self, name, value)
|
||||
if self.on_update is not None:
|
||||
self.on_update(self)
|
||||
return property(fget, fset)
|
||||
|
||||
#: The units to use, usually "bytes"
|
||||
units = _callback_property('_units')
|
||||
#: The start point of the range or `None`.
|
||||
start = _callback_property('_start')
|
||||
#: The stop point of the range (non-inclusive) or `None`. Can only be
|
||||
#: `None` if also start is `None`.
|
||||
stop = _callback_property('_stop')
|
||||
#: The length of the range or `None`.
|
||||
length = _callback_property('_length')
|
||||
|
||||
def set(self, start, stop, length=None, units='bytes'):
|
||||
"""Simple method to update the ranges."""
|
||||
assert is_byte_range_valid(start, stop, length), \
|
||||
'Bad range provided'
|
||||
self._units = units
|
||||
self._start = start
|
||||
self._stop = stop
|
||||
self._length = length
|
||||
if self.on_update is not None:
|
||||
self.on_update(self)
|
||||
|
||||
def unset(self):
|
||||
"""Sets the units to `None` which indicates that the header should
|
||||
no longer be used.
|
||||
"""
|
||||
self.set(None, None, units=None)
|
||||
|
||||
def to_header(self):
|
||||
if self.units is None:
|
||||
return ''
|
||||
if self.length is None:
|
||||
length = '*'
|
||||
else:
|
||||
length = self.length
|
||||
if self.start is None:
|
||||
return '%s */%s' % (self.units, length)
|
||||
return '%s %s-%s/%s' % (
|
||||
self.units,
|
||||
self.start,
|
||||
self.stop - 1,
|
||||
length
|
||||
)
|
||||
|
||||
def __nonzero__(self):
|
||||
return self.units is not None
|
||||
|
||||
def __str__(self):
|
||||
return self.to_header()
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s %r>' % (self.__class__.__name__, str(self))
|
||||
|
||||
|
||||
class Authorization(ImmutableDictMixin, dict):
|
||||
"""Represents an `Authorization` header sent by the client. You should
|
||||
not create this kind of object yourself but use it when it's returned by
|
||||
@@ -2275,7 +2426,17 @@ class FileStorage(object):
|
||||
headers=None):
|
||||
self.name = name
|
||||
self.stream = stream or _empty_stream
|
||||
self.filename = filename or getattr(stream, 'name', None)
|
||||
|
||||
# if no filename is provided we can attempt to get the filename
|
||||
# from the stream object passed. There we have to be careful to
|
||||
# skip things like <fdopen>, <stderr> etc. Python marks these
|
||||
# special filenames with angular brackets.
|
||||
if filename is None:
|
||||
filename = getattr(stream, 'name', None)
|
||||
if filename and filename[0] == '<' and filename[-1] == '>':
|
||||
filename = None
|
||||
|
||||
self.filename = filename
|
||||
self.content_type = content_type
|
||||
self.content_length = content_length
|
||||
if headers is None:
|
||||
@@ -2361,13 +2522,6 @@ class FileStorage(object):
|
||||
|
||||
# circular dependencies
|
||||
from werkzeug.http import dump_options_header, dump_header, generate_etag, \
|
||||
quote_header_value, parse_set_header, unquote_etag, \
|
||||
parse_options_header
|
||||
|
||||
|
||||
# create all the special key errors now that the classes are defined.
|
||||
from werkzeug.exceptions import BadRequest
|
||||
for _cls in MultiDict, OrderedMultiDict, CombinedMultiDict, Headers, \
|
||||
EnvironHeaders:
|
||||
_cls.KeyError = BadRequest.wrap(KeyError, _cls.__name__ + '.KeyError')
|
||||
del _cls
|
||||
quote_header_value, parse_set_header, unquote_etag, quote_etag, \
|
||||
parse_options_header, http_date, is_byte_range_valid
|
||||
from werkzeug.exceptions import BadRequestKeyError
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
WSGI application traceback debugger.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import mimetypes
|
||||
@@ -13,6 +13,7 @@ from os.path import join, dirname, basename, isfile
|
||||
from werkzeug.wrappers import BaseRequest as Request, BaseResponse as Response
|
||||
from werkzeug.debug.tbtools import get_current_traceback, render_console_html
|
||||
from werkzeug.debug.console import Console
|
||||
from werkzeug.security import gen_salt
|
||||
|
||||
|
||||
#: import this here because it once was documented as being available
|
||||
@@ -77,7 +78,8 @@ class DebuggedApplication(object):
|
||||
self.console_path = console_path
|
||||
self.console_init_func = console_init_func
|
||||
self.show_hidden_frames = show_hidden_frames
|
||||
self.lodgeit_url=lodgeit_url
|
||||
self.lodgeit_url = lodgeit_url
|
||||
self.secret = gen_salt(20)
|
||||
|
||||
def debug_application(self, environ, start_response):
|
||||
"""Run the application and conserve the traceback frames."""
|
||||
@@ -113,7 +115,8 @@ class DebuggedApplication(object):
|
||||
'sent.\n')
|
||||
else:
|
||||
yield traceback.render_full(evalex=self.evalex,
|
||||
lodgeit_url=self.lodgeit_url) \
|
||||
lodgeit_url=self.lodgeit_url,
|
||||
secret=self.secret) \
|
||||
.encode('utf-8', 'replace')
|
||||
|
||||
traceback.log(environ['wsgi.errors'])
|
||||
@@ -126,12 +129,13 @@ class DebuggedApplication(object):
|
||||
"""Display a standalone shell."""
|
||||
if 0 not in self.frames:
|
||||
self.frames[0] = _ConsoleFrame(self.console_init_func())
|
||||
return Response(render_console_html(), mimetype='text/html')
|
||||
return Response(render_console_html(secret=self.secret),
|
||||
mimetype='text/html')
|
||||
|
||||
def paste_traceback(self, request, traceback):
|
||||
"""Paste the traceback and return a JSON response."""
|
||||
paste_id = traceback.paste(self.lodgeit_url)
|
||||
return Response('{"url": "%sshow/%s/", "id": %s}'
|
||||
return Response('{"url": "%sshow/%s/", "id": "%s"}'
|
||||
% (self.lodgeit_url, paste_id, paste_id),
|
||||
mimetype='application/json')
|
||||
|
||||
@@ -162,15 +166,18 @@ class DebuggedApplication(object):
|
||||
if request.args.get('__debugger__') == 'yes':
|
||||
cmd = request.args.get('cmd')
|
||||
arg = request.args.get('f')
|
||||
secret = request.args.get('s')
|
||||
traceback = self.tracebacks.get(request.args.get('tb', type=int))
|
||||
frame = self.frames.get(request.args.get('frm', type=int))
|
||||
if cmd == 'resource' and arg:
|
||||
response = self.get_resource(request, arg)
|
||||
elif cmd == 'paste' and traceback is not None:
|
||||
elif cmd == 'paste' and traceback is not None and \
|
||||
secret == self.secret:
|
||||
response = self.paste_traceback(request, traceback)
|
||||
elif cmd == 'source' and frame:
|
||||
elif cmd == 'source' and frame and self.secret == secret:
|
||||
response = self.get_source(request, frame)
|
||||
elif self.evalex and cmd is not None and frame is not None:
|
||||
elif self.evalex and cmd is not None and frame is not None and \
|
||||
self.secret == secret:
|
||||
response = self.execute_command(request, cmd, frame)
|
||||
elif self.evalex and self.console_path is not None and \
|
||||
request.path == self.console_path:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
Interactive console support.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD.
|
||||
"""
|
||||
import sys
|
||||
@@ -35,10 +35,14 @@ class HTMLStringO(object):
|
||||
pass
|
||||
|
||||
def seek(self, n, mode=0):
|
||||
raise IOError('Bad file descriptor')
|
||||
pass
|
||||
|
||||
def readline(self):
|
||||
raise IOError('Bad file descriptor')
|
||||
if len(self._buffer) == 0:
|
||||
return ''
|
||||
ret = self._buffer[0]
|
||||
del self._buffer[0]
|
||||
return ret
|
||||
|
||||
def reset(self):
|
||||
val = ''.join(self._buffer)
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.debug.render
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Render the traceback debugging page.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import pprint
|
||||
from os.path import dirname, join
|
||||
|
||||
from werkzeug.templates import Template
|
||||
|
||||
|
||||
def get_template(name):
|
||||
return Template.from_file(join(dirname(__file__), 'shared', name),
|
||||
unicode_mode=False, errors='ignore')
|
||||
|
||||
|
||||
def load_resource(res):
|
||||
try:
|
||||
f = file(join(dirname(__file__), 'shared', res))
|
||||
except IOError:
|
||||
return ''
|
||||
try:
|
||||
return f.read()
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
|
||||
t_body = get_template('body.tmpl')
|
||||
t_codetable = get_template('codetable.tmpl')
|
||||
t_vartable = get_template('vartable.tmpl')
|
||||
|
||||
|
||||
def code_table(frame):
|
||||
from werkzeug.debug.util import Namespace
|
||||
lines = []
|
||||
lineno = frame['context_lineno']
|
||||
if lineno is not None:
|
||||
lineno += 1
|
||||
for l in frame['pre_context']:
|
||||
lines.append(Namespace(mode='pre', lineno=lineno, code=l))
|
||||
lineno += 1
|
||||
lines.append(Namespace(mode='cur', lineno=lineno,
|
||||
code=frame['context_line']))
|
||||
lineno += 1
|
||||
for l in frame['post_context']:
|
||||
lines.append(Namespace(mode='post', lineno=lineno, code=l))
|
||||
lineno += 1
|
||||
else:
|
||||
lines.append(Namespace(mode='cur', lineno=1,
|
||||
code='Sourcecode not available'))
|
||||
|
||||
return t_codetable.render(lines=lines)
|
||||
|
||||
|
||||
def var_table(var):
|
||||
def safe_pformat(x):
|
||||
try:
|
||||
lines = pprint.pformat(x).splitlines()
|
||||
except Exception:
|
||||
return '?'
|
||||
tmp = []
|
||||
for line in lines:
|
||||
if len(line) > 79:
|
||||
line = line[:79] + '...'
|
||||
tmp.append(line)
|
||||
return '\n'.join(tmp)
|
||||
|
||||
# dicts
|
||||
if isinstance(var, dict) or hasattr(var, 'items'):
|
||||
value = var.items()
|
||||
if not value:
|
||||
typ = 'empty'
|
||||
else:
|
||||
typ = 'dict'
|
||||
value.sort()
|
||||
value = [(repr(key), safe_pformat(val)) for key, val in value]
|
||||
|
||||
# lists
|
||||
elif isinstance(var, list):
|
||||
if not var:
|
||||
typ = 'empty'
|
||||
else:
|
||||
typ = 'list'
|
||||
value = [safe_pformat(item) for item in var]
|
||||
|
||||
# others
|
||||
else:
|
||||
typ = 'simple'
|
||||
value = repr(var)
|
||||
|
||||
return t_vartable.render(type=typ, value=value)
|
||||
|
||||
|
||||
def debug_page(context):
|
||||
tc = context.to_dict()
|
||||
tc['var_table'] = var_table
|
||||
tc['code_table'] = code_table
|
||||
return t_body.render(tc)
|
||||
@@ -10,7 +10,7 @@
|
||||
Together with the CSS and JavaScript files of the debugger this gives
|
||||
a colorful and more compact output.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD.
|
||||
"""
|
||||
import sys
|
||||
|
||||
|
Before Width: | Height: | Size: 507 B After Width: | Height: | Size: 507 B |
@@ -45,7 +45,7 @@ $(function() {
|
||||
sourceView.slideUp('fast');
|
||||
});
|
||||
$.get(document.location.pathname, {__debugger__: 'yes', cmd:
|
||||
'source', frm: frameID}, function(data) {
|
||||
'source', frm: frameID, s: SECRET}, function(data) {
|
||||
$('table', sourceView)
|
||||
.replaceWith(data);
|
||||
if (!sourceView.is(':visible'))
|
||||
@@ -96,7 +96,8 @@ $(function() {
|
||||
$.ajax({
|
||||
dataType: 'json',
|
||||
url: document.location.pathname,
|
||||
data: {__debugger__: 'yes', tb: TRACEBACK, cmd: 'paste'},
|
||||
data: {__debugger__: 'yes', tb: TRACEBACK, cmd: 'paste',
|
||||
s: SECRET},
|
||||
success: function(data) {
|
||||
$('div.plain span.pastemessage')
|
||||
.removeClass('pastemessage')
|
||||
@@ -134,7 +135,7 @@ function openShell(consoleNode, target, frameID) {
|
||||
.submit(function() {
|
||||
var cmd = command.val();
|
||||
$.get(document.location.pathname, {
|
||||
__debugger__: 'yes', cmd: cmd, frm: frameID}, function(data) {
|
||||
__debugger__: 'yes', cmd: cmd, frm: frameID, s: SECRET}, function(data) {
|
||||
var tmp = $('<div>').html(data);
|
||||
$('span.extended', tmp).each(function() {
|
||||
var hidden = $(this).wrap('<span>').hide();
|
||||
|
||||
|
Before Width: | Height: | Size: 191 B After Width: | Height: | Size: 191 B |
|
Before Width: | Height: | Size: 200 B After Width: | Height: | Size: 200 B |
|
Before Width: | Height: | Size: 818 B After Width: | Height: | Size: 818 B |
@@ -23,7 +23,7 @@ textarea { font-family: 'Consolas', 'Monaco', 'Bitstream Vera Sans Mono',
|
||||
div.debugger { text-align: left; padding: 12px; margin: auto;
|
||||
background-color: white; }
|
||||
h1 { font-size: 36px; margin: 0 0 0.3em 0; }
|
||||
div.detail p { margin: 0 0 8px 13px; font-size: 14px; }
|
||||
div.detail p { margin: 0 0 8px 13px; font-size: 14px; white-space: pre-wrap; }
|
||||
div.explanation { margin: 20px 13px; font-size: 15px; color: #555; }
|
||||
div.footer { font-size: 13px; text-align: right; margin: 30px 0;
|
||||
color: #86989B; }
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This module provides various traceback related utility functions.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD.
|
||||
"""
|
||||
import re
|
||||
@@ -42,7 +42,8 @@ HEADER = u'''\
|
||||
<script type="text/javascript">
|
||||
var TRACEBACK = %(traceback_id)d,
|
||||
CONSOLE_MODE = %(console)s,
|
||||
EVALEX = %(evalex)s
|
||||
EVALEX = %(evalex)s,
|
||||
SECRET = "%(secret)s";
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
@@ -127,11 +128,12 @@ SOURCE_LINE_HTML = u'''\
|
||||
'''
|
||||
|
||||
|
||||
def render_console_html():
|
||||
def render_console_html(secret):
|
||||
return CONSOLE_HTML % {
|
||||
'evalex': 'true',
|
||||
'console': 'true',
|
||||
'title': 'Console',
|
||||
'secret': secret,
|
||||
'traceback_id': -1
|
||||
}
|
||||
|
||||
@@ -261,7 +263,7 @@ class Traceback(object):
|
||||
"""Create a paste and return the paste id."""
|
||||
from xmlrpclib import ServerProxy
|
||||
srv = ServerProxy('%sxmlrpc/' % lodgeit_url)
|
||||
return srv.pastes.newPaste('pytb', self.plaintext)
|
||||
return srv.pastes.newPaste('pytb', self.plaintext, '', '', '', True)
|
||||
|
||||
def render_summary(self, include_title=True):
|
||||
"""Render the traceback for the interactive console."""
|
||||
@@ -296,7 +298,8 @@ class Traceback(object):
|
||||
'description': description_wrapper % escape(self.exception)
|
||||
}
|
||||
|
||||
def render_full(self, evalex=False, lodgeit_url=None):
|
||||
def render_full(self, evalex=False, lodgeit_url=None,
|
||||
secret=None):
|
||||
"""Render the Full HTML page with the traceback info."""
|
||||
exc = escape(self.exception)
|
||||
return PAGE_HTML % {
|
||||
@@ -309,7 +312,8 @@ class Traceback(object):
|
||||
'summary': self.render_summary(include_title=False),
|
||||
'plaintext': self.plaintext,
|
||||
'plaintext_cs': re.sub('-{2,}', '-', self.plaintext),
|
||||
'traceback_id': self.id
|
||||
'traceback_id': self.id,
|
||||
'secret': secret
|
||||
}
|
||||
|
||||
def generate_plaintext_traceback(self):
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
werkzeug.debug.utils
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Various other utilities.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD.
|
||||
"""
|
||||
from os.path import join, dirname
|
||||
from werkzeug.templates import Template
|
||||
|
||||
|
||||
def get_template(filename):
|
||||
return Template.from_file(join(dirname(__file__), 'templates', filename))
|
||||
|
||||
|
||||
def render_template(template_filename, **context):
|
||||
return get_template(template_filename).render(**context)
|
||||
@@ -54,7 +54,7 @@
|
||||
return e
|
||||
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
@@ -279,6 +279,21 @@ class RequestTimeout(HTTPException):
|
||||
)
|
||||
|
||||
|
||||
class Conflict(HTTPException):
|
||||
"""*409* `Conflict`
|
||||
|
||||
Raise to signal that a request cannot be completed because it conflicts
|
||||
with the current state on the server.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
code = 409
|
||||
description = (
|
||||
'<p>A conflict happened while processing the request. The resource '
|
||||
'might have been modified while the request was being processed.'
|
||||
)
|
||||
|
||||
|
||||
class Gone(HTTPException):
|
||||
"""*410* `Gone`
|
||||
|
||||
@@ -355,6 +370,47 @@ class UnsupportedMediaType(HTTPException):
|
||||
)
|
||||
|
||||
|
||||
class RequestedRangeNotSatisfiable(HTTPException):
|
||||
"""*416* `Requested Range Not Satisfiable`
|
||||
|
||||
The client asked for a part of the file that lies beyond the end
|
||||
of the file.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
code = 416
|
||||
description = (
|
||||
'<p>The server cannot provide the requested range.'
|
||||
)
|
||||
|
||||
|
||||
class ExpectationFailed(HTTPException):
|
||||
"""*417* `Expectation Failed`
|
||||
|
||||
The server cannot meet the requirements of the Expect request-header.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
code = 417
|
||||
description = (
|
||||
'<p>The server could not meet the requirements of the Expect header'
|
||||
)
|
||||
|
||||
|
||||
class ImATeapot(HTTPException):
|
||||
"""*418* `I'm a teapot`
|
||||
|
||||
The server should return this if it is a teapot and someone attempted
|
||||
to brew coffee with it.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
code = 418
|
||||
description = (
|
||||
'<p>This server is a teapot, not a coffee machine'
|
||||
)
|
||||
|
||||
|
||||
class InternalServerError(HTTPException):
|
||||
"""*500* `Internal Server Error`
|
||||
|
||||
@@ -456,5 +512,10 @@ class Aborter(object):
|
||||
abort = Aborter()
|
||||
|
||||
|
||||
#: an exception that is used internally to signal both a key error and a
|
||||
#: bad request. Used by a lot of the datastructures.
|
||||
BadRequestKeyError = BadRequest.wrap(KeyError)
|
||||
|
||||
|
||||
# imported here because of circular dependencies of werkzeug.utils
|
||||
from werkzeug.utils import escape
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
This module implements the form parsing. It supports url-encoded forms
|
||||
as well as non-nested multipart uploads.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
@@ -42,12 +42,12 @@ def default_stream_factory(total_content_length, filename, content_type,
|
||||
|
||||
|
||||
def parse_form_data(environ, stream_factory=None, charset='utf-8',
|
||||
errors='ignore', max_form_memory_size=None,
|
||||
errors='replace', max_form_memory_size=None,
|
||||
max_content_length=None, cls=None,
|
||||
silent=True):
|
||||
"""Parse the form data in the environ and return it as tuple in the form
|
||||
``(stream, form, files)``. You should only call this method if the
|
||||
transport method is `POST` or `PUT`.
|
||||
transport method is `POST`, `PUT`, or `PATCH`.
|
||||
|
||||
If the mimetype of the data transmitted is `multipart/form-data` the
|
||||
files multidict will be filled with `FileStorage` objects. If the
|
||||
@@ -169,7 +169,7 @@ def is_valid_multipart_boundary(boundary):
|
||||
|
||||
|
||||
def parse_multipart(file, boundary, content_length, stream_factory=None,
|
||||
charset='utf-8', errors='ignore', buffer_size=10 * 1024,
|
||||
charset='utf-8', errors='replace', buffer_size=10 * 1024,
|
||||
max_form_memory_size=None):
|
||||
"""Parse a multipart/form-data stream. This is invoked by
|
||||
:func:`utils.parse_form_data` if the content type matches. Currently it
|
||||
|
||||
@@ -13,11 +13,11 @@
|
||||
module.
|
||||
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
import inspect
|
||||
from time import time
|
||||
try:
|
||||
from email.utils import parsedate_tz
|
||||
except ImportError: # pragma: no cover
|
||||
@@ -32,7 +32,8 @@ except ImportError: # pragma: no cover
|
||||
|
||||
#: HTTP_STATUS_CODES is "exported" from this module.
|
||||
#: XXX: move to werkzeug.consts or something
|
||||
from werkzeug._internal import HTTP_STATUS_CODES, _dump_date
|
||||
from werkzeug._internal import HTTP_STATUS_CODES, _dump_date, \
|
||||
_ExtendedCookie, _ExtendedMorsel, _decode_unicode
|
||||
|
||||
|
||||
_accept_re = re.compile(r'([^\s;,]+)(?:[^,]*?;\s*q=(\d*(?:\.\d+)?))?')
|
||||
@@ -284,13 +285,14 @@ def parse_cache_control_header(value, on_update=None, cls=None):
|
||||
|
||||
.. versionadded:: 0.5
|
||||
The `cls` was added. If not specified an immutable
|
||||
:class:`RequestCacheControl` is returned.
|
||||
:class:`~werkzeug.datastructures.RequestCacheControl` is returned.
|
||||
|
||||
:param value: a cache control header to be parsed.
|
||||
:param on_update: an optional callable that is called every time a
|
||||
value on the :class:`CacheControl` object is changed.
|
||||
:param on_update: an optional callable that is called every time a value
|
||||
on the :class:`~werkzeug.datastructures.CacheControl`
|
||||
object is changed.
|
||||
:param cls: the class for the returned object. By default
|
||||
:class:`RequestCacheControl` is used.
|
||||
:class:`~werkzeug.datastructures.RequestCacheControl` is used.
|
||||
:return: a `cls` object.
|
||||
"""
|
||||
if cls is None:
|
||||
@@ -301,7 +303,8 @@ def parse_cache_control_header(value, on_update=None, cls=None):
|
||||
|
||||
|
||||
def parse_set_header(value, on_update=None):
|
||||
"""Parse a set-like header and return a :class:`HeaderSet` object:
|
||||
"""Parse a set-like header and return a
|
||||
:class:`~werkzeug.datastructures.HeaderSet` object:
|
||||
|
||||
>>> hs = parse_set_header('token, "quoted value"')
|
||||
|
||||
@@ -320,8 +323,9 @@ def parse_set_header(value, on_update=None):
|
||||
|
||||
:param value: a set header to be parsed.
|
||||
:param on_update: an optional callable that is called every time a
|
||||
value on the :class:`HeaderSet` object is changed.
|
||||
:return: a :class:`HeaderSet`
|
||||
value on the :class:`~werkzeug.datastructures.HeaderSet`
|
||||
object is changed.
|
||||
:return: a :class:`~werkzeug.datastructures.HeaderSet`
|
||||
"""
|
||||
if not value:
|
||||
return HeaderSet(None, on_update)
|
||||
@@ -331,10 +335,11 @@ def parse_set_header(value, on_update=None):
|
||||
def parse_authorization_header(value):
|
||||
"""Parse an HTTP basic/digest authorization header transmitted by the web
|
||||
browser. The return value is either `None` if the header was invalid or
|
||||
not given, otherwise an :class:`Authorization` object.
|
||||
not given, otherwise an :class:`~werkzeug.datastructures.Authorization`
|
||||
object.
|
||||
|
||||
:param value: the authorization header to parse.
|
||||
:return: a :class:`Authorization` object or `None`.
|
||||
:return: a :class:`~werkzeug.datastructures.Authorization` object or `None`.
|
||||
"""
|
||||
if not value:
|
||||
return
|
||||
@@ -360,13 +365,14 @@ def parse_authorization_header(value):
|
||||
|
||||
|
||||
def parse_www_authenticate_header(value, on_update=None):
|
||||
"""Parse an HTTP WWW-Authenticate header into a :class:`WWWAuthenticate`
|
||||
object.
|
||||
"""Parse an HTTP WWW-Authenticate header into a
|
||||
:class:`~werkzeug.datastructures.WWWAuthenticate` object.
|
||||
|
||||
:param value: a WWW-Authenticate header to parse.
|
||||
:param on_update: an optional callable that is called every time a
|
||||
value on the :class:`WWWAuthenticate` object is changed.
|
||||
:return: a :class:`WWWAuthenticate` object.
|
||||
:param on_update: an optional callable that is called every time a value
|
||||
on the :class:`~werkzeug.datastructures.WWWAuthenticate`
|
||||
object is changed.
|
||||
:return: a :class:`~werkzeug.datastructures.WWWAuthenticate` object.
|
||||
"""
|
||||
if not value:
|
||||
return WWWAuthenticate(on_update=on_update)
|
||||
@@ -379,6 +385,109 @@ def parse_www_authenticate_header(value, on_update=None):
|
||||
on_update)
|
||||
|
||||
|
||||
def parse_if_range_header(value):
|
||||
"""Parses an if-range header which can be an etag or a date. Returns
|
||||
a :class:`~werkzeug.datastructures.IfRange` object.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
if not value:
|
||||
return IfRange()
|
||||
date = parse_date(value)
|
||||
if date is not None:
|
||||
return IfRange(date=date)
|
||||
# drop weakness information
|
||||
return IfRange(unquote_etag(value)[0])
|
||||
|
||||
|
||||
def parse_range_header(value, make_inclusive=True):
|
||||
"""Parses a range header into a :class:`~werkzeug.datastructures.Range`
|
||||
object. If the header is missing or malformed `None` is returned.
|
||||
`ranges` is a list of ``(start, stop)`` tuples where the ranges are
|
||||
non-inclusive.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
if not value or '=' not in value:
|
||||
return None
|
||||
|
||||
ranges = []
|
||||
last_end = 0
|
||||
units, rng = value.split('=', 1)
|
||||
units = units.strip().lower()
|
||||
|
||||
for item in rng.split(','):
|
||||
item = item.strip()
|
||||
if '-' not in item:
|
||||
return None
|
||||
if item.startswith('-'):
|
||||
if last_end < 0:
|
||||
return None
|
||||
begin = int(item)
|
||||
end = None
|
||||
last_end = -1
|
||||
elif '-' in item:
|
||||
begin, end = item.split('-', 1)
|
||||
begin = int(begin)
|
||||
if begin < last_end or last_end < 0:
|
||||
return None
|
||||
if end:
|
||||
end = int(end) + 1
|
||||
if begin >= end:
|
||||
return None
|
||||
else:
|
||||
end = None
|
||||
last_end = end
|
||||
ranges.append((begin, end))
|
||||
|
||||
return Range(units, ranges)
|
||||
|
||||
|
||||
def parse_content_range_header(value, on_update=None):
|
||||
"""Parses a range header into a
|
||||
:class:`~werkzeug.datastructures.ContentRange` object or `None` if
|
||||
parsing is not possible.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
|
||||
:param value: a content range header to be parsed.
|
||||
:param on_update: an optional callable that is called every time a value
|
||||
on the :class:`~werkzeug.datastructures.ContentRange`
|
||||
object is changed.
|
||||
"""
|
||||
if value is None:
|
||||
return None
|
||||
try:
|
||||
units, rangedef = (value or '').strip().split(None, 1)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if '/' not in rangedef:
|
||||
return None
|
||||
rng, length = rangedef.split('/', 1)
|
||||
if length == '*':
|
||||
length = None
|
||||
elif length.isdigit():
|
||||
length = int(length)
|
||||
else:
|
||||
return None
|
||||
|
||||
if rng == '*':
|
||||
return ContentRange(units, None, None, length, on_update=on_update)
|
||||
elif '-' not in rng:
|
||||
return None
|
||||
|
||||
start, stop = rng.split('-', 1)
|
||||
try:
|
||||
start = int(start)
|
||||
stop = int(stop) + 1
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
if is_byte_range_valid(start, stop, length):
|
||||
return ContentRange(units, start, stop, length, on_update=on_update)
|
||||
|
||||
|
||||
def quote_etag(etag, weak=False):
|
||||
"""Quote an etag.
|
||||
|
||||
@@ -420,7 +529,7 @@ def parse_etags(value):
|
||||
"""Parse an etag header.
|
||||
|
||||
:param value: the tag header to parse
|
||||
:return: an :class:`ETags` object.
|
||||
:return: an :class:`~werkzeug.datastructures.ETags` object.
|
||||
"""
|
||||
if not value:
|
||||
return ETags()
|
||||
@@ -532,6 +641,12 @@ def is_resource_modified(environ, etag=None, data=None, last_modified=None):
|
||||
unmodified = False
|
||||
if isinstance(last_modified, basestring):
|
||||
last_modified = parse_date(last_modified)
|
||||
|
||||
# ensure that microsecond is zero because the HTTP spec does not transmit
|
||||
# that either and we might have some false positives. See issue #39
|
||||
if last_modified is not None:
|
||||
last_modified = last_modified.replace(microsecond=0)
|
||||
|
||||
modified_since = parse_date(environ.get('HTTP_IF_MODIFIED_SINCE'))
|
||||
|
||||
if modified_since and last_modified and last_modified <= modified_since:
|
||||
@@ -596,12 +711,129 @@ def is_hop_by_hop_header(header):
|
||||
return header.lower() in _hop_by_pop_headers
|
||||
|
||||
|
||||
def parse_cookie(header, charset='utf-8', errors='replace',
|
||||
cls=None):
|
||||
"""Parse a cookie. Either from a string or WSGI environ.
|
||||
|
||||
Per default encoding errors are ignored. If you want a different behavior
|
||||
you can set `errors` to ``'replace'`` or ``'strict'``. In strict mode a
|
||||
:exc:`HTTPUnicodeError` is raised.
|
||||
|
||||
.. versionchanged:: 0.5
|
||||
This function now returns a :class:`TypeConversionDict` instead of a
|
||||
regular dict. The `cls` parameter was added.
|
||||
|
||||
:param header: the header to be used to parse the cookie. Alternatively
|
||||
this can be a WSGI environment.
|
||||
:param charset: the charset for the cookie values.
|
||||
:param errors: the error behavior for the charset decoding.
|
||||
:param cls: an optional dict class to use. If this is not specified
|
||||
or `None` the default :class:`TypeConversionDict` is
|
||||
used.
|
||||
"""
|
||||
if isinstance(header, dict):
|
||||
header = header.get('HTTP_COOKIE', '')
|
||||
if cls is None:
|
||||
cls = TypeConversionDict
|
||||
cookie = _ExtendedCookie()
|
||||
cookie.load(header)
|
||||
result = {}
|
||||
|
||||
# decode to unicode and skip broken items. Our extended morsel
|
||||
# and extended cookie will catch CookieErrors and convert them to
|
||||
# `None` items which we have to skip here.
|
||||
for key, value in cookie.iteritems():
|
||||
if value.value is not None:
|
||||
result[key] = _decode_unicode(unquote_header_value(value.value),
|
||||
charset, errors)
|
||||
|
||||
return cls(result)
|
||||
|
||||
|
||||
def dump_cookie(key, value='', max_age=None, expires=None, path='/',
|
||||
domain=None, secure=None, httponly=False, charset='utf-8',
|
||||
sync_expires=True):
|
||||
"""Creates a new Set-Cookie header without the ``Set-Cookie`` prefix
|
||||
The parameters are the same as in the cookie Morsel object in the
|
||||
Python standard library but it accepts unicode data, too.
|
||||
|
||||
:param max_age: should be a number of seconds, or `None` (default) if
|
||||
the cookie should last only as long as the client's
|
||||
browser session. Additionally `timedelta` objects
|
||||
are accepted, too.
|
||||
:param expires: should be a `datetime` object or unix timestamp.
|
||||
:param path: limits the cookie to a given path, per default it will
|
||||
span the whole domain.
|
||||
:param domain: Use this if you want to set a cross-domain cookie. For
|
||||
example, ``domain=".example.com"`` will set a cookie
|
||||
that is readable by the domain ``www.example.com``,
|
||||
``foo.example.com`` etc. Otherwise, a cookie will only
|
||||
be readable by the domain that set it.
|
||||
:param secure: The cookie will only be available via HTTPS
|
||||
:param httponly: disallow JavaScript to access the cookie. This is an
|
||||
extension to the cookie standard and probably not
|
||||
supported by all browsers.
|
||||
:param charset: the encoding for unicode values.
|
||||
:param sync_expires: automatically set expires if max_age is defined
|
||||
but expires not.
|
||||
"""
|
||||
try:
|
||||
key = str(key)
|
||||
except UnicodeError:
|
||||
raise TypeError('invalid key %r' % key)
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode(charset)
|
||||
value = quote_header_value(value)
|
||||
morsel = _ExtendedMorsel(key, value)
|
||||
if isinstance(max_age, timedelta):
|
||||
max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds
|
||||
if expires is not None:
|
||||
if not isinstance(expires, basestring):
|
||||
expires = cookie_date(expires)
|
||||
morsel['expires'] = expires
|
||||
elif max_age is not None and sync_expires:
|
||||
morsel['expires'] = cookie_date(time() + max_age)
|
||||
if domain and ':' in domain:
|
||||
# The port part of the domain should NOT be used. Strip it
|
||||
domain = domain.split(':', 1)[0]
|
||||
if domain:
|
||||
assert '.' in domain, (
|
||||
"Setting \"domain\" for a cookie on a server running localy (ex: "
|
||||
"localhost) is not supportted by complying browsers. You should "
|
||||
"have something like: \"127.0.0.1 localhost dev.localhost\" on "
|
||||
"your hosts file and then point your server to run on "
|
||||
"\"dev.localhost\" and also set \"domain\" for \"dev.localhost\""
|
||||
)
|
||||
for k, v in (('path', path), ('domain', domain), ('secure', secure),
|
||||
('max-age', max_age), ('httponly', httponly)):
|
||||
if v is not None and v is not False:
|
||||
morsel[k] = str(v)
|
||||
return morsel.output(header='').lstrip()
|
||||
|
||||
|
||||
def is_byte_range_valid(start, stop, length):
|
||||
"""Checks if a given byte content range is valid for the given length.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
if (start is None) != (stop is None):
|
||||
return False
|
||||
elif start is None:
|
||||
return length is None or length >= 0
|
||||
elif length is None:
|
||||
return 0 <= start < stop
|
||||
elif start >= stop:
|
||||
return False
|
||||
return 0 <= start < length
|
||||
|
||||
|
||||
# circular dependency fun
|
||||
from werkzeug.datastructures import Headers, Accept, RequestCacheControl, \
|
||||
ResponseCacheControl, HeaderSet, ETags, Authorization, \
|
||||
WWWAuthenticate
|
||||
from werkzeug.datastructures import Accept, HeaderSet, ETags, Authorization, \
|
||||
WWWAuthenticate, TypeConversionDict, IfRange, Range, ContentRange, \
|
||||
RequestCacheControl
|
||||
|
||||
|
||||
# DEPRECATED
|
||||
# backwards compatible imports
|
||||
from werkzeug.datastructures import MIMEAccept, CharsetAccept, LanguageAccept
|
||||
from werkzeug.datastructures import MIMEAccept, CharsetAccept, \
|
||||
LanguageAccept, Headers
|
||||
|
||||
@@ -5,19 +5,13 @@
|
||||
|
||||
This module implements context-local objects.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
try:
|
||||
from greenlet import getcurrent as get_current_greenlet
|
||||
except ImportError: # pragma: no cover
|
||||
try:
|
||||
from py.magic import greenlet
|
||||
get_current_greenlet = greenlet.getcurrent
|
||||
del greenlet
|
||||
except Exception:
|
||||
# catch all, py.* fails with so many different errors.
|
||||
get_current_greenlet = int
|
||||
get_current_greenlet = int
|
||||
try:
|
||||
from thread import get_ident as get_current_thread, allocate_lock
|
||||
except ImportError: # pragma: no cover
|
||||
|
||||
@@ -14,7 +14,7 @@ r"""
|
||||
This module was introduced in 0.6.1 and is not a public interface.
|
||||
It might become one in later versions of Werkzeug.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
|
||||
@@ -92,15 +92,13 @@
|
||||
method is raised.
|
||||
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
Thomas Johansson.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
import posixpath
|
||||
from pprint import pformat
|
||||
from urlparse import urljoin
|
||||
from itertools import izip
|
||||
|
||||
from werkzeug.urls import url_encode, url_decode, url_quote
|
||||
from werkzeug.utils import redirect, format_string
|
||||
@@ -121,6 +119,56 @@ _rule_re = re.compile(r'''
|
||||
>
|
||||
''', re.VERBOSE)
|
||||
_simple_rule_re = re.compile(r'<([^>]+)>')
|
||||
_converter_args_re = re.compile(r'''
|
||||
((?P<name>\w+)\s*=\s*)?
|
||||
(?P<value>
|
||||
True|False|
|
||||
\d+.\d+|
|
||||
\d+.|
|
||||
\d+|
|
||||
\w+|
|
||||
[urUR]?(?P<stringval>"[^"]*?"|'[^']*')
|
||||
)\s*,
|
||||
''', re.VERBOSE|re.UNICODE)
|
||||
|
||||
|
||||
_PYTHON_CONSTANTS = {
|
||||
'None': None,
|
||||
'True': True,
|
||||
'False': False
|
||||
}
|
||||
|
||||
|
||||
def _pythonize(value):
|
||||
if value in _PYTHON_CONSTANTS:
|
||||
return _PYTHON_CONSTANTS[value]
|
||||
for convert in int, float:
|
||||
try:
|
||||
return convert(value)
|
||||
except ValueError:
|
||||
pass
|
||||
if value[:1] == value[-1:] and value[0] in '"\'':
|
||||
value = value[1:-1]
|
||||
return unicode(value)
|
||||
|
||||
|
||||
def parse_converter_args(argstr):
|
||||
argstr += ','
|
||||
args = []
|
||||
kwargs = {}
|
||||
|
||||
for item in _converter_args_re.finditer(argstr):
|
||||
value = item.group('stringval')
|
||||
if value is None:
|
||||
value = item.group('value')
|
||||
value = _pythonize(value)
|
||||
if not item.group('name'):
|
||||
args.append(value)
|
||||
else:
|
||||
name = item.group('name')
|
||||
kwargs[name] = value
|
||||
|
||||
return tuple(args), kwargs
|
||||
|
||||
|
||||
def parse_rule(rule):
|
||||
@@ -164,8 +212,7 @@ def get_converter(map, name, args):
|
||||
if not name in map.converters:
|
||||
raise LookupError('the converter %r does not exist' % name)
|
||||
if args:
|
||||
storage = type('_Storage', (), {'__getitem__': lambda s, x: x})()
|
||||
args, kwargs = eval(u'(lambda *a, **kw: (a, kw))(%s)' % args, {}, storage)
|
||||
args, kwargs = parse_converter_args(args)
|
||||
else:
|
||||
args = ()
|
||||
kwargs = {}
|
||||
@@ -200,6 +247,13 @@ class RequestSlash(RoutingException):
|
||||
"""Internal exception."""
|
||||
|
||||
|
||||
class RequestAliasRedirect(RoutingException):
|
||||
"""This rule is an alias and wants to redirect to the canonical URL."""
|
||||
|
||||
def __init__(self, matched_values):
|
||||
self.matched_values = matched_values
|
||||
|
||||
|
||||
class BuildError(RoutingException, LookupError):
|
||||
"""Raised if the build system cannot find a URL for an endpoint with the
|
||||
values provided.
|
||||
@@ -353,7 +407,7 @@ class RuleTemplateFactory(RuleFactory):
|
||||
for rulefactory in self.rules:
|
||||
for rule in rulefactory.get_rules(map):
|
||||
new_defaults = subdomain = None
|
||||
if rule.defaults is not None:
|
||||
if rule.defaults:
|
||||
new_defaults = {}
|
||||
for key, value in rule.defaults.iteritems():
|
||||
if isinstance(value, basestring):
|
||||
@@ -475,11 +529,23 @@ class Rule(RuleFactory):
|
||||
Keep in mind that the URL will be joined against the URL root of the
|
||||
script so don't use a leading slash on the target URL unless you
|
||||
really mean root of that domain.
|
||||
|
||||
`alias`
|
||||
If enabled this rule serves as an alias for another rule with the same
|
||||
endpoint and arguments.
|
||||
|
||||
`host`
|
||||
If provided and the URL map has host matching enabled this can be
|
||||
used to provide a match rule for the whole host. This also means
|
||||
that the subdomain feature is disabled.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
The `alias` and `host` parameters were added.
|
||||
"""
|
||||
|
||||
def __init__(self, string, defaults=None, subdomain=None, methods=None,
|
||||
build_only=False, endpoint=None, strict_slashes=None,
|
||||
redirect_to=None):
|
||||
redirect_to=None, alias=False, host=None):
|
||||
if not string.startswith('/'):
|
||||
raise ValueError('urls must start with a leading slash')
|
||||
self.rule = string
|
||||
@@ -488,8 +554,10 @@ class Rule(RuleFactory):
|
||||
self.map = None
|
||||
self.strict_slashes = strict_slashes
|
||||
self.subdomain = subdomain
|
||||
self.host = host
|
||||
self.defaults = defaults
|
||||
self.build_only = build_only
|
||||
self.alias = alias
|
||||
if methods is None:
|
||||
self.methods = None
|
||||
else:
|
||||
@@ -497,10 +565,9 @@ class Rule(RuleFactory):
|
||||
if 'HEAD' not in self.methods and 'GET' in self.methods:
|
||||
self.methods.add('HEAD')
|
||||
self.endpoint = endpoint
|
||||
self.greediness = 0
|
||||
self.redirect_to = redirect_to
|
||||
|
||||
if defaults is not None:
|
||||
if defaults:
|
||||
self.arguments = set(map(str, defaults))
|
||||
else:
|
||||
self.arguments = set()
|
||||
@@ -510,11 +577,11 @@ class Rule(RuleFactory):
|
||||
"""Return an unbound copy of this rule. This can be useful if you
|
||||
want to reuse an already bound URL for another map."""
|
||||
defaults = None
|
||||
if self.defaults is not None:
|
||||
if self.defaults:
|
||||
defaults = dict(self.defaults)
|
||||
return Rule(self.rule, defaults, self.subdomain, self.methods,
|
||||
self.build_only, self.endpoint, self.strict_slashes,
|
||||
self.redirect_to)
|
||||
self.redirect_to, self.alias, self.host)
|
||||
|
||||
def get_rules(self, map):
|
||||
yield self
|
||||
@@ -546,42 +613,54 @@ class Rule(RuleFactory):
|
||||
def compile(self):
|
||||
"""Compiles the regular expression and stores it."""
|
||||
assert self.map is not None, 'rule not bound'
|
||||
rule = self.subdomain + '|' + (self.is_leaf and self.rule
|
||||
or self.rule.rstrip('/'))
|
||||
|
||||
if self.map.host_matching:
|
||||
domain_rule = self.host or ''
|
||||
else:
|
||||
domain_rule = self.subdomain or ''
|
||||
|
||||
self._trace = []
|
||||
self._converters = {}
|
||||
self._weights = []
|
||||
|
||||
regex_parts = []
|
||||
for converter, arguments, variable in parse_rule(rule):
|
||||
if converter is None:
|
||||
regex_parts.append(re.escape(variable))
|
||||
self._trace.append((False, variable))
|
||||
self._weights.append(len(variable))
|
||||
else:
|
||||
convobj = get_converter(self.map, converter, arguments)
|
||||
regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex))
|
||||
self._converters[variable] = convobj
|
||||
self._trace.append((True, variable))
|
||||
self._weights.append(convobj.weight)
|
||||
self.arguments.add(str(variable))
|
||||
if convobj.is_greedy:
|
||||
self.greediness += 1
|
||||
|
||||
def _build_regex(rule):
|
||||
for converter, arguments, variable in parse_rule(rule):
|
||||
if converter is None:
|
||||
regex_parts.append(re.escape(variable))
|
||||
self._trace.append((False, variable))
|
||||
for part in variable.split('/'):
|
||||
if part:
|
||||
self._weights.append((0, -len(part)))
|
||||
else:
|
||||
convobj = get_converter(self.map, converter, arguments)
|
||||
regex_parts.append('(?P<%s>%s)' % (variable, convobj.regex))
|
||||
self._converters[variable] = convobj
|
||||
self._trace.append((True, variable))
|
||||
self._weights.append((1, convobj.weight))
|
||||
self.arguments.add(str(variable))
|
||||
|
||||
_build_regex(domain_rule)
|
||||
regex_parts.append('\\|')
|
||||
self._trace.append((False, '|'))
|
||||
_build_regex(self.is_leaf and self.rule or self.rule.rstrip('/'))
|
||||
if not self.is_leaf:
|
||||
self._trace.append((False, '/'))
|
||||
|
||||
if not self.build_only:
|
||||
regex = r'^%s%s$' % (
|
||||
u''.join(regex_parts),
|
||||
(not self.is_leaf or not self.strict_slashes) and \
|
||||
'(?<!/)(?P<__suffix__>/?)' or ''
|
||||
)
|
||||
self._regex = re.compile(regex, re.UNICODE)
|
||||
if self.build_only:
|
||||
return
|
||||
regex = r'^%s%s$' % (
|
||||
u''.join(regex_parts),
|
||||
(not self.is_leaf or not self.strict_slashes) and \
|
||||
'(?<!/)(?P<__suffix__>/?)' or ''
|
||||
)
|
||||
self._regex = re.compile(regex, re.UNICODE)
|
||||
|
||||
def match(self, path):
|
||||
"""Check if the rule matches a given path. Path is a string in the
|
||||
form ``"subdomain|/path(method)"`` and is assembled by the map.
|
||||
form ``"subdomain|/path(method)"`` and is assembled by the map. If
|
||||
the map is doing host matching the subdomain part will be the host
|
||||
instead.
|
||||
|
||||
If the rule matches a dict with the converted values is returned,
|
||||
otherwise the return value is `None`.
|
||||
@@ -611,8 +690,12 @@ class Rule(RuleFactory):
|
||||
except ValidationError:
|
||||
return
|
||||
result[str(name)] = value
|
||||
if self.defaults is not None:
|
||||
if self.defaults:
|
||||
result.update(self.defaults)
|
||||
|
||||
if self.alias and self.map.redirect_defaults:
|
||||
raise RequestAliasRedirect(result)
|
||||
|
||||
return result
|
||||
|
||||
def build(self, values, append_unknown=True):
|
||||
@@ -633,7 +716,7 @@ class Rule(RuleFactory):
|
||||
processed.add(data)
|
||||
else:
|
||||
add(data)
|
||||
subdomain, url = (u''.join(tmp)).split('|', 1)
|
||||
domain_part, url = (u''.join(tmp)).split('|', 1)
|
||||
|
||||
if append_unknown:
|
||||
query_vars = MultiDict(values)
|
||||
@@ -646,14 +729,14 @@ class Rule(RuleFactory):
|
||||
sort=self.map.sort_parameters,
|
||||
key=self.map.sort_key)
|
||||
|
||||
return subdomain, url
|
||||
return domain_part, url
|
||||
|
||||
def provides_defaults_for(self, rule):
|
||||
"""Check if this rule has defaults for a given rule.
|
||||
|
||||
:internal:
|
||||
"""
|
||||
return not self.build_only and self.defaults is not None and \
|
||||
return not self.build_only and self.defaults and \
|
||||
self.endpoint == rule.endpoint and self != rule and \
|
||||
self.arguments == rule.arguments
|
||||
|
||||
@@ -662,83 +745,52 @@ class Rule(RuleFactory):
|
||||
|
||||
:internal:
|
||||
"""
|
||||
if method is not None:
|
||||
if self.methods is not None and method not in self.methods:
|
||||
# if a method was given explicitly and that method is not supported
|
||||
# by this rule, this rule is not suitable.
|
||||
if method is not None and self.methods is not None \
|
||||
and method not in self.methods:
|
||||
return False
|
||||
|
||||
defaults = self.defaults or ()
|
||||
|
||||
# all arguments required must be either in the defaults dict or
|
||||
# the value dictionary otherwise it's not suitable
|
||||
for key in self.arguments:
|
||||
if key not in defaults and key not in values:
|
||||
return False
|
||||
|
||||
valueset = set(values)
|
||||
|
||||
for key in self.arguments - set(self.defaults or ()):
|
||||
if key not in values:
|
||||
return False
|
||||
|
||||
if self.arguments.issubset(valueset):
|
||||
if self.defaults is None:
|
||||
return True
|
||||
for key, value in self.defaults.iteritems():
|
||||
if value != values[key]:
|
||||
# in case defaults are given we ensure taht either the value was
|
||||
# skipped or the value is the same as the default value.
|
||||
if defaults:
|
||||
for key, value in defaults.iteritems():
|
||||
if key in values and value != values[key]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def match_compare(self, other):
|
||||
"""Compare this object with another one for matching.
|
||||
def match_compare_key(self):
|
||||
"""The match compare key for sorting.
|
||||
|
||||
Current implementation:
|
||||
|
||||
1. rules without any arguments come first for performance
|
||||
reasons only as we expect them to match faster and some
|
||||
common ones usually don't have any arguments (index pages etc.)
|
||||
2. The more complex rules come first so the second argument is the
|
||||
negative length of the number of weights.
|
||||
3. lastly we order by the actual weights.
|
||||
|
||||
:internal:
|
||||
"""
|
||||
for sw, ow in izip(self._weights, other._weights):
|
||||
if sw > ow:
|
||||
return -1
|
||||
elif sw < ow:
|
||||
return 1
|
||||
if len(self._weights) > len(other._weights):
|
||||
return -1
|
||||
if len(self._weights) < len(other._weights):
|
||||
return 1
|
||||
if not other.arguments and self.arguments:
|
||||
return 1
|
||||
elif other.arguments and not self.arguments:
|
||||
return -1
|
||||
elif other.defaults is None and self.defaults is not None:
|
||||
return 1
|
||||
elif other.defaults is not None and self.defaults is None:
|
||||
return -1
|
||||
elif self.greediness > other.greediness:
|
||||
return -1
|
||||
elif self.greediness < other.greediness:
|
||||
return 1
|
||||
elif len(self.arguments) > len(other.arguments):
|
||||
return 1
|
||||
elif len(self.arguments) < len(other.arguments):
|
||||
return -1
|
||||
return 1
|
||||
return bool(self.arguments), -len(self._weights), self._weights
|
||||
|
||||
def build_compare(self, other):
|
||||
"""Compare this object with another one for building.
|
||||
def build_compare_key(self):
|
||||
"""The build compare key for sorting.
|
||||
|
||||
:internal:
|
||||
"""
|
||||
if not other.arguments and self.arguments:
|
||||
return -1
|
||||
elif other.arguments and not self.arguments:
|
||||
return 1
|
||||
elif other.defaults is None and self.defaults is not None:
|
||||
return -1
|
||||
elif other.defaults is not None and self.defaults is None:
|
||||
return 1
|
||||
elif self.provides_defaults_for(other):
|
||||
return -1
|
||||
elif other.provides_defaults_for(self):
|
||||
return 1
|
||||
elif self.greediness > other.greediness:
|
||||
return -1
|
||||
elif self.greediness < other.greediness:
|
||||
return 1
|
||||
elif len(self.arguments) > len(other.arguments):
|
||||
return -1
|
||||
elif len(self.arguments) < len(other.arguments):
|
||||
return 1
|
||||
return -1
|
||||
return self.alias and 1 or 0, -len(self.arguments), \
|
||||
-len(self.defaults or ())
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__class__ is other.__class__ and \
|
||||
@@ -776,7 +828,6 @@ class Rule(RuleFactory):
|
||||
class BaseConverter(object):
|
||||
"""Base class for all converters."""
|
||||
regex = '[^/]+'
|
||||
is_greedy = False
|
||||
weight = 100
|
||||
|
||||
def __init__(self, map):
|
||||
@@ -825,9 +876,9 @@ class UnicodeConverter(BaseConverter):
|
||||
|
||||
class AnyConverter(BaseConverter):
|
||||
"""Matches one of the items provided. Items can either be Python
|
||||
identifiers or unicode strings::
|
||||
identifiers or strings::
|
||||
|
||||
Rule('/<any(about, help, imprint, u"class"):page_name>')
|
||||
Rule('/<any(about, help, imprint, class, "foo,bar"):page_name>')
|
||||
|
||||
:param map: the :class:`Map`.
|
||||
:param items: this function accepts the possible items as positional
|
||||
@@ -849,8 +900,7 @@ class PathConverter(BaseConverter):
|
||||
:param map: the :class:`Map`.
|
||||
"""
|
||||
regex = '[^/].*?'
|
||||
is_greedy = True
|
||||
weight = 50
|
||||
weight = 200
|
||||
|
||||
|
||||
class NumberConverter(BaseConverter):
|
||||
@@ -858,6 +908,7 @@ class NumberConverter(BaseConverter):
|
||||
|
||||
:internal:
|
||||
"""
|
||||
weight = 50
|
||||
|
||||
def __init__(self, map, fixed_digits=0, min=None, max=None):
|
||||
BaseConverter.__init__(self, map)
|
||||
@@ -951,12 +1002,16 @@ class Map(object):
|
||||
See `url_encode` for more details.
|
||||
:param sort_key: The sort key function for `url_encode`.
|
||||
:param encoding_errors: the error method to use for decoding
|
||||
:param host_matching: if set to `True` it enables the host matching
|
||||
feature and disables the subdomain one. If
|
||||
enabled the `host` parameter to rules is used
|
||||
instead of the `subdomain` one.
|
||||
|
||||
.. versionadded:: 0.5
|
||||
`sort_parameters` and `sort_key` was added.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
`encoding_errors` was added.
|
||||
`encoding_errors` and `host_matching` was added.
|
||||
"""
|
||||
|
||||
#: .. versionadded:: 0.6
|
||||
@@ -966,7 +1021,7 @@ class Map(object):
|
||||
def __init__(self, rules=None, default_subdomain='', charset='utf-8',
|
||||
strict_slashes=True, redirect_defaults=True,
|
||||
converters=None, sort_parameters=False, sort_key=None,
|
||||
encoding_errors='ignore'):
|
||||
encoding_errors='replace', host_matching=False):
|
||||
self._rules = []
|
||||
self._rules_by_endpoint = {}
|
||||
self._remap = True
|
||||
@@ -976,6 +1031,7 @@ class Map(object):
|
||||
self.encoding_errors = encoding_errors
|
||||
self.strict_slashes = strict_slashes
|
||||
self.redirect_defaults = redirect_defaults
|
||||
self.host_matching = host_matching
|
||||
|
||||
self.converters = self.default_converters.copy()
|
||||
if converters:
|
||||
@@ -1014,6 +1070,7 @@ class Map(object):
|
||||
are returned.
|
||||
:return: an iterator
|
||||
"""
|
||||
self.update()
|
||||
if endpoint is not None:
|
||||
return iter(self._rules_by_endpoint[endpoint])
|
||||
return iter(self._rules)
|
||||
@@ -1051,8 +1108,15 @@ class Map(object):
|
||||
|
||||
.. versionadded:: 0.7
|
||||
`query_args` added
|
||||
|
||||
.. versionadded:: 0.8
|
||||
`query_args` can now also be a string.
|
||||
"""
|
||||
if subdomain is None:
|
||||
if self.host_matching:
|
||||
if subdomain is not None:
|
||||
raise RuntimeError('host matching enabled and a '
|
||||
'subdomain was provided')
|
||||
elif subdomain is None:
|
||||
subdomain = self.default_subdomain
|
||||
if script_name is None:
|
||||
script_name = '/'
|
||||
@@ -1098,7 +1162,7 @@ class Map(object):
|
||||
if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \
|
||||
in (('https', '443'), ('http', '80')):
|
||||
server_name += ':' + environ['SERVER_PORT']
|
||||
elif subdomain is None:
|
||||
elif subdomain is None and not self.host_matching:
|
||||
if 'HTTP_HOST' in environ:
|
||||
wsgi_server_name = environ.get('HTTP_HOST')
|
||||
else:
|
||||
@@ -1117,21 +1181,22 @@ class Map(object):
|
||||
subdomain = '.'.join(filter(None, cur_server_name[:offset]))
|
||||
return Map.bind(self, server_name, environ.get('SCRIPT_NAME'),
|
||||
subdomain, environ['wsgi.url_scheme'],
|
||||
environ['REQUEST_METHOD'], environ.get('PATH_INFO'))
|
||||
environ['REQUEST_METHOD'], environ.get('PATH_INFO'),
|
||||
query_args=environ.get('QUERY_STRING', ''))
|
||||
|
||||
def update(self):
|
||||
"""Called before matching and building to keep the compiled rules
|
||||
in the correct order after things changed.
|
||||
"""
|
||||
if self._remap:
|
||||
self._rules.sort(lambda a, b: a.match_compare(b))
|
||||
self._rules.sort(key=lambda x: x.match_compare_key())
|
||||
for rules in self._rules_by_endpoint.itervalues():
|
||||
rules.sort(lambda a, b: a.build_compare(b))
|
||||
rules.sort(key=lambda x: x.build_compare_key())
|
||||
self._remap = False
|
||||
|
||||
def __repr__(self):
|
||||
rules = self.iter_rules()
|
||||
return '%s([%s])' % (self.__class__.__name__, pformat(list(rules)))
|
||||
return '%s(%s)' % (self.__class__.__name__, pformat(list(rules)))
|
||||
|
||||
|
||||
class MapAdapter(object):
|
||||
@@ -1272,14 +1337,18 @@ class MapAdapter(object):
|
||||
:param return_rule: return the rule that matched instead of just the
|
||||
endpoint (defaults to `False`).
|
||||
:param query_args: optional query arguments that are used for
|
||||
automatic redirects. It's currently not possible
|
||||
to use the query arguments for URL matching.
|
||||
automatic redirects as string or dictionary. It's
|
||||
currently not possible to use the query arguments
|
||||
for URL matching.
|
||||
|
||||
.. versionadded:: 0.6
|
||||
`return_rule` was added.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
`query_args` was added.
|
||||
|
||||
.. versionchanged:: 0.8
|
||||
`query_args` can now also be a string.
|
||||
"""
|
||||
self.map.update()
|
||||
if path_info is None:
|
||||
@@ -1287,13 +1356,13 @@ class MapAdapter(object):
|
||||
if not isinstance(path_info, unicode):
|
||||
path_info = path_info.decode(self.map.charset,
|
||||
self.map.encoding_errors)
|
||||
if '?' in path_info:
|
||||
path_info, query_args = path_info.split('?')
|
||||
query_args = url_decode(query_args, self.map.charset)
|
||||
if query_args is None:
|
||||
query_args = self.query_args
|
||||
method = (method or self.default_method).upper()
|
||||
path = u'%s|/%s' % (self.subdomain, path_info.lstrip('/'))
|
||||
|
||||
path = u'%s|/%s' % (self.map.host_matching and self.server_name or
|
||||
self.subdomain, path_info.lstrip('/'))
|
||||
|
||||
have_match_for = set()
|
||||
for rule in self.map._rules:
|
||||
try:
|
||||
@@ -1301,19 +1370,21 @@ class MapAdapter(object):
|
||||
except RequestSlash:
|
||||
raise RequestRedirect(self.make_redirect_url(
|
||||
path_info + '/', query_args))
|
||||
except RequestAliasRedirect, e:
|
||||
raise RequestRedirect(self.make_alias_redirect_url(
|
||||
path, rule.endpoint, e.matched_values, method, query_args))
|
||||
if rv is None:
|
||||
continue
|
||||
if rule.methods is not None and method not in rule.methods:
|
||||
have_match_for.update(rule.methods)
|
||||
continue
|
||||
|
||||
if self.map.redirect_defaults:
|
||||
for r in self.map._rules_by_endpoint[rule.endpoint]:
|
||||
if r.provides_defaults_for(rule) and \
|
||||
r.suitable_for(rv, method):
|
||||
rv.update(r.defaults)
|
||||
subdomain, path = r.build(rv)
|
||||
raise RequestRedirect(self.make_redirect_url(
|
||||
path, query_args, subdomain=subdomain))
|
||||
redirect_url = self.get_default_redirect(rule, method, rv,
|
||||
query_args)
|
||||
if redirect_url is not None:
|
||||
raise RequestRedirect(redirect_url)
|
||||
|
||||
if rule.redirect_to is not None:
|
||||
if isinstance(rule.redirect_to, basestring):
|
||||
def _handle_match(match):
|
||||
@@ -1329,10 +1400,12 @@ class MapAdapter(object):
|
||||
self.server_name,
|
||||
self.script_name
|
||||
), redirect_url)))
|
||||
|
||||
if return_rule:
|
||||
return rule, rv
|
||||
else:
|
||||
return rule.endpoint, rv
|
||||
|
||||
if have_match_for:
|
||||
raise MethodNotAllowed(valid_methods=list(have_match_for))
|
||||
raise NotFound()
|
||||
@@ -1350,7 +1423,7 @@ class MapAdapter(object):
|
||||
self.match(path_info, method)
|
||||
except RequestRedirect:
|
||||
pass
|
||||
except (NotFound, MethodNotAllowed):
|
||||
except HTTPException:
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -1367,22 +1440,71 @@ class MapAdapter(object):
|
||||
pass
|
||||
return []
|
||||
|
||||
def make_redirect_url(self, path_info, query_args=None, subdomain=None):
|
||||
"""Creates a redirect URL."""
|
||||
suffix = ''
|
||||
if query_args:
|
||||
suffix = '?' + url_encode(query_args, self.map.charset)
|
||||
def get_host(self, domain_part):
|
||||
"""Figures out the full host name for the given domain part. The
|
||||
domain part is a subdomain in case host matching is disabled or
|
||||
a full host name.
|
||||
"""
|
||||
if self.map.host_matching:
|
||||
if domain_part is None:
|
||||
return self.server_name
|
||||
return domain_part
|
||||
subdomain = domain_part
|
||||
if subdomain is None:
|
||||
subdomain = self.subdomain
|
||||
return str('%s://%s%s/%s%s' % (
|
||||
return (subdomain and subdomain + '.' or '') + self.server_name
|
||||
|
||||
def get_default_redirect(self, rule, method, values, query_args):
|
||||
"""A helper that returns the URL to redirect to if it finds one.
|
||||
This is used for default redirecting only.
|
||||
|
||||
:internal:
|
||||
"""
|
||||
assert self.map.redirect_defaults
|
||||
for r in self.map._rules_by_endpoint[rule.endpoint]:
|
||||
# every rule that comes after this one, including ourself
|
||||
# has a lower priority for the defaults. We order the ones
|
||||
# with the highest priority up for building.
|
||||
if r is rule:
|
||||
break
|
||||
if r.provides_defaults_for(rule) and \
|
||||
r.suitable_for(values, method):
|
||||
values.update(r.defaults)
|
||||
domain_part, path = r.build(values)
|
||||
return self.make_redirect_url(
|
||||
path, query_args, domain_part=domain_part)
|
||||
|
||||
def encode_query_args(self, query_args):
|
||||
if not isinstance(query_args, basestring):
|
||||
query_args = url_encode(query_args, self.map.charset)
|
||||
return query_args
|
||||
|
||||
def make_redirect_url(self, path_info, query_args=None, domain_part=None):
|
||||
"""Creates a redirect URL.
|
||||
|
||||
:internal:
|
||||
"""
|
||||
suffix = ''
|
||||
if query_args:
|
||||
suffix = '?' + self.encode_query_args(query_args)
|
||||
return str('%s://%s/%s%s' % (
|
||||
self.url_scheme,
|
||||
subdomain and subdomain + '.' or '',
|
||||
self.server_name,
|
||||
self.get_host(domain_part),
|
||||
posixpath.join(self.script_name[:-1].lstrip('/'),
|
||||
url_quote(path_info.lstrip('/'), self.map.charset)),
|
||||
suffix
|
||||
))
|
||||
|
||||
def make_alias_redirect_url(self, path, endpoint, values, method, query_args):
|
||||
"""Internally called to make an alias redirect URL."""
|
||||
url = self.build(endpoint, values, method, append_unknown=False,
|
||||
force_external=True)
|
||||
if query_args:
|
||||
url += '?' + self.encode_query_args(query_args)
|
||||
assert url != path, 'detected invalid alias setting. No canonical ' \
|
||||
'URL found'
|
||||
return url
|
||||
|
||||
def _partial_build(self, endpoint, values, method, append_unknown):
|
||||
"""Helper for :meth:`build`. Returns subdomain and path for the
|
||||
rule that accepts this endpoint, values and method.
|
||||
@@ -1471,14 +1593,18 @@ class MapAdapter(object):
|
||||
rv = self._partial_build(endpoint, values, method, append_unknown)
|
||||
if rv is None:
|
||||
raise BuildError(endpoint, values, method)
|
||||
subdomain, path = rv
|
||||
domain_part, path = rv
|
||||
|
||||
if not force_external and subdomain == self.subdomain:
|
||||
host = self.get_host(domain_part)
|
||||
|
||||
# shortcut this.
|
||||
if not force_external and (
|
||||
(self.map.host_matching and host == self.server_name) or
|
||||
(not self.map.host_matching and domain_part == self.subdomain)):
|
||||
return str(urljoin(self.script_name, './' + path.lstrip('/')))
|
||||
return str('%s://%s%s%s/%s' % (
|
||||
return str('%s://%s%s/%s' % (
|
||||
self.url_scheme,
|
||||
subdomain and subdomain + '.' or '',
|
||||
self.server_name,
|
||||
host,
|
||||
self.script_name[:-1],
|
||||
path.lstrip('/')
|
||||
))
|
||||
|
||||
@@ -67,7 +67,7 @@ r'''
|
||||
or as named parameters, pretty much like Python function calls.
|
||||
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
'''
|
||||
import sys
|
||||
@@ -262,11 +262,15 @@ def make_shell(init_func=None, banner=None, use_ipython=True):
|
||||
namespace = init_func()
|
||||
if ipython:
|
||||
try:
|
||||
import IPython
|
||||
try:
|
||||
from IPython.frontend.terminal.embed import InteractiveShellEmbed
|
||||
sh = InteractiveShellEmbed(banner1=banner)
|
||||
except ImportError:
|
||||
from IPython.Shell import IPShellEmbed
|
||||
sh = IPShellEmbed(banner=banner)
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
sh = IPython.Shell.IPShellEmbed(banner=banner)
|
||||
sh(global_ns={}, local_ns=namespace)
|
||||
return
|
||||
from code import interact
|
||||
|
||||
@@ -5,11 +5,14 @@
|
||||
|
||||
Security related helpers such as secure password hashing tools.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
import hmac
|
||||
import string
|
||||
import posixpath
|
||||
from itertools import izip
|
||||
from random import SystemRandom
|
||||
|
||||
# because the API of hmac changed with the introduction of the
|
||||
@@ -31,6 +34,24 @@ SALT_CHARS = string.letters + string.digits
|
||||
|
||||
|
||||
_sys_rng = SystemRandom()
|
||||
_os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep]
|
||||
if sep not in (None, '/'))
|
||||
|
||||
|
||||
def safe_str_cmp(a, b):
|
||||
"""This function compares strings in somewhat constant time. This
|
||||
requires that the length of at least one string is known in advance.
|
||||
|
||||
Returns `True` if the two strings are equal or `False` if they are not.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
"""
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
rv = 0
|
||||
for x, y in izip(a, b):
|
||||
rv |= ord(x) ^ ord(y)
|
||||
return rv == 0
|
||||
|
||||
|
||||
def gen_salt(length):
|
||||
@@ -101,4 +122,20 @@ def check_password_hash(pwhash, password):
|
||||
if pwhash.count('$') < 2:
|
||||
return False
|
||||
method, salt, hashval = pwhash.split('$', 2)
|
||||
return _hash_internal(method, salt, password) == hashval
|
||||
return safe_str_cmp(_hash_internal(method, salt, password), hashval)
|
||||
|
||||
|
||||
def safe_join(directory, filename):
|
||||
"""Safely join `directory` and `filename`. If this cannot be done,
|
||||
this function returns ``None``.
|
||||
|
||||
:param directory: the base directory.
|
||||
:param filename: the untrusted filename relative to that directory.
|
||||
"""
|
||||
filename = posixpath.normpath(filename)
|
||||
for sep in _os_alt_seps:
|
||||
if sep in filename:
|
||||
return None
|
||||
if os.path.isabs(filename) or filename.startswith('../'):
|
||||
return None
|
||||
return os.path.join(directory, filename)
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
instead of a simple start file.
|
||||
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
@@ -40,6 +40,7 @@ import socket
|
||||
import sys
|
||||
import time
|
||||
import thread
|
||||
import signal
|
||||
import subprocess
|
||||
from urllib import unquote
|
||||
from SocketServer import ThreadingMixIn, ForkingMixIn
|
||||
@@ -63,6 +64,10 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
else:
|
||||
path_info = self.path
|
||||
query = ''
|
||||
|
||||
def shutdown_server():
|
||||
self.server.shutdown_signal = True
|
||||
|
||||
url_scheme = self.server.ssl_context is None and 'http' or 'https'
|
||||
environ = {
|
||||
'wsgi.version': (1, 0),
|
||||
@@ -72,6 +77,8 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
'wsgi.multithread': self.server.multithread,
|
||||
'wsgi.multiprocess': self.server.multiprocess,
|
||||
'wsgi.run_once': False,
|
||||
'werkzeug.server.shutdown':
|
||||
shutdown_server,
|
||||
'SERVER_SOFTWARE': self.server_version,
|
||||
'REQUEST_METHOD': self.command,
|
||||
'SCRIPT_NAME': '',
|
||||
@@ -171,12 +178,27 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object):
|
||||
def handle(self):
|
||||
"""Handles a request ignoring dropped connections."""
|
||||
try:
|
||||
return BaseHTTPRequestHandler.handle(self)
|
||||
rv = BaseHTTPRequestHandler.handle(self)
|
||||
except (socket.error, socket.timeout), e:
|
||||
self.connection_dropped(e)
|
||||
except Exception:
|
||||
if self.server.ssl_context is None or not is_ssl_error():
|
||||
raise
|
||||
if self.server.shutdown_signal:
|
||||
self.initiate_shutdown()
|
||||
return rv
|
||||
|
||||
def initiate_shutdown(self):
|
||||
"""A horrible, horrible way to kill the server for Python 2.6 and
|
||||
later. It's the best we can do.
|
||||
"""
|
||||
# reloader active
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
os.kill(os.getpid(), signal.SIGKILL)
|
||||
# python 2.7
|
||||
self.server._BaseServer__shutdown_request = True
|
||||
# python 2.6
|
||||
self.server._BaseServer__serving = False
|
||||
|
||||
def connection_dropped(self, error, environ=None):
|
||||
"""Called if the connection was closed by the client. By default
|
||||
@@ -299,6 +321,7 @@ class BaseWSGIServer(HTTPServer, object):
|
||||
"""Simple single-threaded, single-process WSGI server."""
|
||||
multithread = False
|
||||
multiprocess = False
|
||||
request_queue_size = 128
|
||||
|
||||
def __init__(self, host, port, app, handler=None,
|
||||
passthrough_errors=False, ssl_context=None):
|
||||
@@ -308,6 +331,7 @@ class BaseWSGIServer(HTTPServer, object):
|
||||
HTTPServer.__init__(self, (host, int(port)), handler)
|
||||
self.app = app
|
||||
self.passthrough_errors = passthrough_errors
|
||||
self.shutdown_signal = False
|
||||
|
||||
if ssl_context is not None:
|
||||
try:
|
||||
@@ -326,6 +350,7 @@ class BaseWSGIServer(HTTPServer, object):
|
||||
_log(type, message, *args)
|
||||
|
||||
def serve_forever(self):
|
||||
self.shutdown_signal = False
|
||||
try:
|
||||
HTTPServer.serve_forever(self)
|
||||
except KeyboardInterrupt:
|
||||
@@ -410,6 +435,7 @@ def reloader_loop(extra_files=None, interval=1):
|
||||
|
||||
reloader(fnames, interval=interval)
|
||||
|
||||
|
||||
def _reloader_stat_loop(fnames, interval=1):
|
||||
mtimes = {}
|
||||
while 1:
|
||||
@@ -428,15 +454,23 @@ def _reloader_stat_loop(fnames, interval=1):
|
||||
sys.exit(3)
|
||||
time.sleep(interval)
|
||||
|
||||
|
||||
def _reloader_inotify(fnames, interval=None):
|
||||
#: Mutated by inotify loop when changes occur.
|
||||
# Mutated by inotify loop when changes occur.
|
||||
changed = [False]
|
||||
|
||||
# Setup inotify watches
|
||||
from pyinotify import WatchManager, EventsCodes, Notifier
|
||||
from pyinotify import WatchManager, Notifier
|
||||
|
||||
# this API changed at one point, support both
|
||||
try:
|
||||
from pyinotify import EventsCodes as ec
|
||||
ec.IN_ATTRIB
|
||||
except (ImportError, AttributeError):
|
||||
import pyinotify as ec
|
||||
|
||||
wm = WatchManager()
|
||||
mask = "IN_DELETE_SELF IN_MOVE_SELF IN_MODIFY IN_ATTRIB".split()
|
||||
mask = reduce(lambda m, a: m | getattr(EventsCodes, a), mask, 0)
|
||||
mask = ec.IN_DELETE_SELF | ec.IN_MOVE_SELF | ec.IN_MODIFY | ec.IN_ATTRIB
|
||||
|
||||
def signal_changed(event):
|
||||
if changed[0]:
|
||||
@@ -459,15 +493,11 @@ def _reloader_inotify(fnames, interval=None):
|
||||
notif.stop()
|
||||
sys.exit(3)
|
||||
|
||||
# Decide which reloader to use
|
||||
try:
|
||||
__import__("pyinotify") # Pyflakes-avoidant
|
||||
except ImportError:
|
||||
reloader = _reloader_stat_loop
|
||||
reloader_name = "stat() polling"
|
||||
else:
|
||||
reloader = _reloader_inotify
|
||||
reloader_name = "inotify events"
|
||||
|
||||
# currently we always use the stat loop reloader for the simple reason
|
||||
# that the inotify one does not respond to added files properly. Also
|
||||
# it's quite buggy and the API is a mess.
|
||||
reloader = _reloader_stat_loop
|
||||
|
||||
|
||||
def restart_with_reloader():
|
||||
@@ -475,7 +505,7 @@ def restart_with_reloader():
|
||||
but running the reloader thread.
|
||||
"""
|
||||
while 1:
|
||||
_log('info', ' * Restarting with reloader: %s', reloader_name)
|
||||
_log('info', ' * Restarting with reloader')
|
||||
args = [sys.executable] + sys.argv
|
||||
new_environ = os.environ.copy()
|
||||
new_environ['WERKZEUG_RUN_MAIN'] = 'true'
|
||||
@@ -495,6 +525,8 @@ def restart_with_reloader():
|
||||
|
||||
def run_with_reloader(main_func, extra_files=None, interval=1):
|
||||
"""Run the given function in an independent python interpreter."""
|
||||
import signal
|
||||
signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))
|
||||
if os.environ.get('WERKZEUG_RUN_MAIN') == 'true':
|
||||
thread.start_new_thread(main_func, ())
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,7 @@ r"""
|
||||
|
||||
A minimal template engine.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD License.
|
||||
"""
|
||||
import sys
|
||||
@@ -19,6 +19,11 @@ from werkzeug._internal import _decode_unicode
|
||||
from werkzeug.datastructures import MultiDict
|
||||
|
||||
|
||||
from warnings import warn
|
||||
warn(DeprecationWarning('werkzeug.templates is deprecated and '
|
||||
'will be removed in Werkzeug 1.0'))
|
||||
|
||||
|
||||
# Copyright notice: The `parse_data` method uses the string interpolation
|
||||
# algorithm by Ka-Ping Yee which originally was part of `Itpl20.py`_.
|
||||
#
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This module implements a client to WSGI applications for testing.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import sys
|
||||
@@ -23,6 +23,7 @@ from werkzeug._internal import _empty_stream, _get_environ
|
||||
from werkzeug.wrappers import BaseRequest
|
||||
from werkzeug.urls import url_encode, url_fix, iri_to_uri, _unquote
|
||||
from werkzeug.wsgi import get_host, get_current_url, ClosingIterator
|
||||
from werkzeug.utils import dump_cookie
|
||||
from werkzeug.datastructures import FileMultiDict, MultiDict, \
|
||||
CombinedMultiDict, Headers, FileStorage
|
||||
|
||||
@@ -85,7 +86,7 @@ def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500,
|
||||
else:
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode(charset)
|
||||
write('\r\n\r\n' + value)
|
||||
write('\r\n\r\n' + str(value))
|
||||
write('\r\n')
|
||||
write('--%s--\r\n' % boundary)
|
||||
|
||||
@@ -357,7 +358,7 @@ class EnvironBuilder(object):
|
||||
def _get_content_type(self):
|
||||
ct = self.headers.get('Content-Type')
|
||||
if ct is None and not self._input_stream:
|
||||
if self.method in ('POST', 'PUT'):
|
||||
if self.method in ('POST', 'PUT', 'PATCH'):
|
||||
if self._files:
|
||||
return 'multipart/form-data'
|
||||
return 'application/x-www-form-urlencoded'
|
||||
@@ -609,6 +610,25 @@ class Client(object):
|
||||
self.redirect_client = None
|
||||
self.allow_subdomain_redirects = allow_subdomain_redirects
|
||||
|
||||
def set_cookie(self, server_name, key, value='', max_age=None,
|
||||
expires=None, path='/', domain=None, secure=None,
|
||||
httponly=False, charset='utf-8'):
|
||||
"""Sets a cookie in the client's cookie jar. The server name
|
||||
is required and has to match the one that is also passed to
|
||||
the open call.
|
||||
"""
|
||||
assert self.cookie_jar is not None, 'cookies disabled'
|
||||
header = dump_cookie(key, value, max_age, expires, path, domain,
|
||||
secure, httponly, charset)
|
||||
environ = create_environ(path, base_url='http://' + server_name)
|
||||
headers = [('Set-Cookie', header)]
|
||||
self.cookie_jar.extract_wsgi(environ, headers)
|
||||
|
||||
def delete_cookie(self, server_name, key, path='/', domain=None):
|
||||
"""Deletes a cookie in the test client."""
|
||||
self.set_cookie(server_name, key, expires=0, max_age=0,
|
||||
path=path, domain=domain)
|
||||
|
||||
def open(self, *args, **kwargs):
|
||||
"""Takes the same arguments as the :class:`EnvironBuilder` class with
|
||||
some additions: You can provide a :class:`EnvironBuilder` or a WSGI
|
||||
@@ -714,6 +734,11 @@ class Client(object):
|
||||
kw['method'] = 'GET'
|
||||
return self.open(*args, **kw)
|
||||
|
||||
def patch(self, *args, **kw):
|
||||
"""Like open but method is enforced to PATCH."""
|
||||
kw['method'] = 'PATCH'
|
||||
return self.open(*args, **kw)
|
||||
|
||||
def post(self, *args, **kw):
|
||||
"""Like open but method is enforced to POST."""
|
||||
kw['method'] = 'POST'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Provide a small test application that can be used to test a WSGI server
|
||||
and check it for WSGI compliance.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
@@ -160,9 +160,13 @@ def render_testapp(req):
|
||||
b.project_name.lower()))
|
||||
python_eggs = []
|
||||
for egg in eggs:
|
||||
try:
|
||||
version = egg.version
|
||||
except (ValueError, AttributeError):
|
||||
version = 'unknown'
|
||||
python_eggs.append('<li>%s <small>[%s]</small>' % (
|
||||
escape(egg.project_name),
|
||||
escape(egg.version)
|
||||
escape(version)
|
||||
))
|
||||
|
||||
wsgi_env = []
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This module implements various URL related functions.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import urlparse
|
||||
@@ -57,13 +57,13 @@ def _safe_urlsplit(s):
|
||||
to what we think it is.
|
||||
"""
|
||||
rv = urlparse.urlsplit(s)
|
||||
if type(rv[1]) is not type(s):
|
||||
try:
|
||||
return tuple(map(type(s), rv))
|
||||
except UnicodeError:
|
||||
# oh well, we most likely will break later again, but
|
||||
# let's just say it worked out well to that point.
|
||||
pass
|
||||
# we have to check rv[2] here and not rv[1] as rv[1] will be
|
||||
# an empty bytestring in case no domain was given.
|
||||
if type(rv[2]) is not type(s):
|
||||
assert hasattr(urlparse, 'clear_cache')
|
||||
urlparse.clear_cache()
|
||||
rv = urlparse.urlsplit(s)
|
||||
assert type(rv[2]) is type(s)
|
||||
return rv
|
||||
|
||||
|
||||
@@ -143,10 +143,12 @@ def iri_to_uri(iri, charset='utf-8'):
|
||||
path = _quote(path.encode(charset), safe="/:~+")
|
||||
query = _quote(query.encode(charset), safe="=%&[]:;$()+,!?*/")
|
||||
|
||||
return urlparse.urlunsplit([scheme, hostname, path, query, fragment])
|
||||
# this absolutely always must return a string. Otherwise some parts of
|
||||
# the system might perform double quoting (#61)
|
||||
return str(urlparse.urlunsplit([scheme, hostname, path, query, fragment]))
|
||||
|
||||
|
||||
def uri_to_iri(uri, charset='utf-8', errors='ignore'):
|
||||
def uri_to_iri(uri, charset='utf-8', errors='replace'):
|
||||
r"""Converts a URI in a given charset to a IRI.
|
||||
|
||||
Examples for URI versus IRI
|
||||
@@ -203,7 +205,7 @@ def uri_to_iri(uri, charset='utf-8', errors='ignore'):
|
||||
|
||||
|
||||
def url_decode(s, charset='utf-8', decode_keys=False, include_empty=True,
|
||||
errors='ignore', separator='&', cls=None):
|
||||
errors='replace', separator='&', cls=None):
|
||||
"""Parse a querystring and return it as :class:`MultiDict`. Per default
|
||||
only values are decoded into unicode strings. If `decode_keys` is set to
|
||||
`True` the same will happen for keys.
|
||||
@@ -321,7 +323,7 @@ def url_quote_plus(s, charset='utf-8', safe=''):
|
||||
return _quote_plus(s, safe=safe)
|
||||
|
||||
|
||||
def url_unquote(s, charset='utf-8', errors='ignore'):
|
||||
def url_unquote(s, charset='utf-8', errors='replace'):
|
||||
"""URL decode a single string with a given decoding.
|
||||
|
||||
Per default encoding errors are ignored. If you want a different behavior
|
||||
@@ -337,7 +339,7 @@ def url_unquote(s, charset='utf-8', errors='ignore'):
|
||||
return _decode_unicode(_unquote(s), charset, errors)
|
||||
|
||||
|
||||
def url_unquote_plus(s, charset='utf-8', errors='ignore'):
|
||||
def url_unquote_plus(s, charset='utf-8', errors='replace'):
|
||||
"""URL decode a single string with the given decoding and decode
|
||||
a "+" to whitespace.
|
||||
|
||||
@@ -368,7 +370,7 @@ def url_fix(s, charset='utf-8'):
|
||||
unicode string.
|
||||
"""
|
||||
if isinstance(s, unicode):
|
||||
s = s.encode(charset, 'ignore')
|
||||
s = s.encode(charset, 'replace')
|
||||
scheme, netloc, path, qs, anchor = _safe_urlsplit(s)
|
||||
path = _quote(path, '/%')
|
||||
qs = _quote_plus(qs, ':&%=')
|
||||
@@ -458,7 +460,7 @@ class Href(object):
|
||||
if path:
|
||||
if not rv.endswith('/'):
|
||||
rv += '/'
|
||||
rv = urlparse.urljoin(rv, path)
|
||||
rv = urlparse.urljoin(rv, './' + path)
|
||||
if query:
|
||||
rv += '?' + url_encode(query, self.charset, sort=self.sort,
|
||||
key=self.key)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
browsers.
|
||||
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
@@ -18,7 +18,7 @@ class UserAgentParser(object):
|
||||
"""A simple user agent parser. Used by the `UserAgent`."""
|
||||
|
||||
platforms = (
|
||||
('iphone', 'iphone'),
|
||||
('iphone|ios', 'iphone'),
|
||||
(r'darwin|mac|os\s*x', 'macos'),
|
||||
('win', 'windows'),
|
||||
(r'android', 'android'),
|
||||
|
||||
@@ -7,17 +7,15 @@
|
||||
them are used by the request and response wrappers but especially for
|
||||
middleware development it makes sense to use them without the wrappers.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import re
|
||||
import os
|
||||
from time import time
|
||||
from datetime import datetime, timedelta
|
||||
import sys
|
||||
|
||||
from werkzeug._internal import _decode_unicode, \
|
||||
_iter_modules, _ExtendedCookie, _ExtendedMorsel, \
|
||||
_DictAccessorProperty, _parse_signature, _missing
|
||||
from werkzeug._internal import _iter_modules, _DictAccessorProperty, \
|
||||
_parse_signature, _missing
|
||||
|
||||
|
||||
_format_re = re.compile(r'\$(?:(%s)|\{(%s)\})' % (('[a-zA-Z_][a-zA-Z0-9_]*',) * 2))
|
||||
@@ -340,106 +338,6 @@ def unescape(s):
|
||||
return _entity_re.sub(handle_match, s)
|
||||
|
||||
|
||||
def parse_cookie(header, charset='utf-8', errors='ignore',
|
||||
cls=None):
|
||||
"""Parse a cookie. Either from a string or WSGI environ.
|
||||
|
||||
Per default encoding errors are ignored. If you want a different behavior
|
||||
you can set `errors` to ``'replace'`` or ``'strict'``. In strict mode a
|
||||
:exc:`HTTPUnicodeError` is raised.
|
||||
|
||||
.. versionchanged:: 0.5
|
||||
This function now returns a :class:`TypeConversionDict` instead of a
|
||||
regular dict. The `cls` parameter was added.
|
||||
|
||||
:param header: the header to be used to parse the cookie. Alternatively
|
||||
this can be a WSGI environment.
|
||||
:param charset: the charset for the cookie values.
|
||||
:param errors: the error behavior for the charset decoding.
|
||||
:param cls: an optional dict class to use. If this is not specified
|
||||
or `None` the default :class:`TypeConversionDict` is
|
||||
used.
|
||||
"""
|
||||
if isinstance(header, dict):
|
||||
header = header.get('HTTP_COOKIE', '')
|
||||
if cls is None:
|
||||
cls = TypeConversionDict
|
||||
cookie = _ExtendedCookie()
|
||||
cookie.load(header)
|
||||
result = {}
|
||||
|
||||
# decode to unicode and skip broken items. Our extended morsel
|
||||
# and extended cookie will catch CookieErrors and convert them to
|
||||
# `None` items which we have to skip here.
|
||||
for key, value in cookie.iteritems():
|
||||
if value.value is not None:
|
||||
result[key] = _decode_unicode(unquote_header_value(value.value),
|
||||
charset, errors)
|
||||
|
||||
return cls(result)
|
||||
|
||||
|
||||
def dump_cookie(key, value='', max_age=None, expires=None, path='/',
|
||||
domain=None, secure=None, httponly=False, charset='utf-8',
|
||||
sync_expires=True):
|
||||
"""Creates a new Set-Cookie header without the ``Set-Cookie`` prefix
|
||||
The parameters are the same as in the cookie Morsel object in the
|
||||
Python standard library but it accepts unicode data, too.
|
||||
|
||||
:param max_age: should be a number of seconds, or `None` (default) if
|
||||
the cookie should last only as long as the client's
|
||||
browser session. Additionally `timedelta` objects
|
||||
are accepted, too.
|
||||
:param expires: should be a `datetime` object or unix timestamp.
|
||||
:param path: limits the cookie to a given path, per default it will
|
||||
span the whole domain.
|
||||
:param domain: Use this if you want to set a cross-domain cookie. For
|
||||
example, ``domain=".example.com"`` will set a cookie
|
||||
that is readable by the domain ``www.example.com``,
|
||||
``foo.example.com`` etc. Otherwise, a cookie will only
|
||||
be readable by the domain that set it.
|
||||
:param secure: The cookie will only be available via HTTPS
|
||||
:param httponly: disallow JavaScript to access the cookie. This is an
|
||||
extension to the cookie standard and probably not
|
||||
supported by all browsers.
|
||||
:param charset: the encoding for unicode values.
|
||||
:param sync_expires: automatically set expires if max_age is defined
|
||||
but expires not.
|
||||
"""
|
||||
try:
|
||||
key = str(key)
|
||||
except UnicodeError:
|
||||
raise TypeError('invalid key %r' % key)
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode(charset)
|
||||
value = quote_header_value(value)
|
||||
morsel = _ExtendedMorsel(key, value)
|
||||
if isinstance(max_age, timedelta):
|
||||
max_age = (max_age.days * 60 * 60 * 24) + max_age.seconds
|
||||
if expires is not None:
|
||||
if not isinstance(expires, basestring):
|
||||
expires = cookie_date(expires)
|
||||
morsel['expires'] = expires
|
||||
elif max_age is not None and sync_expires:
|
||||
morsel['expires'] = cookie_date(time() + max_age)
|
||||
if domain and ':' in domain:
|
||||
# The port part of the domain should NOT be used. Strip it
|
||||
domain = domain.split(':', 1)[0]
|
||||
if domain:
|
||||
assert '.' in domain, (
|
||||
"Setting \"domain\" for a cookie on a server running localy (ex: "
|
||||
"localhost) is not supportted by complying browsers. You should "
|
||||
"have something like: \"127.0.0.1 localhost dev.localhost\" on "
|
||||
"your hosts file and then point your server to run on "
|
||||
"\"dev.localhost\" and also set \"domain\" for \"dev.localhost\""
|
||||
)
|
||||
for k, v in (('path', path), ('domain', domain), ('secure', secure),
|
||||
('max-age', max_age), ('httponly', httponly)):
|
||||
if v is not None and v is not False:
|
||||
morsel[k] = str(v)
|
||||
return morsel.output(header='').lstrip()
|
||||
|
||||
|
||||
def redirect(location, code=302):
|
||||
"""Return a response object (a WSGI application) that, if called,
|
||||
redirects the client to the target location. Supported codes are 301,
|
||||
@@ -452,7 +350,7 @@ def redirect(location, code=302):
|
||||
the :func:`iri_to_uri` function.
|
||||
|
||||
:param location: the location the response should redirect to.
|
||||
:param code: the redirect status code.
|
||||
:param code: the redirect status code. defaults to 302.
|
||||
"""
|
||||
assert code in (201, 301, 302, 303, 305, 307), 'invalid code'
|
||||
from werkzeug.wrappers import BaseResponse
|
||||
@@ -521,9 +419,9 @@ def import_string(import_name, silent=False):
|
||||
modname = module + '.' + obj
|
||||
__import__(modname)
|
||||
return sys.modules[modname]
|
||||
except ImportError:
|
||||
except ImportError, e:
|
||||
if not silent:
|
||||
raise
|
||||
raise ImportStringError(import_name, e), None, sys.exc_info()[2]
|
||||
|
||||
|
||||
def find_modules(import_path, include_packages=False, recursive=False):
|
||||
@@ -658,13 +556,56 @@ class ArgumentValidationError(ValueError):
|
||||
))
|
||||
|
||||
|
||||
class ImportStringError(ImportError):
|
||||
"""Provides information about a failed :func:`import_string` attempt."""
|
||||
|
||||
#: String in dotted notation that failed to be imported.
|
||||
import_name = None
|
||||
#: Wrapped exception.
|
||||
exception = None
|
||||
|
||||
def __init__(self, import_name, exception):
|
||||
self.import_name = import_name
|
||||
self.exception = exception
|
||||
|
||||
msg = (
|
||||
'import_string() failed for %r. Possible reasons are:\n\n'
|
||||
'- missing __init__.py in a package;\n'
|
||||
'- package or module path not included in sys.path;\n'
|
||||
'- duplicated package or module name taking precedence in '
|
||||
'sys.path;\n'
|
||||
'- missing module, class, function or variable;\n\n'
|
||||
'Debugged import:\n\n%s\n\n'
|
||||
'Original exception:\n\n%s: %s')
|
||||
|
||||
name = ''
|
||||
tracked = []
|
||||
for part in import_name.replace(':', '.').split('.'):
|
||||
name += (name and '.') + part
|
||||
imported = import_string(name, silent=True)
|
||||
if imported:
|
||||
tracked.append((name, imported.__file__))
|
||||
else:
|
||||
track = ['- %r found in %r.' % (n, i) for n, i in tracked]
|
||||
track.append('- %r not found.' % name)
|
||||
msg = msg % (import_name, '\n'.join(track),
|
||||
exception.__class__.__name__, str(exception))
|
||||
break
|
||||
|
||||
ImportError.__init__(self, msg)
|
||||
|
||||
def __repr__(self):
|
||||
return '<%s(%r, %r)>' % (self.__class__.__name__, self.import_name,
|
||||
self.exception)
|
||||
|
||||
|
||||
# circular dependencies
|
||||
from werkzeug.http import quote_header_value, unquote_header_value, \
|
||||
cookie_date
|
||||
from werkzeug.datastructures import TypeConversionDict
|
||||
|
||||
# DEPRECATED
|
||||
# these objects were previously in this module as well. we import
|
||||
# them here for backwards compatibility with old pickles.
|
||||
from werkzeug.datastructures import MultiDict, CombinedMultiDict, \
|
||||
Headers, EnvironHeaders
|
||||
from werkzeug.http import parse_cookie, dump_cookie
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
decoded into an unicode object if possible and if it makes sense.
|
||||
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import urlparse
|
||||
@@ -28,17 +28,20 @@ from werkzeug.http import HTTP_STATUS_CODES, \
|
||||
parse_date, generate_etag, is_resource_modified, unquote_etag, \
|
||||
quote_etag, parse_set_header, parse_authorization_header, \
|
||||
parse_www_authenticate_header, remove_entity_headers, \
|
||||
parse_options_header, dump_options_header, http_date
|
||||
parse_options_header, dump_options_header, http_date, \
|
||||
parse_if_range_header, parse_cookie, dump_cookie, \
|
||||
parse_range_header, parse_content_range_header, dump_header
|
||||
from werkzeug.urls import url_decode, iri_to_uri
|
||||
from werkzeug.formparser import parse_form_data, default_stream_factory
|
||||
from werkzeug.utils import cached_property, environ_property, \
|
||||
parse_cookie, dump_cookie, header_property, get_content_type
|
||||
header_property, get_content_type
|
||||
from werkzeug.wsgi import get_current_url, get_host, LimitedStream, \
|
||||
ClosingIterator
|
||||
from werkzeug.datastructures import MultiDict, CombinedMultiDict, Headers, \
|
||||
EnvironHeaders, ImmutableMultiDict, ImmutableTypeConversionDict, \
|
||||
ImmutableList, MIMEAccept, CharsetAccept, LanguageAccept, \
|
||||
ResponseCacheControl, RequestCacheControl, CallbackDict
|
||||
ResponseCacheControl, RequestCacheControl, CallbackDict, \
|
||||
ContentRange
|
||||
from werkzeug._internal import _empty_stream, _decode_unicode, \
|
||||
_patch_wrapper, _get_environ
|
||||
|
||||
@@ -113,17 +116,14 @@ class BaseRequest(object):
|
||||
#: the charset for the request, defaults to utf-8
|
||||
charset = 'utf-8'
|
||||
|
||||
#: the error handling procedure for errors, defaults to 'ignore'
|
||||
encoding_errors = 'ignore'
|
||||
|
||||
#: set to True if the application runs behind an HTTP proxy
|
||||
is_behind_proxy = False
|
||||
#: the error handling procedure for errors, defaults to 'replace'
|
||||
encoding_errors = 'replace'
|
||||
|
||||
#: the maximum content length. This is forwarded to the form data
|
||||
#: parsing function (:func:`parse_form_data`). When set and the
|
||||
#: :attr:`form` or :attr:`files` attribute is accessed and the
|
||||
#: parsing fails because more than the specified value is transmitted
|
||||
#: a :exc:`~exceptions.RequestEntityTooLarge` exception is raised.
|
||||
#: a :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception is raised.
|
||||
#:
|
||||
#: Have a look at :ref:`dealing-with-request-data` for more details.
|
||||
#:
|
||||
@@ -134,7 +134,7 @@ class BaseRequest(object):
|
||||
#: parsing function (:func:`parse_form_data`). When set and the
|
||||
#: :attr:`form` or :attr:`files` attribute is accessed and the
|
||||
#: data in memory for post data is longer than the specified value a
|
||||
#: :exc:`~exceptions.RequestEntityTooLarge` exception is raised.
|
||||
#: :exc:`~werkzeug.exceptions.RequestEntityTooLarge` exception is raised.
|
||||
#:
|
||||
#: Have a look at :ref:`dealing-with-request-data` for more details.
|
||||
#:
|
||||
@@ -142,25 +142,27 @@ class BaseRequest(object):
|
||||
max_form_memory_size = None
|
||||
|
||||
#: the class to use for `args` and `form`. The default is an
|
||||
#: :class:`ImmutableMultiDict` which supports multiple values per key.
|
||||
#: alternatively it makes sense to use an :class:`ImmutableOrderedMultiDict`
|
||||
#: which preserves order or a :class:`ImmutableDict` which is
|
||||
#: the fastest but only remembers the last key. It is also possible
|
||||
#: to use mutable structures, but this is not recommended.
|
||||
#: :class:`~werkzeug.datastructures.ImmutableMultiDict` which supports
|
||||
#: multiple values per key. alternatively it makes sense to use an
|
||||
#: :class:`~werkzeug.datastructures.ImmutableOrderedMultiDict` which
|
||||
#: preserves order or a :class:`~werkzeug.datastructures.ImmutableDict`
|
||||
#: which is the fastest but only remembers the last key. It is also
|
||||
#: possible to use mutable structures, but this is not recommended.
|
||||
#:
|
||||
#: .. versionadded:: 0.6
|
||||
parameter_storage_class = ImmutableMultiDict
|
||||
|
||||
#: the type to be used for list values from the incoming WSGI
|
||||
#: environment. By default an :class:`ImmutableList` is used
|
||||
#: the type to be used for list values from the incoming WSGI environment.
|
||||
#: By default an :class:`~werkzeug.datastructures.ImmutableList` is used
|
||||
#: (for example for :attr:`access_list`).
|
||||
#:
|
||||
#: .. versionadded:: 0.6
|
||||
list_storage_class = ImmutableList
|
||||
|
||||
#: the type to be used for dict values from the incoming WSGI
|
||||
#: environment. By default an :class:`ImmutableTypeConversionDict`
|
||||
#: is used (for example for :attr:`cookies`).
|
||||
#: the type to be used for dict values from the incoming WSGI environment.
|
||||
#: By default an
|
||||
#: :class:`~werkzeug.datastructures.ImmutableTypeConversionDict` is used
|
||||
#: (for example for :attr:`cookies`).
|
||||
#:
|
||||
#: .. versionadded:: 0.6
|
||||
dict_storage_class = ImmutableTypeConversionDict
|
||||
@@ -205,12 +207,13 @@ class BaseRequest(object):
|
||||
object (:class:`Client`) that allows to create multipart requests,
|
||||
support for cookies etc.
|
||||
|
||||
This accepts the same options as the :class:`EnvironBuilder`.
|
||||
This accepts the same options as the
|
||||
:class:`~werkzeug.test.EnvironBuilder`.
|
||||
|
||||
.. versionchanged:: 0.5
|
||||
This method now accepts the same arguments as
|
||||
:class:`EnvironBuilder`. Because of this the `environ` parameter
|
||||
is now called `environ_overrides`.
|
||||
:class:`~werkzeug.test.EnvironBuilder`. Because of this the
|
||||
`environ` parameter is now called `environ_overrides`.
|
||||
|
||||
:return: request object
|
||||
"""
|
||||
@@ -277,7 +280,7 @@ class BaseRequest(object):
|
||||
filled with the incoming form data. As a matter of fact the input
|
||||
stream will be empty afterwards.
|
||||
|
||||
:internal:
|
||||
.. versionadded:: 0.8
|
||||
"""
|
||||
# abort early if we have already consumed the stream
|
||||
if 'stream' in self.__dict__:
|
||||
@@ -288,7 +291,7 @@ class BaseRequest(object):
|
||||
'that, set `shallow` to False.')
|
||||
data = None
|
||||
stream = _empty_stream
|
||||
if self.environ['REQUEST_METHOD'] in ('POST', 'PUT'):
|
||||
if self.environ['REQUEST_METHOD'] in ('POST', 'PUT', 'PATCH'):
|
||||
try:
|
||||
data = parse_form_data(self.environ, self._get_file_stream,
|
||||
self.charset, self.encoding_errors,
|
||||
@@ -344,7 +347,8 @@ class BaseRequest(object):
|
||||
|
||||
@cached_property
|
||||
def args(self):
|
||||
"""The parsed URL parameters. By default a :class:`ImmutableMultiDict`
|
||||
"""The parsed URL parameters. By default an
|
||||
:class:`~werkzeug.datastructures.ImmutableMultiDict`
|
||||
is returned from this function. This can be changed by setting
|
||||
:attr:`parameter_storage_class` to a different type. This might
|
||||
be necessary if the order of the form data is important.
|
||||
@@ -366,7 +370,8 @@ class BaseRequest(object):
|
||||
|
||||
@cached_property
|
||||
def form(self):
|
||||
"""The form parameters. By default a :class:`ImmutableMultiDict`
|
||||
"""The form parameters. By default an
|
||||
:class:`~werkzeug.datastructures.ImmutableMultiDict`
|
||||
is returned from this function. This can be changed by setting
|
||||
:attr:`parameter_storage_class` to a different type. This might
|
||||
be necessary if the order of the form data is important.
|
||||
@@ -386,16 +391,18 @@ class BaseRequest(object):
|
||||
|
||||
@cached_property
|
||||
def files(self):
|
||||
""":class:`MultiDict` object containing all uploaded files. Each key in
|
||||
:attr:`files` is the name from the ``<input type="file" name="">``. Each
|
||||
value in :attr:`files` is a Werkzeug :class:`FileStorage` object.
|
||||
""":class:`~werkzeug.datastructures.MultiDict` object containing
|
||||
all uploaded files. Each key in :attr:`files` is the name from the
|
||||
``<input type="file" name="">``. Each value in :attr:`files` is a
|
||||
Werkzeug :class:`~werkzeug.datastructures.FileStorage` object.
|
||||
|
||||
Note that :attr:`files` will only contain data if the request method was
|
||||
POST or PUT and the ``<form>`` that posted to the request had
|
||||
POST, PUT or PATCH and the ``<form>`` that posted to the request had
|
||||
``enctype="multipart/form-data"``. It will be empty otherwise.
|
||||
|
||||
See the :class:`MultiDict` / :class:`FileStorage` documentation for more
|
||||
details about the used data structure.
|
||||
See the :class:`~werkzeug.datastructures.MultiDict` /
|
||||
:class:`~werkzeug.datastructures.FileStorage` documentation for
|
||||
more details about the used data structure.
|
||||
"""
|
||||
self._load_form_data()
|
||||
return self.files
|
||||
@@ -409,7 +416,7 @@ class BaseRequest(object):
|
||||
@cached_property
|
||||
def headers(self):
|
||||
"""The headers from the WSGI environ as immutable
|
||||
:class:`EnvironHeaders`.
|
||||
:class:`~werkzeug.datastructures.EnvironHeaders`.
|
||||
"""
|
||||
return EnvironHeaders(self.environ)
|
||||
|
||||
@@ -473,8 +480,6 @@ class BaseRequest(object):
|
||||
@property
|
||||
def remote_addr(self):
|
||||
"""The remote address of the client."""
|
||||
if self.is_behind_proxy and self.access_route:
|
||||
return self.access_route[0]
|
||||
return self.environ.get('REMOTE_ADDR')
|
||||
|
||||
remote_user = environ_property('REMOTE_USER', doc='''
|
||||
@@ -482,6 +487,11 @@ class BaseRequest(object):
|
||||
protected, this attribute contains the username the user has
|
||||
authenticated as.''')
|
||||
|
||||
scheme = environ_property('wsgi.url_scheme', doc='''
|
||||
URL scheme (http or https).
|
||||
|
||||
.. versionadded:: 0.7''')
|
||||
|
||||
is_xhr = property(lambda x: x.environ.get('HTTP_X_REQUESTED_WITH', '')
|
||||
.lower() == 'xmlhttprequest', doc='''
|
||||
True if the request was triggered via a JavaScript XMLHttpRequest.
|
||||
@@ -544,9 +554,10 @@ class BaseResponse(object):
|
||||
encoded. Please refer to `the unicode chapter <unicode.txt>`_ for more
|
||||
details about customizing the behavior.
|
||||
|
||||
Response can be any kind of iterable or string. If it's a string
|
||||
it's considered being an iterable with one item which is the string
|
||||
passed. Headers can be a list of tuples or a :class:`Headers` object.
|
||||
Response can be any kind of iterable or string. If it's a string it's
|
||||
considered being an iterable with one item which is the string passed.
|
||||
Headers can be a list of tuples or a
|
||||
:class:`~werkzeug.datastructures.Headers` object.
|
||||
|
||||
Special note for `mimetype` and `content_type`: For most mime types
|
||||
`mimetype` and `content_type` work the same, the difference affects
|
||||
@@ -560,7 +571,8 @@ class BaseResponse(object):
|
||||
|
||||
:param response: a string or response iterable.
|
||||
:param status: a string with a status or an integer with the status code.
|
||||
:param headers: a list of headers or an :class:`Headers` object.
|
||||
:param headers: a list of headers or a
|
||||
:class:`~werkzeug.datastructures.Headers` object.
|
||||
:param mimetype: the mimetype for the request. See notice above.
|
||||
:param content_type: the content type for the request. See notice above.
|
||||
:param direct_passthrough: if set to `True` :meth:`iter_encoded` is not
|
||||
@@ -627,11 +639,14 @@ class BaseResponse(object):
|
||||
|
||||
def call_on_close(self, func):
|
||||
"""Adds a function to the internal list of functions that should
|
||||
be called as part of closing down the response.
|
||||
be called as part of closing down the response. Since 0.7 this
|
||||
function also returns the function that was passed so that this
|
||||
can be used as a decorator.
|
||||
|
||||
.. versionadded:: 0.6
|
||||
"""
|
||||
self._on_close.append(func)
|
||||
return func
|
||||
|
||||
def __repr__(self):
|
||||
if self.is_sequence:
|
||||
@@ -918,7 +933,8 @@ class BaseResponse(object):
|
||||
encoded and the iterable is buffered.
|
||||
|
||||
:param environ: the WSGI environment of the request.
|
||||
:return: returns a new :class:`Headers` object.
|
||||
:return: returns a new :class:`~werkzeug.datastructures.Headers`
|
||||
object.
|
||||
"""
|
||||
headers = Headers(self.headers)
|
||||
location = None
|
||||
@@ -963,8 +979,10 @@ class BaseResponse(object):
|
||||
# if we can determine the content length automatically, we
|
||||
# should try to do that. But only if this does not involve
|
||||
# flattening the iterator or encoding of unicode strings in
|
||||
# the response.
|
||||
if self.is_sequence and content_length is None:
|
||||
# the response. We however should not do that if we have a 304
|
||||
# response.
|
||||
if self.is_sequence and content_length is None and \
|
||||
self.status_code != 304:
|
||||
try:
|
||||
content_length = sum(len(str(x)) for x in self.response)
|
||||
except UnicodeError:
|
||||
@@ -1027,7 +1045,7 @@ class BaseResponse(object):
|
||||
else:
|
||||
headers = self.get_wsgi_headers(environ)
|
||||
app_iter = self.get_app_iter(environ)
|
||||
return app_iter, self.status, headers.to_list(self.charset)
|
||||
return app_iter, self.status, headers.to_list()
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
"""Process this response as WSGI application.
|
||||
@@ -1043,22 +1061,23 @@ class BaseResponse(object):
|
||||
|
||||
|
||||
class AcceptMixin(object):
|
||||
"""A mixin for classes with an :attr:`~BaseResponse.environ` attribute to
|
||||
get all the HTTP accept headers as :class:`Accept` objects (or subclasses
|
||||
"""A mixin for classes with an :attr:`~BaseResponse.environ` attribute
|
||||
to get all the HTTP accept headers as
|
||||
:class:`~werkzeug.datastructures.Accept` objects (or subclasses
|
||||
thereof).
|
||||
"""
|
||||
|
||||
@cached_property
|
||||
def accept_mimetypes(self):
|
||||
"""List of mimetypes this client supports as :class:`MIMEAccept`
|
||||
object.
|
||||
"""List of mimetypes this client supports as
|
||||
:class:`~werkzeug.datastructures.MIMEAccept` object.
|
||||
"""
|
||||
return parse_accept_header(self.environ.get('HTTP_ACCEPT'), MIMEAccept)
|
||||
|
||||
@cached_property
|
||||
def accept_charsets(self):
|
||||
"""List of charsets this client supports as :class:`CharsetAccept`
|
||||
object.
|
||||
"""List of charsets this client supports as
|
||||
:class:`~werkzeug.datastructures.CharsetAccept` object.
|
||||
"""
|
||||
return parse_accept_header(self.environ.get('HTTP_ACCEPT_CHARSET'),
|
||||
CharsetAccept)
|
||||
@@ -1073,11 +1092,12 @@ class AcceptMixin(object):
|
||||
|
||||
@cached_property
|
||||
def accept_languages(self):
|
||||
"""List of languages this client accepts as :class:`LanguageAccept`
|
||||
object.
|
||||
"""List of languages this client accepts as
|
||||
:class:`~werkzeug.datastructures.LanguageAccept` object.
|
||||
|
||||
.. versionchanged 0.5
|
||||
In previous versions this was a regular :class:`Accept` object.
|
||||
In previous versions this was a regular
|
||||
:class:`~werkzeug.datastructures.Accept` object.
|
||||
"""
|
||||
return parse_accept_header(self.environ.get('HTTP_ACCEPT_LANGUAGE'),
|
||||
LanguageAccept)
|
||||
@@ -1091,8 +1111,8 @@ class ETagRequestMixin(object):
|
||||
|
||||
@cached_property
|
||||
def cache_control(self):
|
||||
"""A :class:`RequestCacheControl` object for the incoming cache control
|
||||
headers.
|
||||
"""A :class:`~werkzeug.datastructures.RequestCacheControl` object
|
||||
for the incoming cache control headers.
|
||||
"""
|
||||
cache_control = self.environ.get('HTTP_CACHE_CONTROL')
|
||||
return parse_cache_control_header(cache_control, None,
|
||||
@@ -1102,8 +1122,7 @@ class ETagRequestMixin(object):
|
||||
def if_match(self):
|
||||
"""An object containing all the etags in the `If-Match` header.
|
||||
|
||||
:rtype: :class:`~ETags`
|
||||
|
||||
:rtype: :class:`~werkzeug.datastructures.ETags`
|
||||
"""
|
||||
return parse_etags(self.environ.get('HTTP_IF_MATCH'))
|
||||
|
||||
@@ -1111,8 +1130,7 @@ class ETagRequestMixin(object):
|
||||
def if_none_match(self):
|
||||
"""An object containing all the etags in the `If-None-Match` header.
|
||||
|
||||
:rtype: :class:`~ETags`
|
||||
|
||||
:rtype: :class:`~werkzeug.datastructures.ETags`
|
||||
"""
|
||||
return parse_etags(self.environ.get('HTTP_IF_NONE_MATCH'))
|
||||
|
||||
@@ -1126,11 +1144,31 @@ class ETagRequestMixin(object):
|
||||
"""The parsed `If-Unmodified-Since` header as datetime object."""
|
||||
return parse_date(self.environ.get('HTTP_IF_UNMODIFIED_SINCE'))
|
||||
|
||||
@cached_property
|
||||
def if_range(self):
|
||||
"""The parsed `If-Range` header.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
|
||||
:rtype: :class:`~werkzeug.datastructures.IfRange`
|
||||
"""
|
||||
return parse_if_range_header(self.environ.get('HTTP_IF_RANGE'))
|
||||
|
||||
@cached_property
|
||||
def range(self):
|
||||
"""The parsed `Range` header.
|
||||
|
||||
.. versionadded:: 0.7
|
||||
|
||||
:rtype: :class:`~werkzeug.datastructures.Range`
|
||||
"""
|
||||
return parse_range_header(self.environ.get('HTTP_RANGE'))
|
||||
|
||||
|
||||
class UserAgentMixin(object):
|
||||
"""Adds a `user_agent` attribute to the request object which contains the
|
||||
parsed user agent of the browser that triggered the request as a
|
||||
:class:`~UserAgent` object.
|
||||
:class:`~werkzeug.useragents.UserAgent` object.
|
||||
"""
|
||||
|
||||
@cached_property
|
||||
@@ -1141,8 +1179,9 @@ class UserAgentMixin(object):
|
||||
|
||||
|
||||
class AuthorizationMixin(object):
|
||||
"""Adds an :attr:`authorization` property that represents the parsed value
|
||||
of the `Authorization` header as :class:`Authorization` object.
|
||||
"""Adds an :attr:`authorization` property that represents the parsed
|
||||
value of the `Authorization` header as
|
||||
:class:`~werkzeug.datastructures.Authorization` object.
|
||||
"""
|
||||
|
||||
@cached_property
|
||||
@@ -1155,7 +1194,8 @@ class AuthorizationMixin(object):
|
||||
class ETagResponseMixin(object):
|
||||
"""Adds extra functionality to a response object for etag and cache
|
||||
handling. This mixin requires an object with at least a `headers`
|
||||
object that implements a dict like interface similar to :class:`Headers`.
|
||||
object that implements a dict like interface similar to
|
||||
:class:`~werkzeug.datastructures.Headers`.
|
||||
|
||||
If you want the :meth:`freeze` method to automatically add an etag, you
|
||||
have to mixin this method before the response base class. The default
|
||||
@@ -1235,6 +1275,44 @@ class ETagResponseMixin(object):
|
||||
self.add_etag()
|
||||
super(ETagResponseMixin, self).freeze()
|
||||
|
||||
accept_ranges = header_property('Accept-Ranges', doc='''
|
||||
The `Accept-Ranges` header. Even though the name would indicate
|
||||
that multiple values are supported, it must be one string token only.
|
||||
|
||||
The values ``'bytes'`` and ``'none'`` are common.
|
||||
|
||||
.. versionadded:: 0.7''')
|
||||
|
||||
def _get_content_range(self):
|
||||
def on_update(rng):
|
||||
if not rng:
|
||||
del self.headers['content-range']
|
||||
else:
|
||||
self.headers['Content-Range'] = rng.to_header()
|
||||
rv = parse_content_range_header(self.headers.get('content-range'),
|
||||
on_update)
|
||||
# always provide a content range object to make the descriptor
|
||||
# more user friendly. It provides an unset() method that can be
|
||||
# used to remove the header quickly.
|
||||
if rv is None:
|
||||
rv = ContentRange(None, None, None, on_update=on_update)
|
||||
return rv
|
||||
def _set_content_range(self, value):
|
||||
if not value:
|
||||
del self.headers['content-range']
|
||||
elif isinstance(value, basestring):
|
||||
self.headers['Content-Range'] = value
|
||||
else:
|
||||
self.headers['Content-Range'] = value.to_header()
|
||||
content_range = property(_get_content_range, _set_content_range, doc='''
|
||||
The `Content-Range` header as
|
||||
:class:`~werkzeug.datastructures.ContentRange` object. Even if the
|
||||
header is not set it wil provide such an object for easier
|
||||
manipulation.
|
||||
|
||||
.. versionadded:: 0.7''')
|
||||
del _get_content_range, _set_content_range
|
||||
|
||||
|
||||
class ResponseStream(object):
|
||||
"""A file descriptor like object used by the :class:`ResponseStreamMixin` to
|
||||
@@ -1469,7 +1547,14 @@ class CommonResponseDescriptorsMixin(object):
|
||||
elif header_set:
|
||||
self.headers[name] = header_set.to_header()
|
||||
return parse_set_header(self.headers.get(name), on_update)
|
||||
return property(fget, doc=doc)
|
||||
def fset(self, value):
|
||||
if not value:
|
||||
del self.headers[name]
|
||||
elif isinstance(value, basestring):
|
||||
self.headers[name] = value
|
||||
else:
|
||||
self.headers[name] = dump_header(value)
|
||||
return property(fget, fset, doc=doc)
|
||||
|
||||
vary = _set_property('Vary', doc='''
|
||||
The Vary field value indicates the set of request-header fields that
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
This module implements WSGI related helpers.
|
||||
|
||||
:copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details.
|
||||
:license: BSD, see LICENSE for more details.
|
||||
"""
|
||||
import os
|
||||
@@ -153,7 +153,7 @@ def peek_path_info(environ):
|
||||
|
||||
|
||||
def extract_path_info(environ_or_baseurl, path_or_url, charset='utf-8',
|
||||
errors='ignore', collapse_http_schemes=True):
|
||||
errors='replace', collapse_http_schemes=True):
|
||||
"""Extracts the path info from the given URL (or WSGI environment) and
|
||||
path. The path info returned is a unicode string, not a bytestring
|
||||
suitable for a WSGI environment. The URLs might also be IRIs.
|
||||
@@ -298,7 +298,7 @@ class SharedDataMiddleware(object):
|
||||
:param app: the application to wrap. If you don't want to wrap an
|
||||
application you can pass it :exc:`NotFound`.
|
||||
:param exports: a dict of exported files and folders.
|
||||
:param diallow: a list of :func:`~fnmatch.fnmatch` rules.
|
||||
:param disallow: a list of :func:`~fnmatch.fnmatch` rules.
|
||||
:param fallback_mimetype: the fallback mimetype for unknown files.
|
||||
:param cache: enable or disable caching headers.
|
||||
:Param cache_timeout: the cache timeout in seconds for the headers.
|
||||
|
||||