From d7f758dff4789d6b5d10cc947bddb4f36c56c6fd Mon Sep 17 00:00:00 2001 From: Ruud Date: Wed, 24 Aug 2011 20:59:59 +0200 Subject: [PATCH] Flask update --- couchpotato/__init__.py | 4 +- couchpotato/api/__init__.py | 4 +- couchpotato/cli.py | 9 +- libs/flask/__init__.py | 15 +- libs/flask/app.py | 881 ++++++++++++++++++++----- libs/flask/blueprints.py | 318 +++++++++ libs/flask/config.py | 22 +- libs/flask/ctx.py | 96 ++- libs/flask/debughelpers.py | 78 +++ libs/flask/globals.py | 2 +- libs/flask/helpers.py | 231 +++++-- libs/flask/logging.py | 3 +- libs/flask/module.py | 222 +------ libs/flask/session.py | 38 +- libs/flask/sessions.py | 177 +++++ libs/flask/signals.py | 1 + libs/flask/templating.py | 84 ++- libs/flask/testing.py | 2 +- libs/flask/views.py | 112 ++++ libs/flask/wrappers.py | 60 +- libs/imdb/locale/generatepot.py | 0 libs/imdb/locale/rebuildmo.py | 0 libs/jinja2/__init__.py | 6 +- libs/jinja2/bccache.py | 40 +- libs/jinja2/compiler.py | 15 +- libs/jinja2/debug.py | 8 +- libs/jinja2/filters.py | 44 +- libs/jinja2/loaders.py | 3 +- libs/jinja2/runtime.py | 2 - libs/jinja2/tests.py | 15 + libs/jinja2/testsuite/filters.py | 11 +- libs/jinja2/testsuite/tests.py | 10 +- libs/sqlalchemy/ext/declarative.py | 0 libs/themoviedb/__init__.py | 0 libs/themoviedb/tmdb.py | 0 libs/transmissionrpc/LICENSE | 0 libs/transmissionrpc/__init__.py | 0 libs/transmissionrpc/client.py | 0 libs/transmissionrpc/constants.py | 0 libs/transmissionrpc/error.py | 0 libs/transmissionrpc/httphandler.py | 0 libs/transmissionrpc/session.py | 0 libs/transmissionrpc/torrent.py | 0 libs/transmissionrpc/utils.py | 0 libs/werkzeug/__init__.py | 24 +- libs/werkzeug/_internal.py | 19 +- libs/werkzeug/contrib/__init__.py | 2 +- libs/werkzeug/contrib/atom.py | 4 +- libs/werkzeug/contrib/cache.py | 127 +++- libs/werkzeug/contrib/fixers.py | 15 +- libs/werkzeug/contrib/iterio.py | 4 +- libs/werkzeug/contrib/jsrouting.py | 39 +- libs/werkzeug/contrib/kickstart.py | 2 +- libs/werkzeug/contrib/limiter.py | 2 +- libs/werkzeug/contrib/lint.py | 2 +- libs/werkzeug/contrib/profiler.py | 2 +- libs/werkzeug/contrib/securecookie.py | 18 +- libs/werkzeug/contrib/sessions.py | 2 +- libs/werkzeug/contrib/testtools.py | 2 +- libs/werkzeug/contrib/wrappers.py | 2 +- libs/werkzeug/datastructures.py | 248 +++++-- libs/werkzeug/debug/__init__.py | 23 +- libs/werkzeug/debug/console.py | 10 +- libs/werkzeug/debug/render.py | 103 --- libs/werkzeug/debug/repr.py | 2 +- libs/werkzeug/debug/shared/console.png | Bin libs/werkzeug/debug/shared/debugger.js | 7 +- libs/werkzeug/debug/shared/less.png | Bin libs/werkzeug/debug/shared/more.png | Bin libs/werkzeug/debug/shared/source.png | Bin libs/werkzeug/debug/shared/style.css | 2 +- libs/werkzeug/debug/tbtools.py | 16 +- libs/werkzeug/debug/utils.py | 20 - libs/werkzeug/exceptions.py | 63 +- libs/werkzeug/formparser.py | 8 +- libs/werkzeug/http.py | 276 +++++++- libs/werkzeug/local.py | 10 +- libs/werkzeug/posixemulation.py | 2 +- libs/werkzeug/routing.py | 414 ++++++++---- libs/werkzeug/script.py | 10 +- libs/werkzeug/security.py | 41 +- libs/werkzeug/serving.py | 64 +- libs/werkzeug/templates.py | 7 +- libs/werkzeug/test.py | 31 +- libs/werkzeug/testapp.py | 8 +- libs/werkzeug/urls.py | 32 +- libs/werkzeug/useragents.py | 4 +- libs/werkzeug/utils.py | 161 ++--- libs/werkzeug/wrappers.py | 217 ++++-- libs/werkzeug/wsgi.py | 6 +- 90 files changed, 3328 insertions(+), 1236 deletions(-) create mode 100644 libs/flask/blueprints.py create mode 100644 libs/flask/debughelpers.py create mode 100644 libs/flask/sessions.py create mode 100644 libs/flask/views.py mode change 100755 => 100644 libs/imdb/locale/generatepot.py mode change 100755 => 100644 libs/imdb/locale/rebuildmo.py mode change 100755 => 100644 libs/sqlalchemy/ext/declarative.py mode change 100755 => 100644 libs/themoviedb/__init__.py mode change 100755 => 100644 libs/themoviedb/tmdb.py mode change 100755 => 100644 libs/transmissionrpc/LICENSE mode change 100755 => 100644 libs/transmissionrpc/__init__.py mode change 100755 => 100644 libs/transmissionrpc/client.py mode change 100755 => 100644 libs/transmissionrpc/constants.py mode change 100755 => 100644 libs/transmissionrpc/error.py mode change 100755 => 100644 libs/transmissionrpc/httphandler.py mode change 100755 => 100644 libs/transmissionrpc/session.py mode change 100755 => 100644 libs/transmissionrpc/torrent.py mode change 100755 => 100644 libs/transmissionrpc/utils.py delete mode 100644 libs/werkzeug/debug/render.py mode change 100755 => 100644 libs/werkzeug/debug/shared/console.png mode change 100755 => 100644 libs/werkzeug/debug/shared/less.png mode change 100755 => 100644 libs/werkzeug/debug/shared/more.png mode change 100755 => 100644 libs/werkzeug/debug/shared/source.png delete mode 100644 libs/werkzeug/debug/utils.py diff --git a/couchpotato/__init__.py b/couchpotato/__init__.py index 92819a54..d4b52540 100644 --- a/couchpotato/__init__.py +++ b/couchpotato/__init__.py @@ -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): diff --git a/couchpotato/api/__init__.py b/couchpotato/api/__init__.py index d0281199..8063d0a6 100644 --- a/couchpotato/api/__init__.py +++ b/couchpotato/api/__init__.py @@ -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) diff --git a/couchpotato/cli.py b/couchpotato/cli.py index 3abc8f5a..2e0552c8 100644 --- a/couchpotato/cli.py +++ b/couchpotato/cli.py @@ -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 + '/', + + # Static path + web.add_url_rule(url_base + '/static/', 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) diff --git a/libs/flask/__init__.py b/libs/flask/__init__.py index ee8508bc..47bf3cab 100644 --- a/libs/flask/__init__.py +++ b/libs/flask/__init__.py @@ -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 diff --git a/libs/flask/app.py b/libs/flask/app.py index 066215be..35577984 100644 --- a/libs/flask/app.py +++ b/libs/flask/app.py @@ -11,33 +11,53 @@ from __future__ import with_statement +import os +import sys from threading import Lock -from datetime import timedelta, datetime +from datetime import timedelta from itertools import chain +from functools import update_wrapper -from jinja2 import Environment - -from werkzeug import ImmutableDict -from werkzeug.routing import Map, Rule +from werkzeug.datastructures import ImmutableDict +from werkzeug.routing import Map, Rule, RequestRedirect from werkzeug.exceptions import HTTPException, InternalServerError, \ - MethodNotAllowed + MethodNotAllowed, BadRequest from .helpers import _PackageBoundObject, url_for, get_flashed_messages, \ - _tojson_filter, _endpoint_from_view_func + locked_cached_property, _tojson_filter, _endpoint_from_view_func, \ + find_package from .wrappers import Request, Response from .config import ConfigAttribute, Config -from .ctx import _RequestContext +from .ctx import RequestContext from .globals import _request_ctx_stack, request -from .session import Session, _NullSession -from .module import _ModuleSetupState -from .templating import _DispatchingJinjaLoader, \ +from .sessions import SecureCookieSessionInterface +from .module import blueprint_is_module +from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor -from .signals import request_started, request_finished, got_request_exception +from .signals import request_started, request_finished, got_request_exception, \ + request_tearing_down # a lock used for logger initialization _logger_lock = Lock() +def setupmethod(f): + """Wraps a method so that it performs a check in debug mode if the + first request was already handled. + """ + def wrapper_func(self, *args, **kwargs): + if self.debug and self._got_first_request: + raise AssertionError('A setup function was called after the ' + 'first request was handled. This usually indicates a bug ' + 'in the application where a module was not imported ' + 'and decorators or other functionality was called too late.\n' + 'To fix this make sure to import all your view modules, ' + 'database models and everything related at a central place ' + 'before the application starts serving requests.') + return f(self, *args, **kwargs) + return update_wrapper(wrapper_func, f) + + class Flask(_PackageBoundObject): """The flask object implements a WSGI application and acts as the central object. It is passed the name of the module or package of the @@ -82,17 +102,36 @@ class Flask(_PackageBoundObject): extension will look for the code in your application that triggered an SQL query in debug mode. If the import name is not properly set up, that debugging information is lost. (For example it would only - pick up SQL queries in `yourapplicaiton.app` and not + pick up SQL queries in `yourapplication.app` and not `yourapplication.views.frontend`) - .. versionadded:: 0.5 - The `static_path` parameter was added. + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. :param import_name: the name of the application package - :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*. + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: the folder with static files that should be served + at `static_url_path`. Defaults to the ``'static'`` + folder in the root path of the application. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to `True` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. """ #: The class that is used for request objects. See :class:`~flask.Request` @@ -103,14 +142,6 @@ class Flask(_PackageBoundObject): #: :class:`~flask.Response` for more information. response_class = Response - #: Path for the static files. If you don't want to use static files - #: you can set this value to `None` in which case no URL rule is added - #: and the development server will no longer serve any static files. - #: - #: This is the default used for application and modules unless a - #: different value is passed to the constructor. - static_path = '/static' - #: The debug flag. Set this to `True` to enable debugging of the #: application. In debug mode the debugger will kick in when an unhandled #: exception ocurrs and the integrated server will automatically reload @@ -120,11 +151,14 @@ class Flask(_PackageBoundObject): #: configuration key. Defaults to `False`. debug = ConfigAttribute('DEBUG') - #: The testing flask. Set this to `True` to enable the test mode of + #: The testing flag. Set this to `True` to enable the test mode of #: Flask extensions (and in the future probably also Flask itself). #: For example this might activate unittest helpers that have an #: additional runtime cost which should not be enabled by default. #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: #: This attribute can also be configured from the config with the #: `TESTING` configuration key. Defaults to `False`. testing = ConfigAttribute('TESTING') @@ -168,6 +202,11 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.4 logger_name = ConfigAttribute('LOGGER_NAME') + #: Enable the deprecated module support? This is active by default + #: in 0.7 but will be changed to False in 0.8. With Flask 1.0 modules + #: will be removed in favor of Blueprints + enable_modules = True + #: The logging format used for the debug logger. This is only used when #: the application is in debug mode, otherwise the attached logging #: handler does the formatting. @@ -190,31 +229,67 @@ class Flask(_PackageBoundObject): 'DEBUG': False, 'TESTING': False, 'PROPAGATE_EXCEPTIONS': None, + 'PRESERVE_CONTEXT_ON_EXCEPTION': None, 'SECRET_KEY': None, 'SESSION_COOKIE_NAME': 'session', 'PERMANENT_SESSION_LIFETIME': timedelta(days=31), 'USE_X_SENDFILE': False, 'LOGGER_NAME': None, 'SERVER_NAME': None, - 'MAX_CONTENT_LENGTH': None + 'MAX_CONTENT_LENGTH': None, + 'TRAP_BAD_REQUEST_ERRORS': False, + 'TRAP_HTTP_EXCEPTIONS': False }) + #: The rule object to use for URL rules created. This is used by + #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. + #: + #: .. versionadded:: 0.7 + url_rule_class = Rule + #: the test client that is used with when `test_client` is used. #: #: .. versionadded:: 0.7 test_client_class = None - def __init__(self, import_name, static_path=None): - _PackageBoundObject.__init__(self, import_name) + #: the session interface to use. By default an instance of + #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. + #: + #: .. versionadded:: 0.8 + session_interface = SecureCookieSessionInterface() + + def __init__(self, import_name, static_path=None, static_url_path=None, + static_folder='static', template_folder='templates', + instance_path=None, instance_relative_config=False): + _PackageBoundObject.__init__(self, import_name, + template_folder=template_folder) if static_path is not None: - self.static_path = static_path + from warnings import warn + warn(DeprecationWarning('static_path is now called ' + 'static_url_path'), stacklevel=2) + static_url_path = static_path + + if static_url_path is not None: + self.static_url_path = static_url_path + if static_folder is not None: + self.static_folder = static_folder + if instance_path is None: + instance_path = self.auto_find_instance_path() + elif not os.path.isabs(instance_path): + raise ValueError('If an instance path is provided it must be ' + 'absolute. A relative path was given instead.') + + #: Holds the path to the instance folder. + #: + #: .. versionadded:: 0.8 + self.instance_path = instance_path #: The configuration dictionary as :class:`Config`. This behaves #: exactly like a regular dictionary but supports additional methods #: to load a config from files. - self.config = Config(self.root_path, self.default_config) + self.config = self.make_config(instance_relative_config) - #: Prepare the deferred setup of the logger. + # Prepare the deferred setup of the logger. self._logger = None self.logger_name = self.import_name @@ -224,32 +299,82 @@ class Flask(_PackageBoundObject): #: To register a view function, use the :meth:`route` decorator. self.view_functions = {} - #: A dictionary of all registered error handlers. The key is - #: be the error code as integer, the value the function that - #: should handle that error. + # support for the now deprecated `error_handlers` attribute. The + # :attr:`error_handler_spec` shall be used now. + self._error_handlers = {} + + #: A dictionary of all registered error handlers. The key is `None` + #: for error handlers active on the application, otherwise the key is + #: the name of the blueprint. Each key points to another dictionary + #: where they key is the status code of the http exception. The + #: special key `None` points to a list of tuples where the first item + #: is the class for the instance check and the second the error handler + #: function. + #: #: To register a error handler, use the :meth:`errorhandler` #: decorator. - self.error_handlers = {} + self.error_handler_spec = {None: self._error_handlers} #: A dictionary with lists of functions that should be called at the #: beginning of the request. The key of the dictionary is the name of - #: the module this function is active for, `None` for all requests. + #: the blueprint this function is active for, `None` for all requests. #: This can for example be used to open database connections or #: getting hold of the currently logged in user. To register a #: function here, use the :meth:`before_request` decorator. self.before_request_funcs = {} + #: A lists of functions that should be called at the beginning of the + #: first request to this instance. To register a function here, use + #: the :meth:`before_first_request` decorator. + #: + #: .. versionadded:: 0.8 + self.before_first_request_funcs = [] + #: A dictionary with lists of functions that should be called after - #: each request. The key of the dictionary is the name of the module + #: each request. The key of the dictionary is the name of the blueprint #: this function is active for, `None` for all requests. This can for #: example be used to open database connections or getting hold of the #: currently logged in user. To register a function here, use the #: :meth:`after_request` decorator. self.after_request_funcs = {} + #: A dictionary with lists of functions that are called after + #: each request, even if an exception has occurred. The key of the + #: dictionary is the name of the blueprint this function is active for, + #: `None` for all requests. These functions are not allowed to modify + #: the request, and their return values are ignored. If an exception + #: occurred while processing the request, it gets passed to each + #: teardown_request function. To register a function here, use the + #: :meth:`teardown_request` decorator. + #: + #: .. versionadded:: 0.7 + self.teardown_request_funcs = {} + + #: A dictionary with lists of functions that can be used as URL + #: value processor functions. Whenever a URL is built these functions + #: are called to modify the dictionary of values in place. The key + #: `None` here is used for application wide + #: callbacks, otherwise the key is the name of the blueprint. + #: Each of these functions has the chance to modify the dictionary + #: + #: .. versionadded:: 0.7 + self.url_value_preprocessors = {} + + #: A dictionary with lists of functions that can be used as URL value + #: preprocessors. The key `None` here is used for application wide + #: callbacks, otherwise the key is the name of the blueprint. + #: Each of these functions has the chance to modify the dictionary + #: of URL values before they are used as the keyword arguments of the + #: view function. For each function registered this one should also + #: provide a :meth:`url_defaults` function that adds the parameters + #: automatically again that were removed that way. + #: + #: .. versionadded:: 0.7 + self.url_default_functions = {} + #: A dictionary with list of functions that are called without argument #: to populate the template context. The key of the dictionary is the - #: name of the module this function is active for, `None` for all + #: name of the blueprint this function is active for, `None` for all #: requests. Each returns a dictionary that the template context is #: updated with. To register a function here, use the #: :meth:`context_processor` decorator. @@ -257,10 +382,12 @@ class Flask(_PackageBoundObject): None: [_default_template_ctx_processor] } - #: all the loaded modules in a dictionary by name. + #: all the attached blueprints in a directory by name. Blueprints + #: can be attached multiple times so this dictionary does not tell + #: you how often they got attached. #: - #: .. versionadded:: 0.5 - self.modules = {} + #: .. versionadded:: 0.7 + self.blueprints = {} #: a place where extensions can store application specific state. For #: example this is where an extension could store database engines and @@ -295,19 +422,48 @@ class Flask(_PackageBoundObject): #: app.url_map.converters['list'] = ListConverter self.url_map = Map() + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + self._before_request_lock = Lock() + # register the static folder for the application. Do that even # if the folder does not exist. First of all it might be created # while the server is running (usually happens during development) # but also because google appengine stores static files somewhere # else when mapped with the .yml file. - self.add_url_rule(self.static_path + '/', - endpoint='static', - view_func=self.send_static_file) + if self.has_static_folder: + self.add_url_rule(self.static_url_path + '/', + endpoint='static', + view_func=self.send_static_file) - #: The Jinja2 environment. It is created from the - #: :attr:`jinja_options`. - self.jinja_env = self.create_jinja_environment() - self.init_jinja_globals() + def _get_error_handlers(self): + from warnings import warn + warn(DeprecationWarning('error_handlers is deprecated, use the ' + 'new error_handler_spec attribute instead.'), stacklevel=1) + return self._error_handlers + def _set_error_handlers(self, value): + self._error_handlers = value + self.error_handler_spec[None] = value + error_handlers = property(_get_error_handlers, _set_error_handlers) + del _get_error_handlers, _set_error_handlers + + @locked_cached_property + def name(self): + """The name of the application. This is usually the import name + with the difference that it's guessed from the run file if the + import name is main. This name is used as a display name when + Flask needs the name of the application. It can be set and overriden + to change the value. + + .. versionadded:: 0.8 + """ + if self.import_name == '__main__': + fn = getattr(sys.modules['__main__'], '__file__', None) + if fn is None: + return 'unknown' + return os.path.splitext(os.path.basename(fn))[0] + return self.import_name @property def propagate_exceptions(self): @@ -321,6 +477,19 @@ class Flask(_PackageBoundObject): return rv return self.testing or self.debug + @property + def preserve_context_on_exception(self): + """Returns the value of the `PRESERVE_CONTEXT_ON_EXCEPTION` + configuration value in case it's set, otherwise a sensible default + is returned. + + .. versionadded:: 0.7 + """ + rv = self.config['PRESERVE_CONTEXT_ON_EXCEPTION'] + if rv is not None: + return rv + return self.debug + @property def logger(self): """A :class:`logging.Logger` object for this application. The @@ -343,29 +512,112 @@ class Flask(_PackageBoundObject): self._logger = rv = create_logger(self) return rv + @locked_cached_property + def jinja_env(self): + """The Jinja2 environment used to load templates.""" + rv = self.create_jinja_environment() + + # Hack to support the init_jinja_globals method which is supported + # until 1.0 but has an API deficiency. + if getattr(self.init_jinja_globals, 'im_func', None) is not \ + Flask.init_jinja_globals.im_func: + from warnings import warn + warn(DeprecationWarning('This flask class uses a customized ' + 'init_jinja_globals() method which is deprecated. ' + 'Move the code from that method into the ' + 'create_jinja_environment() method instead.')) + self.__dict__['jinja_env'] = rv + self.init_jinja_globals() + + return rv + + @property + def got_first_request(self): + """This attribute is set to `True` if the application started + handling the first request. + + .. versionadded:: 0.8 + """ + return self._got_first_request + + def make_config(self, instance_relative=False): + """Used to create the config attribute by the Flask constructor. + The `instance_relative` parameter is passed in from the constructor + of Flask (there named `instance_relative_config`) and indicates if + the config should be relative to the instance path or the root path + of the application. + + .. versionadded:: 0.8 + """ + root_path = self.root_path + if instance_relative: + root_path = self.instance_path + return Config(root_path, self.default_config) + + def auto_find_instance_path(self): + """Tries to locate the instance path if it was not provided to the + constructor of the application class. It will basically calculate + the path to a folder named ``instance`` next to your main file or + the package. + + .. versionadded:: 0.8 + """ + prefix, package_path = find_package(self.import_name) + if prefix is None: + return os.path.join(package_path, 'instance') + return os.path.join(prefix, 'var', self.name + '-instance') + + def open_instance_resource(self, resource, mode='rb'): + """Opens a resource from the application's instance folder + (:attr:`instance_path`). Otherwise works like + :meth:`open_resource`. Instance resources can also be opened for + writing. + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + """ + return open(os.path.join(self.instance_path, resource), mode) + def create_jinja_environment(self): """Creates the Jinja2 environment based on :attr:`jinja_options` - and :meth:`select_jinja_autoescape`. + and :meth:`select_jinja_autoescape`. Since 0.7 this also adds + the Jinja2 globals and filters after initialization. Override + this function to customize the behavior. .. versionadded:: 0.5 """ options = dict(self.jinja_options) if 'autoescape' not in options: options['autoescape'] = self.select_jinja_autoescape - return Environment(loader=_DispatchingJinjaLoader(self), **options) - - def init_jinja_globals(self): - """Called directly after the environment was created to inject - some defaults (like `url_for`, `get_flashed_messages` and the - `tojson` filter. - - .. versionadded:: 0.5 - """ - self.jinja_env.globals.update( + rv = Environment(self, **options) + rv.globals.update( url_for=url_for, get_flashed_messages=get_flashed_messages ) - self.jinja_env.filters['tojson'] = _tojson_filter + rv.filters['tojson'] = _tojson_filter + return rv + + def create_global_jinja_loader(self): + """Creates the loader for the Jinja2 environment. Can be used to + override just the loader and keeping the rest unchanged. It's + discouraged to override this function. Instead one should override + the :meth:`create_jinja_loader` function instead. + + The global loader dispatches between the loaders of the application + and the individual blueprints. + + .. versionadded:: 0.7 + """ + return DispatchingJinjaLoader(self) + + def init_jinja_globals(self): + """Deprecated. Used to initialize the Jinja2 globals. + + .. versionadded:: 0.5 + .. versionchanged:: 0.7 + This method is deprecated with 0.7. Override + :meth:`create_jinja_environment` instead. + """ def select_jinja_autoescape(self, filename): """Returns `True` if autoescaping should be active for the given @@ -389,9 +641,9 @@ class Flask(_PackageBoundObject): to add extra variables. """ funcs = self.template_context_processors[None] - mod = _request_ctx_stack.top.request.module - if mod is not None and mod in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[mod]) + bp = _request_ctx_stack.top.request.blueprint + if bp is not None and bp in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[bp]) orig_ctx = context.copy() for func in funcs: context.update(func()) @@ -424,15 +676,22 @@ class Flask(_PackageBoundObject): to have the server available externally as well. :param port: the port of the webserver :param options: the options to be forwarded to the underlying - Werkzeug server. See :func:`werkzeug.run_simple` - for more information. + Werkzeug server. See + :func:`werkzeug.serving.run_simple` for more + information. """ - from werkzeug import run_simple + from werkzeug.serving import run_simple if 'debug' in options: self.debug = options.pop('debug') options.setdefault('use_reloader', self.debug) options.setdefault('use_debugger', self.debug) - return run_simple(host, port, self, **options) + try: + run_simple(host, port, self, **options) + finally: + # reset the first request information if the development server + # resetted normally. This makes it possible to restart the server + # without reloader and that stuff from an interactive shell. + self._got_first_request = False def test_client(self, use_cookies=True): """Creates a test client for this application. For information @@ -462,45 +721,78 @@ class Flask(_PackageBoundObject): def open_session(self, request): """Creates or opens a new session. Default implementation stores all session data in a signed cookie. This requires that the - :attr:`secret_key` is set. + :attr:`secret_key` is set. Instead of overriding this method + we recommend replacing the :class:`session_interface`. :param request: an instance of :attr:`request_class`. """ - key = self.secret_key - if key is not None: - return Session.load_cookie(request, self.session_cookie_name, - secret_key=key) + return self.session_interface.open_session(self, request) def save_session(self, session, response): """Saves the session if it needs updates. For the default - implementation, check :meth:`open_session`. + implementation, check :meth:`open_session`. Instead of overriding this + method we recommend replacing the :class:`session_interface`. :param session: the session to be saved (a :class:`~werkzeug.contrib.securecookie.SecureCookie` object) :param response: an instance of :attr:`response_class` """ - expires = domain = None - if session.permanent: - expires = datetime.utcnow() + self.permanent_session_lifetime - if self.config['SERVER_NAME'] is not None: - domain = '.' + self.config['SERVER_NAME'] - session.save_cookie(response, self.session_cookie_name, - expires=expires, httponly=True, domain=domain) + return self.session_interface.save_session(self, session, response) + + def make_null_session(self): + """Creates a new instance of a missing session. Instead of overriding + this method we recommend replacing the :class:`session_interface`. + + .. versionadded:: 0.7 + """ + return self.session_interface.make_null_session(self) def register_module(self, module, **options): """Registers a module with this application. The keyword argument of this function are the same as the ones for the constructor of the :class:`Module` class and will override the values of the module if provided. - """ - options.setdefault('url_prefix', module.url_prefix) - options.setdefault('subdomain', module.subdomain) - self.view_functions.update(module.view_functions) - state = _ModuleSetupState(self, **options) - for func in module._register_events: - func(state) + .. versionchanged:: 0.7 + The module system was deprecated in favor for the blueprint + system. + """ + assert blueprint_is_module(module), 'register_module requires ' \ + 'actual module objects. Please upgrade to blueprints though.' + if not self.enable_modules: + raise RuntimeError('Module support was disabled but code ' + 'attempted to register a module named %r' % module) + else: + from warnings import warn + warn(DeprecationWarning('Modules are deprecated. Upgrade to ' + 'using blueprints. Have a look into the documentation for ' + 'more information. If this module was registered by a ' + 'Flask-Extension upgrade the extension or contact the author ' + 'of that extension instead. (Registered %r)' % module), + stacklevel=2) + + self.register_blueprint(module, **options) + + @setupmethod + def register_blueprint(self, blueprint, **options): + """Registers a blueprint on the application. + + .. versionadded:: 0.7 + """ + first_registration = False + if blueprint.name in self.blueprints: + assert self.blueprints[blueprint.name] is blueprint, \ + 'A blueprint\'s name collision ocurred between %r and ' \ + '%r. Both share the same name "%s". Blueprints that ' \ + 'are created on the fly need unique names.' % \ + (blueprint, self.blueprints[blueprint.name], blueprint.name) + else: + self.blueprints[blueprint.name] = blueprint + first_registration = True + blueprint.register(self, options, first_registration) + + @setupmethod def add_url_rule(self, rule, endpoint=None, view_func=None, **options): """Connects a URL rule. Works exactly like the :meth:`route` decorator. If a view_func is provided it will be registered with the @@ -523,6 +815,10 @@ class Flask(_PackageBoundObject): app.view_functions['index'] = index + If a view function is provided some defaults can be specified directly + on the view function. For more information refer to + :ref:`view-func-options`. + .. versionchanged:: 0.2 `view_func` parameter added. @@ -547,12 +843,32 @@ class Flask(_PackageBoundObject): if endpoint is None: endpoint = _endpoint_from_view_func(view_func) options['endpoint'] = endpoint - methods = options.pop('methods', ('GET',)) - provide_automatic_options = False - if 'OPTIONS' not in methods: - methods = tuple(methods) + ('OPTIONS',) - provide_automatic_options = True - rule = Rule(rule, methods=methods, **options) + methods = options.pop('methods', None) + + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only `GET` as default. + if methods is None: + methods = getattr(view_func, 'methods', None) or ('GET',) + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + provide_automatic_options = getattr(view_func, + 'provide_automatic_options', None) + + if provide_automatic_options is None: + if 'OPTIONS' not in methods: + methods = tuple(methods) + ('OPTIONS',) + provide_automatic_options = True + else: + provide_automatic_options = False + + # due to a werkzeug bug we need to make sure that the defaults are + # None if they are an empty dictionary. This should not be necessary + # with Werkzeug 0.7 + options['defaults'] = options.get('defaults') or None + + rule = self.url_rule_class(rule, methods=methods, **options) rule.provide_automatic_options = provide_automatic_options self.url_map.add(rule) if view_func is not None: @@ -630,7 +946,7 @@ class Flask(_PackageBoundObject): return f return decorator - + @setupmethod def endpoint(self, endpoint): """A decorator to register a function as an endpoint. Example:: @@ -646,7 +962,8 @@ class Flask(_PackageBoundObject): return f return decorator - def errorhandler(self, code): + @setupmethod + def errorhandler(self, code_or_exception): """A decorator that is used to register a function give a given error code. Example:: @@ -654,21 +971,62 @@ class Flask(_PackageBoundObject): def page_not_found(error): return 'This page does not exist', 404 + You can also register handlers for arbitrary exceptions:: + + @app.errorhandler(DatabaseError) + def special_exception_handler(error): + return 'Database connection failed', 500 + You can also register a function as error handler without using the :meth:`errorhandler` decorator. The following example is equivalent to the one above:: def page_not_found(error): return 'This page does not exist', 404 - app.error_handlers[404] = page_not_found + app.error_handler_spec[None][404] = page_not_found + + Setting error handlers via assignments to :attr:`error_handler_spec` + however is discouraged as it requires fidling with nested dictionaries + and the special case for arbitrary exception types. + + The first `None` refers to the active blueprint. If the error + handler should be application wide `None` shall be used. + + .. versionadded:: 0.7 + One can now additionally also register custom exception types + that do not necessarily have to be a subclass of the + :class:`~werkzeug.exceptions.HTTPException` class. :param code: the code as integer for the handler """ def decorator(f): - self.error_handlers[code] = f + self._register_error_handler(None, code_or_exception, f) return f return decorator + def register_error_handler(self, code_or_exception, f): + """Alternative error attach function to the :meth:`errorhandler` + decorator that is more straightforward to use for non decorator + usage. + + .. versionadded:: 0.7 + """ + self._register_error_handler(None, code_or_exception, f) + + @setupmethod + def _register_error_handler(self, key, code_or_exception, f): + if isinstance(code_or_exception, HTTPException): + code_or_exception = code_or_exception.code + if isinstance(code_or_exception, (int, long)): + assert code_or_exception != 500 or key is None, \ + 'It is currently not possible to register a 500 internal ' \ + 'server error on a per-blueprint level.' + self.error_handler_spec.setdefault(key, {})[code_or_exception] = f + else: + self.error_handler_spec.setdefault(key, {}).setdefault(None, []) \ + .append((code_or_exception, f)) + + @setupmethod def template_filter(self, name=None): """A decorator that is used to register custom template filter. You can specify a name for the filter, otherwise the function @@ -686,21 +1044,84 @@ class Flask(_PackageBoundObject): return f return decorator + @setupmethod def before_request(self, f): """Registers a function to run before each request.""" self.before_request_funcs.setdefault(None, []).append(f) return f + @setupmethod + def before_first_request(self, f): + """Registers a function to be run before the first request to this + instance of the application. + + .. versionadded:: 0.8 + """ + self.before_first_request_funcs.append(f) + + @setupmethod def after_request(self, f): - """Register a function to be run after each request.""" + """Register a function to be run after each request. Your function + must take one parameter, a :attr:`response_class` object and return + a new response object or the same (see :meth:`process_response`). + + As of Flask 0.7 this function might not be executed at the end of the + request in case an unhandled exception ocurred. + """ self.after_request_funcs.setdefault(None, []).append(f) return f + @setupmethod + def teardown_request(self, f): + """Register a function to be run at the end of each request, + regardless of whether there was an exception or not. These functions + are executed when the request context is popped, even if not an + actual request was performed. + + Example:: + + ctx = app.test_request_context() + ctx.push() + ... + ctx.pop() + + When ``ctx.pop()`` is executed in the above example, the teardown + functions are called just before the request context moves from the + stack of active contexts. This becomes relevant if you are using + such constructs in tests. + + Generally teardown functions must take every necesary step to avoid + that they will fail. If they do execute code that might fail they + will have to surround the execution of these code by try/except + statements and log ocurring errors. + """ + self.teardown_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod def context_processor(self, f): """Registers a template context processor function.""" self.template_context_processors[None].append(f) return f + @setupmethod + def url_value_preprocessor(self, f): + """Registers a function as URL value preprocessor for all view + functions of the application. It's called before the view functions + are called and can modify the url values provided. + """ + self.url_value_preprocessors.setdefault(None, []).append(f) + return f + + @setupmethod + def url_defaults(self, f): + """Callback function for URL defaults for all view functions of the + application. It's called with the endpoint and values and should + update the values passed in place. + """ + self.url_default_functions.setdefault(None, []).append(f) + return f + def handle_http_exception(self, e): """Handles an HTTP exception. By default this will invoke the registered error handlers and fall back to returning the @@ -708,24 +1129,90 @@ class Flask(_PackageBoundObject): .. versionadded: 0.3 """ - handler = self.error_handlers.get(e.code) + handlers = self.error_handler_spec.get(request.blueprint) + if handlers and e.code in handlers: + handler = handlers[e.code] + else: + handler = self.error_handler_spec[None].get(e.code) if handler is None: return e return handler(e) + def trap_http_exception(self, e): + """Checks if an HTTP exception should be trapped or not. By default + this will return `False` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to `True`. It + also returns `True` if ``TRAP_HTTP_EXCEPTIONS`` is set to `True`. + + This is called for all HTTP exceptions raised by a view function. + If it returns `True` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + + .. versionadded:: 0.8 + """ + if self.config['TRAP_HTTP_EXCEPTIONS']: + return True + if self.config['TRAP_BAD_REQUEST_ERRORS']: + return isinstance(e, BadRequest) + return False + + def handle_user_exception(self, e): + """This method is called whenever an exception occurs that should be + handled. A special case are + :class:`~werkzeug.exception.HTTPException`\s which are forwarded by + this function to the :meth:`handle_http_exception` method. This + function will either return a response value or reraise the + exception with the same traceback. + + .. versionadded:: 0.7 + """ + exc_type, exc_value, tb = sys.exc_info() + assert exc_value is e + + # ensure not to trash sys.exc_info() at that point in case someone + # wants the traceback preserved in handle_http_exception. Of course + # we cannot prevent users from trashing it themselves in a custom + # trap_http_exception method so that's their fault then. + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + + blueprint_handlers = () + handlers = self.error_handler_spec.get(request.blueprint) + if handlers is not None: + blueprint_handlers = handlers.get(None, ()) + app_handlers = self.error_handler_spec[None].get(None, ()) + for typecheck, handler in chain(blueprint_handlers, app_handlers): + if isinstance(e, typecheck): + return handler(e) + + raise exc_type, exc_value, tb + def handle_exception(self, e): """Default exception handling that kicks in when an exception - occours that is not catched. In debug mode the exception will + occours that is not caught. In debug mode the exception will be re-raised immediately, otherwise it is logged and the handler for a 500 internal server error is used. If no such handler exists, a default 500 internal server error message is displayed. .. versionadded: 0.3 """ + exc_type, exc_value, tb = sys.exc_info() + got_request_exception.send(self, exception=e) - handler = self.error_handlers.get(500) + handler = self.error_handler_spec[None].get(500) + if self.propagate_exceptions: - raise + # if we want to repropagate the exception, we can attempt to + # raise it with the whole traceback in case we can do that + # (the function was actually called from the except part) + # otherwise, we just raise the error again + if exc_value is e: + raise exc_type, exc_value, tb + else: + raise e + self.logger.exception('Exception on %s [%s]' % ( request.path, request.method @@ -734,26 +1221,79 @@ class Flask(_PackageBoundObject): return InternalServerError() return handler(e) + def raise_routing_exception(self, request): + """Exceptions that are recording during routing are reraised with + this method. During debug we are not reraising redirect requests + for non ``GET``, ``HEAD``, or ``OPTIONS`` requests and we're raising + a different error instead to help debug situations. + + :internal: + """ + if not self.debug \ + or not isinstance(request.routing_exception, RequestRedirect) \ + or request.method in ('GET', 'HEAD', 'OPTIONS'): + raise request.routing_exception + + from .debughelpers import FormDataRoutingRedirect + raise FormDataRoutingRedirect(request) + def dispatch_request(self): """Does the request dispatching. Matches the URL and returns the return value of the view or error handler. This does not have to be a response object. In order to convert the return value to a proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. """ req = _request_ctx_stack.top.request + if req.routing_exception is not None: + self.raise_routing_exception(req) + rule = req.url_rule + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if getattr(rule, 'provide_automatic_options', False) \ + and req.method == 'OPTIONS': + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + return self.view_functions[rule.endpoint](**req.view_args) + + def full_dispatch_request(self): + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + + .. versionadded:: 0.7 + """ + self.try_trigger_before_first_request_functions() try: - if req.routing_exception is not None: - raise req.routing_exception - rule = req.url_rule - # if we provide automatic options for this URL and the - # request came with the OPTIONS method, reply automatically - if getattr(rule, 'provide_automatic_options', False) \ - and req.method == 'OPTIONS': - return self.make_default_options_response() - # otherwise dispatch to the handler for that endpoint - return self.view_functions[rule.endpoint](**req.view_args) - except HTTPException, e: - return self.handle_http_exception(e) + request_started.send(self) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + except Exception, e: + rv = self.handle_user_exception(e) + response = self.make_response(rv) + response = self.process_response(response) + request_finished.send(self, response=response) + return response + + def try_trigger_before_first_request_functions(self): + """Called before each request and will ensure that it triggers + the :attr:`before_first_request_funcs` and only exactly once per + application instance (which means process usually). + + :internal: + """ + if self._got_first_request: + return + with self._before_request_lock: + if self._got_first_request: + return + self._got_first_request = True + for func in self.before_first_request_funcs: + func() def make_default_options_response(self): """This method is called to create the default `OPTIONS` response. @@ -819,17 +1359,41 @@ class Flask(_PackageBoundObject): return self.url_map.bind_to_environ(request.environ, server_name=self.config['SERVER_NAME']) + def inject_url_defaults(self, endpoint, values): + """Injects the URL defaults for the given endpoint directly into + the values dictionary passed. This is used internally and + automatically called on URL building. + + .. versionadded:: 0.7 + """ + funcs = self.url_default_functions.get(None, ()) + if '.' in endpoint: + bp = endpoint.split('.', 1)[0] + funcs = chain(funcs, self.url_default_functions.get(bp, ())) + for func in funcs: + func(endpoint, values) + def preprocess_request(self): """Called before the actual request dispatching and will call every as :meth:`before_request` decorated function. If any of these function returns a value it's handled as if it was the return value from the view and further request handling is stopped. + + This also triggers the :meth:`url_value_processor` functions before + the actualy :meth:`before_request` functions are called. """ + bp = request.blueprint + + funcs = self.url_value_preprocessors.get(None, ()) + if bp is not None and bp in self.url_value_preprocessors: + funcs = chain(funcs, self.url_value_preprocessors[bp]) + for func in funcs: + func(request.endpoint, request.view_args) + funcs = self.before_request_funcs.get(None, ()) - mod = request.module - if mod and mod in self.before_request_funcs: - funcs = chain(funcs, self.before_request_funcs[mod]) + if bp is not None and bp in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[bp]) for func in funcs: rv = func() if rv is not None: @@ -849,23 +1413,41 @@ class Flask(_PackageBoundObject): instance of :attr:`response_class`. """ ctx = _request_ctx_stack.top - mod = ctx.request.module - if not isinstance(ctx.session, _NullSession): + bp = ctx.request.blueprint + if not self.session_interface.is_null_session(ctx.session): self.save_session(ctx.session, response) funcs = () - if mod and mod in self.after_request_funcs: - funcs = reversed(self.after_request_funcs[mod]) + if bp is not None and bp in self.after_request_funcs: + funcs = reversed(self.after_request_funcs[bp]) if None in self.after_request_funcs: funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: response = handler(response) return response + def do_teardown_request(self): + """Called after the actual request dispatching and will + call every as :meth:`teardown_request` decorated function. This is + not actually called by the :class:`Flask` object itself but is always + triggered when the request context is popped. That way we have a + tighter control over certain resources under testing environments. + """ + funcs = reversed(self.teardown_request_funcs.get(None, ())) + bp = request.blueprint + if bp is not None and bp in self.teardown_request_funcs: + funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) + exc = sys.exc_info()[1] + for func in funcs: + rv = func(exc) + if rv is not None: + return rv + request_tearing_down.send(self) + def request_context(self, environ): - """Creates a request context from the given environment and binds - it to the current context. This must be used in combination with - the `with` statement because the request is only bound to the - current context for the duration of the `with` block. + """Creates a :class:`~flask.ctx.RequestContext` from the given + environment and binds it to the current context. This must be used in + combination with the `with` statement because the request is only bound + to the current context for the duration of the `with` block. Example usage:: @@ -883,29 +1465,20 @@ class Flask(_PackageBoundObject): finally: ctx.pop() - The big advantage of this approach is that you can use it without - the try/finally statement in a shell for interactive testing: - - >>> ctx = app.test_request_context() - >>> ctx.bind() - >>> request.path - u'/' - >>> ctx.unbind() - .. versionchanged:: 0.3 Added support for non-with statement usage and `with` statement is now passed the ctx object. :param environ: a WSGI environment """ - return _RequestContext(self, environ) + return RequestContext(self, environ) def test_request_context(self, *args, **kwargs): """Creates a WSGI environment from the given values (see - :func:`werkzeug.create_environ` for more information, this + :func:`werkzeug.test.EnvironBuilder` for more information, this function accepts the same arguments). """ - from werkzeug import create_environ + from werkzeug.test import create_environ environ_overrides = kwargs.setdefault('environ_overrides', {}) if self.config.get('SERVER_NAME'): server_name = self.config.get('SERVER_NAME') @@ -933,11 +1506,11 @@ class Flask(_PackageBoundObject): Then you still have the original application object around and can continue to call methods on it. - .. versionchanged:: 0.4 - The :meth:`after_request` functions are now called even if an - error handler took over request processing. This ensures that - even if an exception happens database have the chance to - properly close the connection. + .. versionchanged:: 0.7 + The behavior of the before and after request callbacks was changed + under error conditions and a new callback was added that will + always execute at the end of the request, independent on if an + error ocurred or not. See :ref:`callbacks-and-errors`. :param environ: a WSGI environment :param start_response: a callable accepting a status code, @@ -946,20 +1519,18 @@ class Flask(_PackageBoundObject): """ with self.request_context(environ): try: - request_started.send(self) - rv = self.preprocess_request() - if rv is None: - rv = self.dispatch_request() - response = self.make_response(rv) + response = self.full_dispatch_request() except Exception, e: response = self.make_response(self.handle_exception(e)) - try: - response = self.process_response(response) - except Exception, e: - response = self.make_response(self.handle_exception(e)) - request_finished.send(self, response=response) return response(environ, start_response) + @property + def modules(self): + from warnings import warn + warn(DeprecationWarning('Flask.modules is deprecated, use ' + 'Flask.blueprints instead'), stacklevel=2) + return self.blueprints + def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response) diff --git a/libs/flask/blueprints.py b/libs/flask/blueprints.py new file mode 100644 index 00000000..075961ab --- /dev/null +++ b/libs/flask/blueprints.py @@ -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 + '/', + 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 diff --git a/libs/flask/config.py b/libs/flask/config.py index aa65f460..06dd02e2 100644 --- a/libs/flask/config.py +++ b/libs/flask/config.py @@ -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 diff --git a/libs/flask/ctx.py b/libs/flask/ctx.py index 1b17086c..0943d10a 100644 --- a/libs/flask/ctx.py +++ b/libs/flask/ctx.py @@ -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() diff --git a/libs/flask/debughelpers.py b/libs/flask/debughelpers.py new file mode 100644 index 00000000..b4f73dd3 --- /dev/null +++ b/libs/flask/debughelpers.py @@ -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 diff --git a/libs/flask/globals.py b/libs/flask/globals.py index 84714105..34099263 100644 --- a/libs/flask/globals.py +++ b/libs/flask/globals.py @@ -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 diff --git a/libs/flask/helpers.py b/libs/flask/helpers.py index ed8a5d5c..a260b03f 100644 --- a/libs/flask/helpers.py +++ b/libs/flask/helpers.py @@ -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 `. @@ -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/') + 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) diff --git a/libs/flask/logging.py b/libs/flask/logging.py index 29caadce..8379ab66 100644 --- a/libs/flask/logging.py +++ b/libs/flask/logging.py @@ -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): diff --git a/libs/flask/module.py b/libs/flask/module.py index 5c914991..61b3cbc4 100644 --- a/libs/flask/module.py +++ b/libs/flask/module.py @@ -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 + '/', - 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' diff --git a/libs/flask/session.py b/libs/flask/session.py index df2d8773..bfe196b0 100644 --- a/libs/flask/session.py +++ b/libs/flask/session.py @@ -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 diff --git a/libs/flask/sessions.py b/libs/flask/sessions.py new file mode 100644 index 00000000..ee006cda --- /dev/null +++ b/libs/flask/sessions.py @@ -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) diff --git a/libs/flask/signals.py b/libs/flask/signals.py index 22447c7c..4eedf68f 100644 --- a/libs/flask/signals.py +++ b/libs/flask/signals.py @@ -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') diff --git a/libs/flask/templating.py b/libs/flask/templating.py index 4db03b75..d38d3824 100644 --- a/libs/flask/templating.py +++ b/libs/flask/templating.py @@ -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): diff --git a/libs/flask/testing.py b/libs/flask/testing.py index 84237336..06a2c016 100644 --- a/libs/flask/testing.py +++ b/libs/flask/testing.py @@ -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 diff --git a/libs/flask/views.py b/libs/flask/views.py new file mode 100644 index 00000000..9a185570 --- /dev/null +++ b/libs/flask/views.py @@ -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/', 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) diff --git a/libs/flask/wrappers.py b/libs/flask/wrappers.py index 4db1e782..aed0a8d0 100644 --- a/libs/flask/wrappers.py +++ b/libs/flask/wrappers.py @@ -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): diff --git a/libs/imdb/locale/generatepot.py b/libs/imdb/locale/generatepot.py old mode 100755 new mode 100644 diff --git a/libs/imdb/locale/rebuildmo.py b/libs/imdb/locale/rebuildmo.py old mode 100755 new mode 100644 diff --git a/libs/jinja2/__init__.py b/libs/jinja2/__init__.py index dec8c27b..5b349d66 100644 --- a/libs/jinja2/__init__.py +++ b/libs/jinja2/__init__.py @@ -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 diff --git a/libs/jinja2/bccache.py b/libs/jinja2/bccache.py index 0f7f566c..0b0ccad1 100644 --- a/libs/jinja2/bccache.py +++ b/libs/jinja2/bccache.py @@ -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): diff --git a/libs/jinja2/compiler.py b/libs/jinja2/compiler.py index d0aadad5..b21cb386 100644 --- a/libs/jinja2/compiler.py +++ b/libs/jinja2/compiler.py @@ -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 diff --git a/libs/jinja2/debug.py b/libs/jinja2/debug.py index f1cc3bc3..2af22223 100644 --- a/libs/jinja2/debug.py +++ b/libs/jinja2/debug.py @@ -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 diff --git a/libs/jinja2/filters.py b/libs/jinja2/filters.py index fe33eb00..1ef47f95 100644 --- a/libs/jinja2/filters.py +++ b/libs/jinja2/filters.py @@ -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)) diff --git a/libs/jinja2/loaders.py b/libs/jinja2/loaders.py index f8420c7a..419a9c8c 100644 --- a/libs/jinja2/loaders.py +++ b/libs/jinja2/loaders.py @@ -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) diff --git a/libs/jinja2/runtime.py b/libs/jinja2/runtime.py index d73483de..a4a47a28 100644 --- a/libs/jinja2/runtime.py +++ b/libs/jinja2/runtime.py @@ -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): diff --git a/libs/jinja2/tests.py b/libs/jinja2/tests.py index d257eca0..50510b0d 100644 --- a/libs/jinja2/tests.py +++ b/libs/jinja2/tests.py @@ -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, diff --git a/libs/jinja2/testsuite/filters.py b/libs/jinja2/testsuite/filters.py index ff04c9ff..aefe7682 100644 --- a/libs/jinja2/testsuite/filters.py +++ b/libs/jinja2/testsuite/filters.py @@ -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): diff --git a/libs/jinja2/testsuite/tests.py b/libs/jinja2/testsuite/tests.py index cd5006fd..3ece7a8f 100644 --- a/libs/jinja2/testsuite/tests.py +++ b/libs/jinja2/testsuite/tests.py @@ -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): diff --git a/libs/sqlalchemy/ext/declarative.py b/libs/sqlalchemy/ext/declarative.py old mode 100755 new mode 100644 diff --git a/libs/themoviedb/__init__.py b/libs/themoviedb/__init__.py old mode 100755 new mode 100644 diff --git a/libs/themoviedb/tmdb.py b/libs/themoviedb/tmdb.py old mode 100755 new mode 100644 diff --git a/libs/transmissionrpc/LICENSE b/libs/transmissionrpc/LICENSE old mode 100755 new mode 100644 diff --git a/libs/transmissionrpc/__init__.py b/libs/transmissionrpc/__init__.py old mode 100755 new mode 100644 diff --git a/libs/transmissionrpc/client.py b/libs/transmissionrpc/client.py old mode 100755 new mode 100644 diff --git a/libs/transmissionrpc/constants.py b/libs/transmissionrpc/constants.py old mode 100755 new mode 100644 diff --git a/libs/transmissionrpc/error.py b/libs/transmissionrpc/error.py old mode 100755 new mode 100644 diff --git a/libs/transmissionrpc/httphandler.py b/libs/transmissionrpc/httphandler.py old mode 100755 new mode 100644 diff --git a/libs/transmissionrpc/session.py b/libs/transmissionrpc/session.py old mode 100755 new mode 100644 diff --git a/libs/transmissionrpc/torrent.py b/libs/transmissionrpc/torrent.py old mode 100755 new mode 100644 diff --git a/libs/transmissionrpc/utils.py b/libs/transmissionrpc/utils.py old mode 100755 new mode 100644 diff --git a/libs/werkzeug/__init__.py b/libs/werkzeug/__init__.py index eff87299..fea2c985 100644 --- a/libs/werkzeug/__init__.py +++ b/libs/werkzeug/__init__.py @@ -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' }) diff --git a/libs/werkzeug/_internal.py b/libs/werkzeug/_internal.py index 5b62e277..f148f146 100644 --- a/libs/werkzeug/_internal.py +++ b/libs/werkzeug/_internal.py @@ -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 [''' -About Werkzeug</> + return [''' +<!DOCTYPE html> +<html> +<head> +<title>About Werkzeug + +

