Flask update

This commit is contained in:
Ruud
2011-08-24 20:59:59 +02:00
parent 970b400a0c
commit d7f758dff4
90 changed files with 3328 additions and 1236 deletions
+2 -2
View File
@@ -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):
+2 -2
View File
@@ -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)
+5 -4
View File
@@ -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)
+11 -4
View File
@@ -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
+726 -155
View File
File diff suppressed because it is too large Load Diff
+318
View File
@@ -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
+15 -7
View File
@@ -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
+88 -8
View File
@@ -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()
+78
View File
@@ -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
+1 -1
View File
@@ -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
+187 -44
View File
@@ -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)
+2 -1
View File
@@ -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):
+17 -205
View File
@@ -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'
+7 -31
View File
@@ -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
+177
View File
@@ -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)
+1
View File
@@ -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')
+61 -23
View File
@@ -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):
+1 -1
View File
@@ -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
+112
View File
@@ -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)
+55 -5
View File
@@ -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):
Executable → Regular
View File
Executable → Regular
View File
+1 -5
View File
@@ -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
+26 -14
View File
@@ -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):
+6 -9
View File
@@ -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
+7 -1
View File
@@ -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
+28 -16
View File
@@ -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))
+1 -2
View File
@@ -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)
-2
View File
@@ -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):
+15
View File
@@ -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,
+9 -2
View File
@@ -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):
+8 -2
View File
@@ -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):
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
View File
Executable → Regular
View File
View File
Executable → Regular
View File
Executable → Regular
View File
Executable → Regular
View File
+7 -17
View File
@@ -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'
})
+13 -6
View File
@@ -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
+1 -1
View File
@@ -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.
"""
+2 -2
View File
@@ -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])
+108 -19
View File
@@ -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
+8 -7
View File
@@ -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
+2 -2
View File
@@ -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
+20 -19
View File
@@ -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'):
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+3 -15
View File
@@ -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):
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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
+201 -47
View File
@@ -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
+15 -8
View File
@@ -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:
+7 -3
View File
@@ -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)
-103
View File
@@ -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)
+1 -1
View File
@@ -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
View File

Before

Width:  |  Height:  |  Size: 507 B

After

Width:  |  Height:  |  Size: 507 B

+4 -3
View File
@@ -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();
View File

Before

Width:  |  Height:  |  Size: 191 B

After

Width:  |  Height:  |  Size: 191 B

View File

Before

Width:  |  Height:  |  Size: 200 B

After

Width:  |  Height:  |  Size: 200 B

View File

Before

Width:  |  Height:  |  Size: 818 B

After

Width:  |  Height:  |  Size: 818 B

+1 -1
View File
@@ -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; }
+10 -6
View File
@@ -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):
-20
View File
@@ -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)
+62 -1
View File
@@ -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
+4 -4
View File
@@ -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
+254 -22
View File
@@ -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
+2 -8
View File
@@ -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
+1 -1
View File
@@ -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
+270 -144
View File
@@ -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('/')
))
+7 -3
View File
@@ -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
+39 -2
View File
@@ -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)
+48 -16
View File
@@ -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:
+6 -1
View File
@@ -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`_.
#
+28 -3
View File
@@ -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 -2
View File
@@ -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 = []
+17 -15
View File
@@ -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)
+2 -2
View File
@@ -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'),
+51 -110
View File
@@ -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
+151 -66
View File
@@ -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
+3 -3
View File
@@ -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.