Werkzeug

-

the Swiss Army knife of Python web development. -

%s\n\n\n''' % gyver]
+

the Swiss Army knife of Python web development.

+
%s\n\n\n
+ +''' % gyver] return easteregged diff --git a/libs/werkzeug/contrib/__init__.py b/libs/werkzeug/contrib/__init__.py index 318e4cbe..ffc48c92 100644 --- a/libs/werkzeug/contrib/__init__.py +++ b/libs/werkzeug/contrib/__init__.py @@ -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. """ diff --git a/libs/werkzeug/contrib/atom.py b/libs/werkzeug/contrib/atom.py index 8a688570..7aaa2fbc 100644 --- a/libs/werkzeug/contrib/atom.py +++ b/libs/werkzeug/contrib/atom.py @@ -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]) diff --git a/libs/werkzeug/contrib/cache.py b/libs/werkzeug/contrib/cache.py index b47b6144..613319f9 100644 --- a/libs/werkzeug/contrib/cache.py +++ b/libs/werkzeug/contrib/cache.py @@ -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 diff --git a/libs/werkzeug/contrib/fixers.py b/libs/werkzeug/contrib/fixers.py index 507589a8..07302491 100644 --- a/libs/werkzeug/contrib/fixers.py +++ b/libs/werkzeug/contrib/fixers.py @@ -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 diff --git a/libs/werkzeug/contrib/iterio.py b/libs/werkzeug/contrib/iterio.py index 11a7e741..4bdb5296 100644 --- a/libs/werkzeug/contrib/iterio.py +++ b/libs/werkzeug/contrib/iterio.py @@ -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 diff --git a/libs/werkzeug/contrib/jsrouting.py b/libs/werkzeug/contrib/jsrouting.py index f1da16e2..9b7d0c05 100644 --- a/libs/werkzeug/contrib/jsrouting.py +++ b/libs/werkzeug/contrib/jsrouting.py @@ -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'): diff --git a/libs/werkzeug/contrib/kickstart.py b/libs/werkzeug/contrib/kickstart.py index 453204e1..43c0e7ca 100644 --- a/libs/werkzeug/contrib/kickstart.py +++ b/libs/werkzeug/contrib/kickstart.py @@ -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 diff --git a/libs/werkzeug/contrib/limiter.py b/libs/werkzeug/contrib/limiter.py index 4c1041c8..d0dbcc5c 100644 --- a/libs/werkzeug/contrib/limiter.py +++ b/libs/werkzeug/contrib/limiter.py @@ -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 diff --git a/libs/werkzeug/contrib/lint.py b/libs/werkzeug/contrib/lint.py index c1dde26e..c7adff9b 100644 --- a/libs/werkzeug/contrib/lint.py +++ b/libs/werkzeug/contrib/lint.py @@ -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 diff --git a/libs/werkzeug/contrib/profiler.py b/libs/werkzeug/contrib/profiler.py index 81747071..58d1465f 100644 --- a/libs/werkzeug/contrib/profiler.py +++ b/libs/werkzeug/contrib/profiler.py @@ -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 diff --git a/libs/werkzeug/contrib/securecookie.py b/libs/werkzeug/contrib/securecookie.py index 835b889f..9e6feebd 100644 --- a/libs/werkzeug/contrib/securecookie.py +++ b/libs/werkzeug/contrib/securecookie.py @@ -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): diff --git a/libs/werkzeug/contrib/sessions.py b/libs/werkzeug/contrib/sessions.py index 7824ef2f..b81351ac 100644 --- a/libs/werkzeug/contrib/sessions.py +++ b/libs/werkzeug/contrib/sessions.py @@ -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 diff --git a/libs/werkzeug/contrib/testtools.py b/libs/werkzeug/contrib/testtools.py index e1828400..9bbf76ad 100644 --- a/libs/werkzeug/contrib/testtools.py +++ b/libs/werkzeug/contrib/testtools.py @@ -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 diff --git a/libs/werkzeug/contrib/wrappers.py b/libs/werkzeug/contrib/wrappers.py index 2e66c36b..bd6a2d45 100644 --- a/libs/werkzeug/contrib/wrappers.py +++ b/libs/werkzeug/contrib/wrappers.py @@ -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 diff --git a/libs/werkzeug/datastructures.py b/libs/werkzeug/datastructures.py index 0b76adca..aac2a6c8 100644 --- a/libs/werkzeug/datastructures.py +++ b/libs/werkzeug/datastructures.py @@ -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 , 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 diff --git a/libs/werkzeug/debug/__init__.py b/libs/werkzeug/debug/__init__.py index 86377b4e..db91b74e 100644 --- a/libs/werkzeug/debug/__init__.py +++ b/libs/werkzeug/debug/__init__.py @@ -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: diff --git a/libs/werkzeug/debug/console.py b/libs/werkzeug/debug/console.py index 176bdf9d..aace8c62 100644 --- a/libs/werkzeug/debug/console.py +++ b/libs/werkzeug/debug/console.py @@ -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) diff --git a/libs/werkzeug/debug/render.py b/libs/werkzeug/debug/render.py deleted file mode 100644 index c5d2c974..00000000 --- a/libs/werkzeug/debug/render.py +++ /dev/null @@ -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) diff --git a/libs/werkzeug/debug/repr.py b/libs/werkzeug/debug/repr.py index 69c0eb50..a6a54a90 100644 --- a/libs/werkzeug/debug/repr.py +++ b/libs/werkzeug/debug/repr.py @@ -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 diff --git a/libs/werkzeug/debug/shared/console.png b/libs/werkzeug/debug/shared/console.png old mode 100755 new mode 100644 diff --git a/libs/werkzeug/debug/shared/debugger.js b/libs/werkzeug/debug/shared/debugger.js index 84d28bf0..6468cba5 100644 --- a/libs/werkzeug/debug/shared/debugger.js +++ b/libs/werkzeug/debug/shared/debugger.js @@ -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 = $('
').html(data); $('span.extended', tmp).each(function() { var hidden = $(this).wrap('').hide(); diff --git a/libs/werkzeug/debug/shared/less.png b/libs/werkzeug/debug/shared/less.png old mode 100755 new mode 100644 diff --git a/libs/werkzeug/debug/shared/more.png b/libs/werkzeug/debug/shared/more.png old mode 100755 new mode 100644 diff --git a/libs/werkzeug/debug/shared/source.png b/libs/werkzeug/debug/shared/source.png old mode 100755 new mode 100644 diff --git a/libs/werkzeug/debug/shared/style.css b/libs/werkzeug/debug/shared/style.css index 4bb9ba9c..53a107ea 100644 --- a/libs/werkzeug/debug/shared/style.css +++ b/libs/werkzeug/debug/shared/style.css @@ -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; } diff --git a/libs/werkzeug/debug/tbtools.py b/libs/werkzeug/debug/tbtools.py index c26771f1..4bcba217 100644 --- a/libs/werkzeug/debug/tbtools.py +++ b/libs/werkzeug/debug/tbtools.py @@ -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'''\ @@ -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): diff --git a/libs/werkzeug/debug/utils.py b/libs/werkzeug/debug/utils.py deleted file mode 100644 index 6e479e4b..00000000 --- a/libs/werkzeug/debug/utils.py +++ /dev/null @@ -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) diff --git a/libs/werkzeug/exceptions.py b/libs/werkzeug/exceptions.py index 67faed86..42751035 100644 --- a/libs/werkzeug/exceptions.py +++ b/libs/werkzeug/exceptions.py @@ -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 = ( + '

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 = ( + '

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 = ( + '

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 = ( + '

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 diff --git a/libs/werkzeug/formparser.py b/libs/werkzeug/formparser.py index 8970556a..c87fffdc 100644 --- a/libs/werkzeug/formparser.py +++ b/libs/werkzeug/formparser.py @@ -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 diff --git a/libs/werkzeug/http.py b/libs/werkzeug/http.py index 6ab5abb7..32e03075 100644 --- a/libs/werkzeug/http.py +++ b/libs/werkzeug/http.py @@ -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 diff --git a/libs/werkzeug/local.py b/libs/werkzeug/local.py index 77b9e781..a5046f70 100644 --- a/libs/werkzeug/local.py +++ b/libs/werkzeug/local.py @@ -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 diff --git a/libs/werkzeug/posixemulation.py b/libs/werkzeug/posixemulation.py index 930fdaa4..d131d23a 100644 --- a/libs/werkzeug/posixemulation.py +++ b/libs/werkzeug/posixemulation.py @@ -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 diff --git a/libs/werkzeug/routing.py b/libs/werkzeug/routing.py index 03f795cb..e847e200 100644 --- a/libs/werkzeug/routing.py +++ b/libs/werkzeug/routing.py @@ -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\w+)\s*=\s*)? + (?P + True|False| + \d+.\d+| + \d+.| + \d+| + \w+| + [urUR]?(?P"[^"]*?"|'[^']*') + )\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 \ - '(?/?)' 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 \ + '(?/?)' 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('/') + Rule('/') :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('/') )) diff --git a/libs/werkzeug/script.py b/libs/werkzeug/script.py index 12fdde1f..2411cbd0 100644 --- a/libs/werkzeug/script.py +++ b/libs/werkzeug/script.py @@ -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 diff --git a/libs/werkzeug/security.py b/libs/werkzeug/security.py index be42cec3..ba78b4df 100644 --- a/libs/werkzeug/security.py +++ b/libs/werkzeug/security.py @@ -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) diff --git a/libs/werkzeug/serving.py b/libs/werkzeug/serving.py index ae553cca..fd7286c6 100644 --- a/libs/werkzeug/serving.py +++ b/libs/werkzeug/serving.py @@ -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: diff --git a/libs/werkzeug/templates.py b/libs/werkzeug/templates.py index 836b6890..a9d60e90 100644 --- a/libs/werkzeug/templates.py +++ b/libs/werkzeug/templates.py @@ -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`_. # diff --git a/libs/werkzeug/test.py b/libs/werkzeug/test.py index cdc184bc..62153d41 100644 --- a/libs/werkzeug/test.py +++ b/libs/werkzeug/test.py @@ -5,7 +5,7 @@ This module implements a client to WSGI applications for testing. - :copyright: (c) 2010 by the Werkzeug Team, see AUTHORS for more details. + :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ import sys @@ -23,6 +23,7 @@ from werkzeug._internal import _empty_stream, _get_environ from werkzeug.wrappers import BaseRequest from werkzeug.urls import url_encode, url_fix, iri_to_uri, _unquote from werkzeug.wsgi import get_host, get_current_url, ClosingIterator +from werkzeug.utils import dump_cookie from werkzeug.datastructures import FileMultiDict, MultiDict, \ CombinedMultiDict, Headers, FileStorage @@ -85,7 +86,7 @@ def stream_encode_multipart(values, use_tempfile=True, threshold=1024 * 500, else: if isinstance(value, unicode): value = value.encode(charset) - write('\r\n\r\n' + value) + write('\r\n\r\n' + str(value)) write('\r\n') write('--%s--\r\n' % boundary) @@ -357,7 +358,7 @@ class EnvironBuilder(object): def _get_content_type(self): ct = self.headers.get('Content-Type') if ct is None and not self._input_stream: - if self.method in ('POST', 'PUT'): + if self.method in ('POST', 'PUT', 'PATCH'): if self._files: return 'multipart/form-data' return 'application/x-www-form-urlencoded' @@ -609,6 +610,25 @@ class Client(object): self.redirect_client = None self.allow_subdomain_redirects = allow_subdomain_redirects + def set_cookie(self, server_name, key, value='', max_age=None, + expires=None, path='/', domain=None, secure=None, + httponly=False, charset='utf-8'): + """Sets a cookie in the client's cookie jar. The server name + is required and has to match the one that is also passed to + the open call. + """ + assert self.cookie_jar is not None, 'cookies disabled' + header = dump_cookie(key, value, max_age, expires, path, domain, + secure, httponly, charset) + environ = create_environ(path, base_url='http://' + server_name) + headers = [('Set-Cookie', header)] + self.cookie_jar.extract_wsgi(environ, headers) + + def delete_cookie(self, server_name, key, path='/', domain=None): + """Deletes a cookie in the test client.""" + self.set_cookie(server_name, key, expires=0, max_age=0, + path=path, domain=domain) + def open(self, *args, **kwargs): """Takes the same arguments as the :class:`EnvironBuilder` class with some additions: You can provide a :class:`EnvironBuilder` or a WSGI @@ -714,6 +734,11 @@ class Client(object): kw['method'] = 'GET' return self.open(*args, **kw) + def patch(self, *args, **kw): + """Like open but method is enforced to PATCH.""" + kw['method'] = 'PATCH' + return self.open(*args, **kw) + def post(self, *args, **kw): """Like open but method is enforced to POST.""" kw['method'] = 'POST' diff --git a/libs/werkzeug/testapp.py b/libs/werkzeug/testapp.py index f1dc42b5..ff660af4 100644 --- a/libs/werkzeug/testapp.py +++ b/libs/werkzeug/testapp.py @@ -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('

  • %s [%s]' % ( escape(egg.project_name), - escape(egg.version) + escape(version) )) wsgi_env = [] diff --git a/libs/werkzeug/urls.py b/libs/werkzeug/urls.py index 2bc1cefe..352b6c7a 100644 --- a/libs/werkzeug/urls.py +++ b/libs/werkzeug/urls.py @@ -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) diff --git a/libs/werkzeug/useragents.py b/libs/werkzeug/useragents.py index c9e4c868..fd70b26b 100644 --- a/libs/werkzeug/useragents.py +++ b/libs/werkzeug/useragents.py @@ -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'), diff --git a/libs/werkzeug/utils.py b/libs/werkzeug/utils.py index 5ca801f6..aedd86d4 100644 --- a/libs/werkzeug/utils.py +++ b/libs/werkzeug/utils.py @@ -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 diff --git a/libs/werkzeug/wrappers.py b/libs/werkzeug/wrappers.py index cc2da14a..1692dc47 100644 --- a/libs/werkzeug/wrappers.py +++ b/libs/werkzeug/wrappers.py @@ -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 ````. 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 + ````. 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 ``
    `` that posted to the request had + POST, PUT or PATCH and the ```` 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 `_ 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 diff --git a/libs/werkzeug/wsgi.py b/libs/werkzeug/wsgi.py index 938f3add..730ded77 100644 --- a/libs/werkzeug/wsgi.py +++ b/libs/werkzeug/wsgi.py @@ -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.