From f7d72dd5a91f4bb6fbd891297825c18565b71393 Mon Sep 17 00:00:00 2001 From: Ruud Date: Fri, 6 Jan 2012 17:09:44 +0100 Subject: [PATCH] Flask update --- libs/flask/__init__.py | 4 +- libs/flask/app.py | 176 +++---- libs/flask/blueprints.py | 29 +- libs/flask/config.py | 10 +- libs/flask/ctx.py | 43 +- libs/flask/debughelpers.py | 3 +- libs/flask/ext/__init__.py | 29 ++ libs/flask/exthook.py | 119 +++++ libs/flask/globals.py | 3 +- libs/flask/helpers.py | 27 +- libs/flask/logging.py | 6 +- libs/flask/module.py | 2 +- libs/flask/session.py | 4 +- libs/flask/sessions.py | 36 +- libs/flask/signals.py | 4 +- libs/flask/templating.py | 2 +- libs/flask/testing.py | 117 +++-- libs/flask/views.py | 44 +- libs/flask/wrappers.py | 2 +- libs/jinja2/__init__.py | 0 libs/jinja2/_debugsupport.c | 0 libs/jinja2/_markupsafe/__init__.py | 0 libs/jinja2/_markupsafe/_bundle.py | 0 libs/jinja2/_markupsafe/_constants.py | 0 libs/jinja2/_markupsafe/_native.py | 0 libs/jinja2/_markupsafe/tests.py | 0 libs/jinja2/_stringdefs.py | 0 libs/jinja2/bccache.py | 0 libs/jinja2/compiler.py | 0 libs/jinja2/constants.py | 0 libs/jinja2/debug.py | 2 +- libs/jinja2/defaults.py | 0 libs/jinja2/environment.py | 10 +- libs/jinja2/exceptions.py | 0 libs/jinja2/ext.py | 10 +- libs/jinja2/filters.py | 28 +- libs/jinja2/lexer.py | 8 +- libs/jinja2/loaders.py | 25 +- libs/jinja2/meta.py | 0 libs/jinja2/nodes.py | 0 libs/jinja2/optimizer.py | 0 libs/jinja2/parser.py | 3 +- libs/jinja2/runtime.py | 0 libs/jinja2/sandbox.py | 0 libs/jinja2/tests.py | 0 libs/jinja2/testsuite/__init__.py | 95 ---- libs/jinja2/testsuite/api.py | 245 ---------- libs/jinja2/testsuite/core_tags.py | 285 ----------- libs/jinja2/testsuite/debug.py | 60 --- libs/jinja2/testsuite/doctests.py | 29 -- libs/jinja2/testsuite/ext.py | 455 ------------------ libs/jinja2/testsuite/filters.py | 356 -------------- libs/jinja2/testsuite/imports.py | 141 ------ libs/jinja2/testsuite/inheritance.py | 227 --------- libs/jinja2/testsuite/lexnparse.py | 387 --------------- libs/jinja2/testsuite/loader.py | 190 -------- libs/jinja2/testsuite/regression.py | 255 ---------- libs/jinja2/testsuite/res/__init__.py | 0 .../testsuite/res/templates/broken.html | 3 - .../testsuite/res/templates/foo/test.html | 1 - .../testsuite/res/templates/syntaxerror.html | 4 - libs/jinja2/testsuite/res/templates/test.html | 1 - libs/jinja2/testsuite/security.py | 165 ------- libs/jinja2/testsuite/tests.py | 93 ---- libs/jinja2/testsuite/utils.py | 82 ---- libs/jinja2/utils.py | 0 libs/jinja2/visitor.py | 0 libs/werkzeug/__init__.py | 2 +- libs/werkzeug/_internal.py | 0 libs/werkzeug/contrib/__init__.py | 0 libs/werkzeug/contrib/atom.py | 0 libs/werkzeug/contrib/cache.py | 193 ++++---- libs/werkzeug/contrib/fixers.py | 23 +- libs/werkzeug/contrib/iterio.py | 18 +- libs/werkzeug/contrib/jsrouting.py | 0 libs/werkzeug/contrib/kickstart.py | 0 libs/werkzeug/contrib/limiter.py | 0 libs/werkzeug/contrib/lint.py | 0 libs/werkzeug/contrib/profiler.py | 0 libs/werkzeug/contrib/securecookie.py | 17 +- libs/werkzeug/contrib/sessions.py | 5 +- libs/werkzeug/contrib/testtools.py | 0 libs/werkzeug/contrib/wrappers.py | 0 libs/werkzeug/datastructures.py | 86 +++- libs/werkzeug/debug/__init__.py | 0 libs/werkzeug/debug/console.py | 0 libs/werkzeug/debug/repr.py | 0 libs/werkzeug/debug/shared/FONT_LICENSE | 0 libs/werkzeug/debug/shared/console.png | Bin libs/werkzeug/debug/shared/debugger.js | 0 libs/werkzeug/debug/shared/jquery.js | 0 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 | 0 libs/werkzeug/debug/shared/ubuntu.ttf | Bin libs/werkzeug/debug/tbtools.py | 0 libs/werkzeug/exceptions.py | 15 + libs/werkzeug/formparser.py | 429 ++++++++++------- libs/werkzeug/http.py | 10 +- libs/werkzeug/local.py | 79 ++- libs/werkzeug/posixemulation.py | 0 libs/werkzeug/routing.py | 33 +- libs/werkzeug/script.py | 0 libs/werkzeug/security.py | 37 +- libs/werkzeug/serving.py | 135 ++++-- libs/werkzeug/templates.py | 0 libs/werkzeug/test.py | 114 ++--- libs/werkzeug/testapp.py | 0 libs/werkzeug/urls.py | 88 +++- libs/werkzeug/useragents.py | 0 libs/werkzeug/utils.py | 10 +- libs/werkzeug/wrappers.py | 108 +++-- libs/werkzeug/wsgi.py | 153 ++++-- 114 files changed, 1540 insertions(+), 3835 deletions(-) mode change 100644 => 100755 libs/flask/__init__.py mode change 100644 => 100755 libs/flask/app.py mode change 100644 => 100755 libs/flask/blueprints.py mode change 100644 => 100755 libs/flask/config.py mode change 100644 => 100755 libs/flask/ctx.py mode change 100644 => 100755 libs/flask/debughelpers.py create mode 100755 libs/flask/ext/__init__.py create mode 100755 libs/flask/exthook.py mode change 100644 => 100755 libs/flask/globals.py mode change 100644 => 100755 libs/flask/helpers.py mode change 100644 => 100755 libs/flask/logging.py mode change 100644 => 100755 libs/flask/module.py mode change 100644 => 100755 libs/flask/session.py mode change 100644 => 100755 libs/flask/sessions.py mode change 100644 => 100755 libs/flask/signals.py mode change 100644 => 100755 libs/flask/templating.py mode change 100644 => 100755 libs/flask/testing.py mode change 100644 => 100755 libs/flask/views.py mode change 100644 => 100755 libs/flask/wrappers.py mode change 100644 => 100755 libs/jinja2/__init__.py mode change 100644 => 100755 libs/jinja2/_debugsupport.c mode change 100644 => 100755 libs/jinja2/_markupsafe/__init__.py mode change 100644 => 100755 libs/jinja2/_markupsafe/_bundle.py mode change 100644 => 100755 libs/jinja2/_markupsafe/_constants.py mode change 100644 => 100755 libs/jinja2/_markupsafe/_native.py mode change 100644 => 100755 libs/jinja2/_markupsafe/tests.py mode change 100644 => 100755 libs/jinja2/_stringdefs.py mode change 100644 => 100755 libs/jinja2/bccache.py mode change 100644 => 100755 libs/jinja2/compiler.py mode change 100644 => 100755 libs/jinja2/constants.py mode change 100644 => 100755 libs/jinja2/debug.py mode change 100644 => 100755 libs/jinja2/defaults.py mode change 100644 => 100755 libs/jinja2/environment.py mode change 100644 => 100755 libs/jinja2/exceptions.py mode change 100644 => 100755 libs/jinja2/ext.py mode change 100644 => 100755 libs/jinja2/filters.py mode change 100644 => 100755 libs/jinja2/lexer.py mode change 100644 => 100755 libs/jinja2/loaders.py mode change 100644 => 100755 libs/jinja2/meta.py mode change 100644 => 100755 libs/jinja2/nodes.py mode change 100644 => 100755 libs/jinja2/optimizer.py mode change 100644 => 100755 libs/jinja2/parser.py mode change 100644 => 100755 libs/jinja2/runtime.py mode change 100644 => 100755 libs/jinja2/sandbox.py mode change 100644 => 100755 libs/jinja2/tests.py delete mode 100644 libs/jinja2/testsuite/__init__.py delete mode 100644 libs/jinja2/testsuite/api.py delete mode 100644 libs/jinja2/testsuite/core_tags.py delete mode 100644 libs/jinja2/testsuite/debug.py delete mode 100644 libs/jinja2/testsuite/doctests.py delete mode 100644 libs/jinja2/testsuite/ext.py delete mode 100644 libs/jinja2/testsuite/filters.py delete mode 100644 libs/jinja2/testsuite/imports.py delete mode 100644 libs/jinja2/testsuite/inheritance.py delete mode 100644 libs/jinja2/testsuite/lexnparse.py delete mode 100644 libs/jinja2/testsuite/loader.py delete mode 100644 libs/jinja2/testsuite/regression.py delete mode 100644 libs/jinja2/testsuite/res/__init__.py delete mode 100644 libs/jinja2/testsuite/res/templates/broken.html delete mode 100644 libs/jinja2/testsuite/res/templates/foo/test.html delete mode 100644 libs/jinja2/testsuite/res/templates/syntaxerror.html delete mode 100644 libs/jinja2/testsuite/res/templates/test.html delete mode 100644 libs/jinja2/testsuite/security.py delete mode 100644 libs/jinja2/testsuite/tests.py delete mode 100644 libs/jinja2/testsuite/utils.py mode change 100644 => 100755 libs/jinja2/utils.py mode change 100644 => 100755 libs/jinja2/visitor.py mode change 100644 => 100755 libs/werkzeug/__init__.py mode change 100644 => 100755 libs/werkzeug/_internal.py mode change 100644 => 100755 libs/werkzeug/contrib/__init__.py mode change 100644 => 100755 libs/werkzeug/contrib/atom.py mode change 100644 => 100755 libs/werkzeug/contrib/cache.py mode change 100644 => 100755 libs/werkzeug/contrib/fixers.py mode change 100644 => 100755 libs/werkzeug/contrib/iterio.py mode change 100644 => 100755 libs/werkzeug/contrib/jsrouting.py mode change 100644 => 100755 libs/werkzeug/contrib/kickstart.py mode change 100644 => 100755 libs/werkzeug/contrib/limiter.py mode change 100644 => 100755 libs/werkzeug/contrib/lint.py mode change 100644 => 100755 libs/werkzeug/contrib/profiler.py mode change 100644 => 100755 libs/werkzeug/contrib/securecookie.py mode change 100644 => 100755 libs/werkzeug/contrib/sessions.py mode change 100644 => 100755 libs/werkzeug/contrib/testtools.py mode change 100644 => 100755 libs/werkzeug/contrib/wrappers.py mode change 100644 => 100755 libs/werkzeug/datastructures.py mode change 100644 => 100755 libs/werkzeug/debug/__init__.py mode change 100644 => 100755 libs/werkzeug/debug/console.py mode change 100644 => 100755 libs/werkzeug/debug/repr.py mode change 100644 => 100755 libs/werkzeug/debug/shared/FONT_LICENSE mode change 100644 => 100755 libs/werkzeug/debug/shared/console.png mode change 100644 => 100755 libs/werkzeug/debug/shared/debugger.js mode change 100644 => 100755 libs/werkzeug/debug/shared/jquery.js mode change 100644 => 100755 libs/werkzeug/debug/shared/less.png mode change 100644 => 100755 libs/werkzeug/debug/shared/more.png mode change 100644 => 100755 libs/werkzeug/debug/shared/source.png mode change 100644 => 100755 libs/werkzeug/debug/shared/style.css mode change 100644 => 100755 libs/werkzeug/debug/shared/ubuntu.ttf mode change 100644 => 100755 libs/werkzeug/debug/tbtools.py mode change 100644 => 100755 libs/werkzeug/exceptions.py mode change 100644 => 100755 libs/werkzeug/formparser.py mode change 100644 => 100755 libs/werkzeug/http.py mode change 100644 => 100755 libs/werkzeug/local.py mode change 100644 => 100755 libs/werkzeug/posixemulation.py mode change 100644 => 100755 libs/werkzeug/routing.py mode change 100644 => 100755 libs/werkzeug/script.py mode change 100644 => 100755 libs/werkzeug/security.py mode change 100644 => 100755 libs/werkzeug/serving.py mode change 100644 => 100755 libs/werkzeug/templates.py mode change 100644 => 100755 libs/werkzeug/test.py mode change 100644 => 100755 libs/werkzeug/testapp.py mode change 100644 => 100755 libs/werkzeug/urls.py mode change 100644 => 100755 libs/werkzeug/useragents.py mode change 100644 => 100755 libs/werkzeug/utils.py mode change 100644 => 100755 libs/werkzeug/wrappers.py mode change 100644 => 100755 libs/werkzeug/wsgi.py diff --git a/libs/flask/__init__.py b/libs/flask/__init__.py old mode 100644 new mode 100755 index 47bf3cab..54bfedda --- a/libs/flask/__init__.py +++ b/libs/flask/__init__.py @@ -6,11 +6,11 @@ A microframework based on Werkzeug. It's extensively documented and follows best practice patterns. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ -__version__ = '0.8-dev' +__version__ = '0.9-dev' # utilities we import from Werkzeug and Jinja2 that are unused # in the module but are exported as public interface. diff --git a/libs/flask/app.py b/libs/flask/app.py old mode 100644 new mode 100755 index 35577984..42ffea4d --- a/libs/flask/app.py +++ b/libs/flask/app.py @@ -5,7 +5,7 @@ This module implements the central WSGI application object. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ @@ -41,6 +41,12 @@ from .signals import request_started, request_finished, got_request_exception, \ _logger_lock = Lock() +def _make_timedelta(value): + if not isinstance(value, timedelta): + return timedelta(seconds=value) + return value + + def setupmethod(f): """Wraps a method so that it performs a check in debug mode if the first request was already handled. @@ -184,7 +190,8 @@ class Flask(_PackageBoundObject): #: This attribute can also be configured from the config with the #: `PERMANENT_SESSION_LIFETIME` configuration key. Defaults to #: ``timedelta(days=31)`` - permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME') + permanent_session_lifetime = ConfigAttribute('PERMANENT_SESSION_LIFETIME', + get_converter=_make_timedelta) #: Enable this if you want to use the X-Sendfile feature. Keep in #: mind that the server has to support this. This only affects files @@ -231,11 +238,16 @@ class Flask(_PackageBoundObject): '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, + 'APPLICATION_ROOT': None, + 'SESSION_COOKIE_NAME': 'session', + 'SESSION_COOKIE_DOMAIN': None, + 'SESSION_COOKIE_PATH': None, + 'SESSION_COOKIE_HTTPONLY': True, + 'SESSION_COOKIE_SECURE': False, 'MAX_CONTENT_LENGTH': None, 'TRAP_BAD_REQUEST_ERRORS': False, 'TRAP_HTTP_EXCEPTIONS': False @@ -461,7 +473,7 @@ class Flask(_PackageBoundObject): if self.import_name == '__main__': fn = getattr(sys.modules['__main__'], '__file__', None) if fn is None: - return 'unknown' + return '__main__' return os.path.splitext(os.path.basename(fn))[0] return self.import_name @@ -601,7 +613,7 @@ class Flask(_PackageBoundObject): """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 :meth:`jinja_loader` function instead. The global loader dispatches between the loaders of the application and the individual blueprints. @@ -652,7 +664,7 @@ class Flask(_PackageBoundObject): # existing views. context.update(orig_ctx) - def run(self, host='127.0.0.1', port=5000, **options): + def run(self, host='127.0.0.1', port=5000, debug=None, **options): """Runs the application on a local development server. If the :attr:`debug` flag is set the server will automatically reload for code changes and show a debugger in case an exception happened. @@ -675,14 +687,16 @@ class Flask(_PackageBoundObject): :param host: the hostname to listen on. set this to ``'0.0.0.0'`` to have the server available externally as well. :param port: the port of the webserver + :param debug: if given, enable or disable debug mode. + See :attr:`debug`. :param options: the options to be forwarded to the underlying Werkzeug server. See :func:`werkzeug.serving.run_simple` for more information. """ from werkzeug.serving import run_simple - if 'debug' in options: - self.debug = options.pop('debug') + if debug is not None: + self.debug = bool(debug) options.setdefault('use_reloader', self.debug) options.setdefault('use_debugger', self.debug) try: @@ -705,6 +719,8 @@ class Flask(_PackageBoundObject): rv = c.get('/?vodka=42') assert request.args['vodka'] == '42' + See :class:`~flask.testing.FlaskClient` for more information. + .. versionchanged:: 0.4 added support for `with` block usage for the client. @@ -815,9 +831,11 @@ 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`. + Internally :meth:`route` invokes :meth:`add_url_rule` so if you want + to customize the behavior via subclassing you only need to change + this method. + + For more information refer to :ref:`url-route-registrations`. .. versionchanged:: 0.2 `view_func` parameter added. @@ -876,73 +894,33 @@ class Flask(_PackageBoundObject): def route(self, rule, **options): """A decorator that is used to register a view function for a - given URL rule. Example:: + given URL rule. This does the same thing as :meth:`add_url_rule` + but is intended for decorator usage:: @app.route('/') def index(): return 'Hello World' - Variables parts in the route can be specified with angular - brackets (``/user/``). By default a variable part - in the URL accepts any string without a slash however a different - converter can be specified as well by using ````. - - Variable parts are passed to the view function as keyword - arguments. - - The following converters are possible: - - =========== =========================================== - `int` accepts integers - `float` like `int` but for floating point values - `path` like the default but also accepts slashes - =========== =========================================== - - Here some examples:: - - @app.route('/') - def index(): - pass - - @app.route('/') - def show_user(username): - pass - - @app.route('/post/') - def show_post(post_id): - pass - - An important detail to keep in mind is how Flask deals with trailing - slashes. The idea is to keep each URL unique so the following rules - apply: - - 1. If a rule ends with a slash and is requested without a slash - by the user, the user is automatically redirected to the same - page with a trailing slash attached. - 2. If a rule does not end with a trailing slash and the user request - the page with a trailing slash, a 404 not found is raised. - - This is consistent with how web servers deal with static files. This - also makes it possible to use relative link targets safely. - - The :meth:`route` decorator accepts a couple of other arguments - as well: + For more information refer to :ref:`url-route-registrations`. :param rule: the URL rule as string - :param methods: a list of methods this rule should be limited + :param endpoint: the endpoint for the registered URL rule. Flask + itself assumes the name of the view function as + endpoint + :param view_func: the function to call when serving a request to the + provided endpoint + :param options: the options to be forwarded to the underlying + :class:`~werkzeug.routing.Rule` object. A change + to Werkzeug is handling of method options. methods + is a list of methods this rule should be limited to (`GET`, `POST` etc.). By default a rule just listens for `GET` (and implicitly `HEAD`). Starting with Flask 0.6, `OPTIONS` is implicitly added and handled by the standard request handling. - :param subdomain: specifies the rule for the subdomain in case - subdomain matching is in use. - :param strict_slashes: can be used to disable the strict slashes - setting for this rule. See above. - :param options: other options to be forwarded to the underlying - :class:`~werkzeug.routing.Rule` object. """ def decorator(f): - self.add_url_rule(rule, None, f, **options) + endpoint = options.pop('endpoint', None) + self.add_url_rule(rule, endpoint, f, **options) return f return decorator @@ -1213,14 +1191,24 @@ class Flask(_PackageBoundObject): else: raise e - self.logger.exception('Exception on %s [%s]' % ( - request.path, - request.method - )) + self.log_exception((exc_type, exc_value, tb)) if handler is None: return InternalServerError() return handler(e) + def log_exception(self, exc_info): + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error('Exception on %s [%s]' % ( + request.path, + request.method + ), exc_info=exc_info) + 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 @@ -1302,17 +1290,18 @@ class Flask(_PackageBoundObject): .. versionadded:: 0.7 """ - # This would be nicer in Werkzeug 0.7, which however currently - # is not released. Werkzeug 0.7 provides a method called - # allowed_methods() that returns all methods that are valid for - # a given path. - methods = [] - try: - _request_ctx_stack.top.url_adapter.match(method='--') - except MethodNotAllowed, e: - methods = e.valid_methods - except HTTPException, e: - pass + adapter = _request_ctx_stack.top.url_adapter + if hasattr(adapter, 'allowed_methods'): + methods = adapter.allowed_methods() + else: + # fallback for Werkzeug < 0.7 + methods = [] + try: + adapter.match(method='--') + except MethodNotAllowed, e: + methods = e.valid_methods + except HTTPException, e: + pass rv = self.response_class() rv.allow.update(methods) return rv @@ -1383,7 +1372,7 @@ class Flask(_PackageBoundObject): This also triggers the :meth:`url_value_processor` functions before the actualy :meth:`before_request` functions are called. """ - bp = request.blueprint + bp = _request_ctx_stack.top.request.blueprint funcs = self.url_value_preprocessors.get(None, ()) if bp is not None and bp in self.url_value_preprocessors: @@ -1414,8 +1403,6 @@ class Flask(_PackageBoundObject): """ ctx = _request_ctx_stack.top bp = ctx.request.blueprint - if not self.session_interface.is_null_session(ctx.session): - self.save_session(ctx.session, response) funcs = () if bp is not None and bp in self.after_request_funcs: funcs = reversed(self.after_request_funcs[bp]) @@ -1423,6 +1410,8 @@ class Flask(_PackageBoundObject): funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: response = handler(response) + if not self.session_interface.is_null_session(ctx.session): + self.save_session(ctx.session, response) return response def do_teardown_request(self): @@ -1433,7 +1422,7 @@ class Flask(_PackageBoundObject): tighter control over certain resources under testing environments. """ funcs = reversed(self.teardown_request_funcs.get(None, ())) - bp = request.blueprint + bp = _request_ctx_stack.top.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] @@ -1478,19 +1467,12 @@ class Flask(_PackageBoundObject): :func:`werkzeug.test.EnvironBuilder` for more information, this function accepts the same arguments). """ - 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') - if ':' not in server_name: - http_host, http_port = server_name, '80' - else: - http_host, http_port = server_name.split(':', 1) - - environ_overrides.setdefault('SERVER_NAME', server_name) - environ_overrides.setdefault('HTTP_HOST', server_name) - environ_overrides.setdefault('SERVER_PORT', http_port) - return self.request_context(create_environ(*args, **kwargs)) + from flask.testing import make_test_environ_builder + builder = make_test_environ_builder(self, *args, **kwargs) + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() def wsgi_app(self, environ, start_response): """The actual WSGI application. This is not implemented in diff --git a/libs/flask/blueprints.py b/libs/flask/blueprints.py old mode 100644 new mode 100755 index 075961ab..fa93adcb --- a/libs/flask/blueprints.py +++ b/libs/flask/blueprints.py @@ -59,7 +59,7 @@ class BlueprintSetupState(object): 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): + 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. @@ -73,7 +73,7 @@ class BlueprintSetupState(object): 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) + view_func, defaults = defaults, **options) class Blueprint(_PackageBoundObject): @@ -89,9 +89,9 @@ class Blueprint(_PackageBoundObject): 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): + 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 @@ -128,14 +128,14 @@ class Blueprint(_PackageBoundObject): func(state) return self.record(update_wrapper(wrapper, func)) - def make_setup_state(self, app, options, first_registration=False): + 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): + 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 @@ -146,8 +146,8 @@ class Blueprint(_PackageBoundObject): 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') + view_func = self.send_static_file, + endpoint = 'static') for deferred in self.deferred_functions: deferred(state) @@ -157,14 +157,17 @@ class Blueprint(_PackageBoundObject): :func:`url_for` function is prefixed with the name of the blueprint. """ def decorator(f): - self.add_url_rule(rule, f.__name__, f, **options) + endpoint = options.pop("endpoint", f.__name__) + self.add_url_rule(rule, endpoint, f, **options) return f return decorator - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + 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. """ + if endpoint: + assert '.' not in endpoint, "Blueprint endpoint's should not contain dot's" self.record(lambda s: s.add_url_rule(rule, endpoint, view_func, **options)) @@ -289,8 +292,8 @@ class Blueprint(_PackageBoundObject): 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)) + self.record_once(lambda s: s.app.url_value_preprocessors + .setdefault(None, []).append(f)) return f def app_url_defaults(self, f): diff --git a/libs/flask/config.py b/libs/flask/config.py old mode 100644 new mode 100755 index 06dd02e2..67dbf9b7 --- a/libs/flask/config.py +++ b/libs/flask/config.py @@ -5,7 +5,7 @@ Implements the configuration related objects. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ @@ -21,13 +21,17 @@ from werkzeug.utils import import_string class ConfigAttribute(object): """Makes an attribute forward to the config""" - def __init__(self, name): + def __init__(self, name, get_converter=None): self.__name__ = name + self.get_converter = get_converter def __get__(self, obj, type=None): if obj is None: return self - return obj.config[self.__name__] + rv = obj.config[self.__name__] + if self.get_converter is not None: + rv = self.get_converter(rv) + return rv def __set__(self, obj, value): obj.config[self.__name__] = value diff --git a/libs/flask/ctx.py b/libs/flask/ctx.py old mode 100644 new mode 100755 index 0943d10a..9a72d251 --- a/libs/flask/ctx.py +++ b/libs/flask/ctx.py @@ -5,7 +5,7 @@ Implements the objects required to keep the context. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ @@ -89,9 +89,13 @@ class RequestContext(object): self.flashes = None self.session = None + # indicator if the context was preserved. Next time another context + # is pushed the preserved context is popped. + self.preserved = False + self.match_request() - # Support for deprecated functionality. This is doing away with + # XXX: Support for deprecated functionality. This is doing away with # Flask 1.0 blueprint = self.request.blueprint if blueprint is not None: @@ -114,6 +118,18 @@ class RequestContext(object): def push(self): """Binds the request context to the current context.""" + # If an exception ocurrs in debug mode or if context preservation is + # activated under exception situations exactly one context stays + # on the stack. The rationale is that you want to access that + # information under debug situations. However if someone forgets to + # pop that context again we want to make sure that on the next push + # it's invalidated otherwise we run at risk that something leaks + # memory. This is usually only a problem in testsuite since this + # functionality is not active in production environments. + top = _request_ctx_stack.top + if top is not None and top.preserved: + top.pop() + _request_ctx_stack.push(self) # Open the session at the moment that the request context is @@ -128,8 +144,15 @@ class RequestContext(object): also trigger the execution of functions registered by the :meth:`~flask.Flask.teardown_request` decorator. """ + self.preserved = False self.app.do_teardown_request() - _request_ctx_stack.pop() + rv = _request_ctx_stack.pop() + assert rv is self, 'Popped wrong request context. (%r instead of %r)' \ + % (rv, self) + + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + rv.request.environ['werkzeug.request'] = None def __enter__(self): self.push() @@ -141,6 +164,16 @@ class RequestContext(object): # access the request object in the interactive shell. Furthermore # 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.preserve_context_on_exception): + if self.request.environ.get('flask._preserve_context') or \ + (tb is not None and self.app.preserve_context_on_exception): + self.preserved = True + else: self.pop() + + def __repr__(self): + return '<%s \'%s\' [%s] of %s>' % ( + self.__class__.__name__, + self.request.url, + self.request.method, + self.app.name + ) diff --git a/libs/flask/debughelpers.py b/libs/flask/debughelpers.py old mode 100644 new mode 100755 index b4f73dd3..edf8c111 --- a/libs/flask/debughelpers.py +++ b/libs/flask/debughelpers.py @@ -54,7 +54,8 @@ class FormDataRoutingRedirect(AssertionError): 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) + 'with form data reliably or without user interaction.' % + request.method) buf.append('\n\nNote: this exception is only raised in debug mode') AssertionError.__init__(self, ''.join(buf).encode('utf-8')) diff --git a/libs/flask/ext/__init__.py b/libs/flask/ext/__init__.py new file mode 100755 index 00000000..f29958a1 --- /dev/null +++ b/libs/flask/ext/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" + flask.ext + ~~~~~~~~~ + + Redirect imports for extensions. This module basically makes it possible + for us to transition from flaskext.foo to flask_foo without having to + force all extensions to upgrade at the same time. + + When a user does ``from flask.ext.foo import bar`` it will attempt to + import ``from flask_foo import bar`` first and when that fails it will + try to import ``from flaskext.foo import bar``. + + We're switching from namespace packages because it was just too painful for + everybody involved. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" + + +def setup(): + from ..exthook import ExtensionImporter + importer = ExtensionImporter(['flask_%s', 'flaskext.%s'], __name__) + importer.install() + + +setup() +del setup diff --git a/libs/flask/exthook.py b/libs/flask/exthook.py new file mode 100755 index 00000000..bb1deb29 --- /dev/null +++ b/libs/flask/exthook.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +""" + flask.exthook + ~~~~~~~~~~~~~ + + Redirect imports for extensions. This module basically makes it possible + for us to transition from flaskext.foo to flask_foo without having to + force all extensions to upgrade at the same time. + + When a user does ``from flask.ext.foo import bar`` it will attempt to + import ``from flask_foo import bar`` first and when that fails it will + try to import ``from flaskext.foo import bar``. + + We're switching from namespace packages because it was just too painful for + everybody involved. + + This is used by `flask.ext`. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import sys +import os + + +class ExtensionImporter(object): + """This importer redirects imports from this submodule to other locations. + This makes it possible to transition from the old flaskext.name to the + newer flask_name without people having a hard time. + """ + + def __init__(self, module_choices, wrapper_module): + self.module_choices = module_choices + self.wrapper_module = wrapper_module + self.prefix = wrapper_module + '.' + self.prefix_cutoff = wrapper_module.count('.') + 1 + + def __eq__(self, other): + return self.__class__.__module__ == other.__class__.__module__ and \ + self.__class__.__name__ == other.__class__.__name__ and \ + self.wrapper_module == other.wrapper_module and \ + self.module_choices == other.module_choices + + def __ne__(self, other): + return not self.__eq__(other) + + def install(self): + sys.meta_path[:] = [x for x in sys.meta_path if self != x] + [self] + + def find_module(self, fullname, path=None): + if fullname.startswith(self.prefix): + return self + + def load_module(self, fullname): + if fullname in sys.modules: + return sys.modules[fullname] + modname = fullname.split('.', self.prefix_cutoff)[self.prefix_cutoff] + for path in self.module_choices: + realname = path % modname + try: + __import__(realname) + except ImportError: + exc_type, exc_value, tb = sys.exc_info() + # since we only establish the entry in sys.modules at the + # very this seems to be redundant, but if recursive imports + # happen we will call into the move import a second time. + # On the second invocation we still don't have an entry for + # fullname in sys.modules, but we will end up with the same + # fake module name and that import will succeed since this + # one already has a temporary entry in the modules dict. + # Since this one "succeeded" temporarily that second + # invocation now will have created a fullname entry in + # sys.modules which we have to kill. + sys.modules.pop(fullname, None) + + # If it's an important traceback we reraise it, otherwise + # we swallow it and try the next choice. The skipped frame + # is the one from __import__ above which we don't care about + if self.is_important_traceback(realname, tb): + raise exc_type, exc_value, tb.tb_next + continue + module = sys.modules[fullname] = sys.modules[realname] + if '.' not in modname: + setattr(sys.modules[self.wrapper_module], modname, module) + return module + raise ImportError('No module named %s' % fullname) + + def is_important_traceback(self, important_module, tb): + """Walks a traceback's frames and checks if any of the frames + originated in the given important module. If that is the case then we + were able to import the module itself but apparently something went + wrong when the module was imported. (Eg: import of an import failed). + """ + while tb is not None: + if self.is_important_frame(important_module, tb): + return True + tb = tb.tb_next + return False + + def is_important_frame(self, important_module, tb): + """Checks a single frame if it's important.""" + g = tb.tb_frame.f_globals + if '__name__' not in g: + return False + + module_name = g['__name__'] + + # Python 2.7 Behavior. Modules are cleaned up late so the + # name shows up properly here. Success! + if module_name == important_module: + return True + + # Some python verisons will will clean up modules so early that the + # module name at that point is no longer set. Try guessing from + # the filename then. + filename = os.path.abspath(tb.tb_frame.f_code.co_filename) + test_string = os.path.sep + important_module.replace('.', os.path.sep) + return test_string + '.py' in filename or \ + test_string + os.path.sep + '__init__.py' in filename diff --git a/libs/flask/globals.py b/libs/flask/globals.py old mode 100644 new mode 100755 index 34099263..16580d16 --- a/libs/flask/globals.py +++ b/libs/flask/globals.py @@ -6,7 +6,7 @@ Defines all the global objects that are proxies to the current active context. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ @@ -19,6 +19,7 @@ def _lookup_object(name): raise RuntimeError('working outside of request context') return getattr(top, name) + # context locals _request_ctx_stack = LocalStack() current_app = LocalProxy(partial(_lookup_object, 'app')) diff --git a/libs/flask/helpers.py b/libs/flask/helpers.py old mode 100644 new mode 100755 index a260b03f..7295dc3c --- a/libs/flask/helpers.py +++ b/libs/flask/helpers.py @@ -5,7 +5,7 @@ Implements various helpers. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ @@ -18,6 +18,7 @@ import mimetypes from time import time from zlib import adler32 from threading import RLock +from werkzeug.urls import url_quote # try to load the best simplejson implementation available. If JSON # is not installed, we add a failing class. @@ -55,6 +56,7 @@ def _assert_have_json(): if not json_available: raise RuntimeError('simplejson not installed') + # figure out if simplejson escapes slashes. This behaviour was changed # from one version to another without reason. if not json_available or '\\/' not in json.dumps('/'): @@ -145,6 +147,13 @@ def make_response(*args): response = make_response(render_template('not_found.html'), 404) + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + Internally this function does the following things: - if no arguments are passed, it creates a new response argument @@ -177,9 +186,14 @@ def url_for(endpoint, **values): For more information, head over to the :ref:`Quickstart `. + .. versionadded:: 0.9 + The `_anchor` and `_method` parameters were added. + :param endpoint: the endpoint of the URL (name of the function) :param values: the variable arguments of the URL rule :param _external: if set to `True`, an absolute URL is generated. + :param _anchor: if provided this is added as anchor to the URL. + :param _method: if provided this explicitly specifies an HTTP method. """ ctx = _request_ctx_stack.top blueprint_name = request.blueprint @@ -197,8 +211,14 @@ def url_for(endpoint, **values): elif endpoint.startswith('.'): endpoint = endpoint[1:] external = values.pop('_external', False) + anchor = values.pop('_anchor', None) + method = values.pop('_method', None) ctx.app.inject_url_defaults(endpoint, values) - return ctx.url_adapter.build(endpoint, values, force_external=external) + rv = ctx.url_adapter.build(endpoint, values, method=method, + force_external=external) + if anchor is not None: + rv += '#' + url_quote(anchor) + return rv def get_template_attribute(template_name, attribute): @@ -477,6 +497,8 @@ def get_root_path(import_name): directory = os.path.dirname(sys.modules[import_name].__file__) return os.path.abspath(directory) except AttributeError: + # this is necessary in case we are running from the interactive + # python shell. It will never be used for production code however return os.getcwd() @@ -492,6 +514,7 @@ def find_package(import_name): root_mod = sys.modules[import_name.split('.')[0]] package_path = getattr(root_mod, '__file__', None) if package_path is None: + # support for the interactive python shell package_path = os.getcwd() else: package_path = os.path.abspath(os.path.dirname(package_path)) diff --git a/libs/flask/logging.py b/libs/flask/logging.py old mode 100644 new mode 100755 index 8379ab66..9ad641d1 --- a/libs/flask/logging.py +++ b/libs/flask/logging.py @@ -5,7 +5,7 @@ Implements the logging support for Flask. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ @@ -25,7 +25,9 @@ def create_logger(app): class DebugLogger(Logger): def getEffectiveLevel(x): - return DEBUG if app.debug else Logger.getEffectiveLevel(x) + if x.level == 0 and app.debug: + return DEBUG + return Logger.getEffectiveLevel(x) class DebugHandler(StreamHandler): def emit(x, record): diff --git a/libs/flask/module.py b/libs/flask/module.py old mode 100644 new mode 100755 index 61b3cbc4..1c4f466c --- a/libs/flask/module.py +++ b/libs/flask/module.py @@ -5,7 +5,7 @@ Implements a class that represents module blueprints. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/libs/flask/session.py b/libs/flask/session.py old mode 100644 new mode 100755 index bfe196b0..1a43fdc1 --- a/libs/flask/session.py +++ b/libs/flask/session.py @@ -6,14 +6,14 @@ This module used to flask with the session global so we moved it over to flask.sessions - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ from warnings import warn warn(DeprecationWarning('please use flask.sessions instead')) -from .sessions import * +from .sessions import SecureCookieSession, NullSession Session = SecureCookieSession _NullSession = NullSession diff --git a/libs/flask/sessions.py b/libs/flask/sessions.py old mode 100644 new mode 100755 index ee006cda..2795bb1f --- a/libs/flask/sessions.py +++ b/libs/flask/sessions.py @@ -6,7 +6,7 @@ Implements cookie based sessions based on Werkzeug's secure cookie system. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ @@ -123,10 +123,34 @@ class SessionInterface(object): """Helpful helper method that returns the cookie domain that should be used for the session cookie if session cookies are used. """ + if app.config['SESSION_COOKIE_DOMAIN'] is not None: + return app.config['SESSION_COOKIE_DOMAIN'] 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_cookie_path(self, app): + """Returns the path for which the cookie should be valid. The + default implementation uses the value from the SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's `None`. + """ + return app.config['SESSION_COOKIE_PATH'] or \ + app.config['APPLICATION_ROOT'] or '/' + + def get_cookie_httponly(self, app): + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config['SESSION_COOKIE_HTTPONLY'] + + def get_cookie_secure(self, app): + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config['SESSION_COOKIE_SECURE'] + 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 @@ -169,9 +193,13 @@ class SecureCookieSessionInterface(SessionInterface): def save_session(self, app, session, response): expires = self.get_expiration_time(app, session) domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + httponly = self.get_cookie_httponly(app) + secure = self.get_cookie_secure(app) if session.modified and not session: - response.delete_cookie(app.session_cookie_name, + response.delete_cookie(app.session_cookie_name, path=path, domain=domain) else: - session.save_cookie(response, app.session_cookie_name, - expires=expires, httponly=True, domain=domain) + session.save_cookie(response, app.session_cookie_name, path=path, + expires=expires, httponly=httponly, + secure=secure, domain=domain) diff --git a/libs/flask/signals.py b/libs/flask/signals.py old mode 100644 new mode 100755 index 4eedf68f..eeb763d4 --- a/libs/flask/signals.py +++ b/libs/flask/signals.py @@ -6,7 +6,7 @@ Implements signals based on blinker if available, otherwise falls silently back to a noop - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ signals_available = False @@ -34,7 +34,7 @@ except ImportError: 'not installed.') send = lambda *a, **kw: None connect = disconnect = has_receivers_for = receivers_for = \ - temporarily_connected_to = _fail + temporarily_connected_to = connected_to = _fail del _fail # the namespace for code signals. If you are not flask code, do diff --git a/libs/flask/templating.py b/libs/flask/templating.py old mode 100644 new mode 100755 index d38d3824..90e8772a --- a/libs/flask/templating.py +++ b/libs/flask/templating.py @@ -5,7 +5,7 @@ Implements the bridge to Jinja2. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ import posixpath diff --git a/libs/flask/testing.py b/libs/flask/testing.py old mode 100644 new mode 100755 index 06a2c016..782b40f6 --- a/libs/flask/testing.py +++ b/libs/flask/testing.py @@ -6,62 +6,113 @@ Implements test support helpers. This module is lazily imported and usually not used in production environments. - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + +from contextlib import contextmanager from werkzeug.test import Client, EnvironBuilder from flask import _request_ctx_stack +def make_test_environ_builder(app, path='/', base_url=None, *args, **kwargs): + """Creates a new test builder with some application defaults thrown in.""" + http_host = app.config.get('SERVER_NAME') + app_root = app.config.get('APPLICATION_ROOT') + if base_url is None: + base_url = 'http://%s/' % (http_host or 'localhost') + if app_root: + base_url += app_root.lstrip('/') + return EnvironBuilder(path, base_url, *args, **kwargs) + + class FlaskClient(Client): - """Works like a regular Werkzeug test client but has some - knowledge about how Flask works to defer the cleanup of the - request context stack to the end of a with body when used - in a with statement. + """Works like a regular Werkzeug test client but has some knowledge about + how Flask works to defer the cleanup of the request context stack to the + end of a with body when used in a with statement. For general information + about how to use this class refer to :class:`werkzeug.test.Client`. + + Basic usage is outlined in the :ref:`testing` chapter. """ - preserve_context = context_preserved = False + preserve_context = False + + @contextmanager + def session_transaction(self, *args, **kwargs): + """When used in combination with a with statement this opens a + session transaction. This can be used to modify the session that + the test client uses. Once the with block is left the session is + stored back. + + with client.session_transaction() as session: + session['value'] = 42 + + Internally this is implemented by going through a temporary test + request context and since session handling could depend on + request variables this function accepts the same arguments as + :meth:`~flask.Flask.test_request_context` which are directly + passed through. + """ + if self.cookie_jar is None: + raise RuntimeError('Session transactions only make sense ' + 'with cookies enabled.') + app = self.application + environ_overrides = kwargs.setdefault('environ_overrides', {}) + self.cookie_jar.inject_wsgi(environ_overrides) + outer_reqctx = _request_ctx_stack.top + with app.test_request_context(*args, **kwargs) as c: + sess = app.open_session(c.request) + if sess is None: + raise RuntimeError('Session backend did not open a session. ' + 'Check the configuration') + + # Since we have to open a new request context for the session + # handling we want to make sure that we hide out own context + # from the caller. By pushing the original request context + # (or None) on top of this and popping it we get exactly that + # behavior. It's important to not use the push and pop + # methods of the actual request context object since that would + # mean that cleanup handlers are called + _request_ctx_stack.push(outer_reqctx) + try: + yield sess + finally: + _request_ctx_stack.pop() + + resp = app.response_class() + if not app.session_interface.is_null_session(sess): + app.save_session(sess, resp) + headers = resp.get_wsgi_headers(c.request.environ) + self.cookie_jar.extract_wsgi(c.request.environ, headers) def open(self, *args, **kwargs): - if self.context_preserved: - _request_ctx_stack.pop() - self.context_preserved = False kwargs.setdefault('environ_overrides', {}) \ ['flask._preserve_context'] = self.preserve_context as_tuple = kwargs.pop('as_tuple', False) buffered = kwargs.pop('buffered', False) follow_redirects = kwargs.pop('follow_redirects', False) + builder = make_test_environ_builder(self.application, *args, **kwargs) - builder = EnvironBuilder(*args, **kwargs) - - if self.application.config.get('SERVER_NAME'): - server_name = self.application.config.get('SERVER_NAME') - if ':' not in server_name: - http_host, http_port = server_name, None - else: - http_host, http_port = server_name.split(':', 1) - if builder.base_url == 'http://localhost/': - # Default Generated Base URL - if http_port != None: - builder.host = http_host + ':' + http_port - else: - builder.host = http_host - old = _request_ctx_stack.top - try: - return Client.open(self, builder, - as_tuple=as_tuple, - buffered=buffered, - follow_redirects=follow_redirects) - finally: - self.context_preserved = _request_ctx_stack.top is not old + return Client.open(self, builder, + as_tuple=as_tuple, + buffered=buffered, + follow_redirects=follow_redirects) def __enter__(self): + if self.preserve_context: + raise RuntimeError('Cannot nest client invocations') self.preserve_context = True return self def __exit__(self, exc_type, exc_value, tb): self.preserve_context = False - if self.context_preserved: - _request_ctx_stack.pop() + + # on exit we want to clean up earlier. Normally the request context + # stays preserved until the next request in the same thread comes + # in. See RequestGlobals.push() for the general behavior. + top = _request_ctx_stack.top + if top is not None and top.preserved: + top.pop() diff --git a/libs/flask/views.py b/libs/flask/views.py old mode 100644 new mode 100755 index 9a185570..f11c3ddd --- a/libs/flask/views.py +++ b/libs/flask/views.py @@ -15,7 +15,6 @@ 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 @@ -30,13 +29,41 @@ class View(object): return 'Hello %s!' % name app.add_url_rule('/hello/', view_func=MyView.as_view('myview')) + + When you want to decorate a pluggable view you will have to either do that + when the view function is created (by wrapping the return value of + :meth:`as_view`) or you can use the :attr:`decorators` attribute:: + + class SecretView(View): + methods = ['GET'] + decorators = [superuser_required] + + def dispatch_request(self): + ... + + The decorators stored in the decorators list are applied one after another + when the view function is created. Note that you can *not* use the class + based decorators since those would decorate the view class and not the + generated view function! """ + #: A for which methods this pluggable view can handle. methods = None + #: The canonical way to decorate class based views is to decorate the + #: return value of as_view(). However since this moves parts of the + #: logic from the class declaration to the place where it's hooked + #: into the routing system. + #: + #: You can place one or more decorators in this list and whenever the + #: view function is created the result is automatically decorated. + #: + #: .. versionadded:: 0.8 + decorators = [] + def dispatch_request(self): """Subclasses have to override this method to implement the - actual view functionc ode. This method is called with all + actual view function code. This method is called with all the arguments from the URL rule. """ raise NotImplementedError() @@ -54,6 +81,13 @@ class View(object): def view(*args, **kwargs): self = view.view_class(*class_args, **class_kwargs) return self.dispatch_request(*args, **kwargs) + + if cls.decorators: + view.__name__ = name + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + # 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 @@ -108,5 +142,9 @@ class MethodView(View): def dispatch_request(self, *args, **kwargs): meth = getattr(self, request.method.lower(), None) - assert meth is not None, 'Not implemented method' + # if the request method is HEAD and we don't have a handler for it + # retry with GET + if meth is None and request.method == 'HEAD': + meth = getattr(self, 'get', None) + assert meth is not None, 'Not implemented method %r' % request.method return meth(*args, **kwargs) diff --git a/libs/flask/wrappers.py b/libs/flask/wrappers.py old mode 100644 new mode 100755 index aed0a8d0..f6ec2788 --- a/libs/flask/wrappers.py +++ b/libs/flask/wrappers.py @@ -5,7 +5,7 @@ Implements the WSGI wrappers (request and response). - :copyright: (c) 2010 by Armin Ronacher. + :copyright: (c) 2011 by Armin Ronacher. :license: BSD, see LICENSE for more details. """ diff --git a/libs/jinja2/__init__.py b/libs/jinja2/__init__.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/_debugsupport.c b/libs/jinja2/_debugsupport.c old mode 100644 new mode 100755 diff --git a/libs/jinja2/_markupsafe/__init__.py b/libs/jinja2/_markupsafe/__init__.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/_markupsafe/_bundle.py b/libs/jinja2/_markupsafe/_bundle.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/_markupsafe/_constants.py b/libs/jinja2/_markupsafe/_constants.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/_markupsafe/_native.py b/libs/jinja2/_markupsafe/_native.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/_markupsafe/tests.py b/libs/jinja2/_markupsafe/tests.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/_stringdefs.py b/libs/jinja2/_stringdefs.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/bccache.py b/libs/jinja2/bccache.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/compiler.py b/libs/jinja2/compiler.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/constants.py b/libs/jinja2/constants.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/debug.py b/libs/jinja2/debug.py old mode 100644 new mode 100755 index 2af22223..3ac4041a --- a/libs/jinja2/debug.py +++ b/libs/jinja2/debug.py @@ -77,7 +77,7 @@ def make_frame_proxy(frame): class ProcessedTraceback(object): - """Holds a Jinja preprocessed traceback for priting or reraising.""" + """Holds a Jinja preprocessed traceback for printing or reraising.""" def __init__(self, exc_type, exc_value, frames): assert frames, 'no frames for this traceback?' diff --git a/libs/jinja2/defaults.py b/libs/jinja2/defaults.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/environment.py b/libs/jinja2/environment.py old mode 100644 new mode 100755 index 7a9a59fc..ebb54548 --- a/libs/jinja2/environment.py +++ b/libs/jinja2/environment.py @@ -67,7 +67,7 @@ def copy_cache(cache): def load_extensions(environment, extensions): """Load the extensions from the list and bind it to the environment. - Returns a dict of instanciated environments. + Returns a dict of instantiated environments. """ result = {} for extension in extensions: @@ -239,7 +239,7 @@ class Environment(object): # passed by keyword rather than position. However it's important to # not change the order of arguments because it's used at least # internally in those cases: - # - spontaneus environments (i18n extension and Template) + # - spontaneous environments (i18n extension and Template) # - unittests # If parameter changes are required only add parameters at the end # and don't change the arguments (or the defaults!) of the arguments @@ -435,7 +435,7 @@ class Environment(object): return stream def _generate(self, source, name, filename, defer_init=False): - """Internal hook that can be overriden to hook a different generate + """Internal hook that can be overridden to hook a different generate method in. .. versionadded:: 2.5 @@ -443,7 +443,7 @@ class Environment(object): return generate(source, self, name, filename, defer_init=defer_init) def _compile(self, source, filename): - """Internal hook that can be overriden to hook a different compile + """Internal hook that can be overridden to hook a different compile method in. .. versionadded:: 2.5 @@ -1053,7 +1053,7 @@ class TemplateStream(object): def dump(self, fp, encoding=None, errors='strict'): """Dump the complete stream into a file or file-like object. Per default unicode strings are written, if you want to encode - before writing specifiy an `encoding`. + before writing specify an `encoding`. Example usage:: diff --git a/libs/jinja2/exceptions.py b/libs/jinja2/exceptions.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/ext.py b/libs/jinja2/ext.py old mode 100644 new mode 100755 index 5ba6efdb..206756fe --- a/libs/jinja2/ext.py +++ b/libs/jinja2/ext.py @@ -552,6 +552,10 @@ def babel_extract(fileobj, keywords, comment_tags, options): The `newstyle_gettext` flag can be set to `True` to enable newstyle gettext calls. + .. versionchanged:: 2.7 + A `silent` option can now be provided. If set to `False` template + syntax errors are propagated instead of being ignored. + :param fileobj: the file-like object the messages should be extracted from :param keywords: a list of keywords (i.e. function names) that should be recognized as translation functions @@ -571,8 +575,10 @@ def babel_extract(fileobj, keywords, comment_tags, options): extensions.add(InternationalizationExtension) def getbool(options, key, default=False): - options.get(key, str(default)).lower() in ('1', 'on', 'yes', 'true') + return options.get(key, str(default)).lower() in \ + ('1', 'on', 'yes', 'true') + silent = getbool(options, 'silent', True) environment = Environment( options.get('block_start_string', BLOCK_START_STRING), options.get('block_end_string', BLOCK_END_STRING), @@ -596,6 +602,8 @@ def babel_extract(fileobj, keywords, comment_tags, options): node = environment.parse(source) tokens = list(environment.lex(environment.preprocess(source))) except TemplateSyntaxError, e: + if not silent: + raise # skip templates with syntax errors return diff --git a/libs/jinja2/filters.py b/libs/jinja2/filters.py old mode 100644 new mode 100755 index 1ef47f95..352b1668 --- a/libs/jinja2/filters.py +++ b/libs/jinja2/filters.py @@ -15,7 +15,7 @@ from operator import itemgetter from itertools import imap, groupby from jinja2.utils import Markup, escape, pformat, urlize, soft_unicode from jinja2.runtime import Undefined -from jinja2.exceptions import FilterArgumentError, SecurityError +from jinja2.exceptions import FilterArgumentError _word_re = re.compile(r'\w+(?u)') @@ -346,25 +346,25 @@ def do_filesizeformat(value, binary=False): bytes = float(value) base = binary and 1024 or 1000 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") + (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" + return '1 Byte' elif bytes < base: - return "%d Bytes" % bytes + return '%d Bytes' % bytes else: for i, prefix in enumerate(prefixes): - unit = base * base ** (i + 1) + unit = base ** (i + 2) if bytes < unit: - return "%.1f %s" % ((bytes / unit), prefix) - return "%.1f %s" % ((bytes / unit), prefix) + return '%.1f %s' % ((base * bytes / unit), prefix) + return '%.1f %s' % ((base * bytes / unit), prefix) def do_pprint(value, verbose=False): diff --git a/libs/jinja2/lexer.py b/libs/jinja2/lexer.py old mode 100644 new mode 100755 index 0d3f6961..69865d08 --- a/libs/jinja2/lexer.py +++ b/libs/jinja2/lexer.py @@ -414,7 +414,7 @@ class Lexer(object): (operator_re, TOKEN_OPERATOR, None) ] - # assamble the root lexing rule. because "|" is ungreedy + # assemble the root lexing rule. because "|" is ungreedy # we have to sort by length so that the lexer continues working # as expected when we have parsing rules like <% for block and # <%= for variables. (if someone wants asp like syntax) @@ -491,7 +491,7 @@ class Lexer(object): } def _normalize_newlines(self, value): - """Called for strings and template data to normlize it to unicode.""" + """Called for strings and template data to normalize it to unicode.""" return newline_re.sub(self.newline_sequence, value) def tokenize(self, source, name=None, filename=None, state=None): @@ -571,7 +571,7 @@ class Lexer(object): if m is None: continue - # we only match blocks and variables if brances / parentheses + # we only match blocks and variables if braces / parentheses # are balanced. continue parsing with the lower rule which # is the operator rule. do this only if the end tags look # like operators @@ -669,7 +669,7 @@ class Lexer(object): # publish new function and start again pos = pos2 break - # if loop terminated without break we havn't found a single match + # if loop terminated without break we haven't found a single match # either we are at the end of the file or we have a problem else: # end of text diff --git a/libs/jinja2/loaders.py b/libs/jinja2/loaders.py old mode 100644 new mode 100755 index 419a9c8c..c90bbe72 --- a/libs/jinja2/loaders.py +++ b/libs/jinja2/loaders.py @@ -330,12 +330,16 @@ class PrefixLoader(BaseLoader): self.mapping = mapping self.delimiter = delimiter - def get_source(self, environment, template): + def get_loader(self, template): try: prefix, name = template.split(self.delimiter, 1) loader = self.mapping[prefix] except (ValueError, KeyError): raise TemplateNotFound(template) + return loader, name + + def get_source(self, environment, template): + loader, name = self.get_loader(template) try: return loader.get_source(environment, name) except TemplateNotFound: @@ -343,6 +347,16 @@ class PrefixLoader(BaseLoader): # (the one that includes the prefix) raise TemplateNotFound(template) + @internalcode + def load(self, environment, name, globals=None): + loader, local_name = self.get_loader(name) + try: + return loader.load(environment, local_name) + except TemplateNotFound: + # re-raise the exception with the correct fileame here. + # (the one that includes the prefix) + raise TemplateNotFound(name) + def list_templates(self): result = [] for prefix, loader in self.mapping.iteritems(): @@ -376,6 +390,15 @@ class ChoiceLoader(BaseLoader): pass raise TemplateNotFound(template) + @internalcode + def load(self, environment, name, globals=None): + for loader in self.loaders: + try: + return loader.load(environment, name, globals) + except TemplateNotFound: + pass + raise TemplateNotFound(name) + def list_templates(self): found = set() for loader in self.loaders: diff --git a/libs/jinja2/meta.py b/libs/jinja2/meta.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/nodes.py b/libs/jinja2/nodes.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/optimizer.py b/libs/jinja2/optimizer.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/parser.py b/libs/jinja2/parser.py old mode 100644 new mode 100755 index d44229ad..21253389 --- a/libs/jinja2/parser.py +++ b/libs/jinja2/parser.py @@ -223,7 +223,7 @@ class Parser(object): # raise a nicer error message in that case. if self.stream.current.type == 'sub': self.fail('Block names in Jinja have to be valid Python ' - 'identifiers and may not contain hypens, use an ' + 'identifiers and may not contain hyphens, use an ' 'underscore instead.') node.body = self.parse_statements(('name:endblock',), drop_needle=True) @@ -698,7 +698,6 @@ class Parser(object): arg = nodes.Const(attr_token.value, lineno=attr_token.lineno) return nodes.Getitem(node, arg, 'load', lineno=token.lineno) if token.type == 'lbracket': - priority_on_attribute = False args = [] while self.stream.current.type != 'rbracket': if args: diff --git a/libs/jinja2/runtime.py b/libs/jinja2/runtime.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/sandbox.py b/libs/jinja2/sandbox.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/tests.py b/libs/jinja2/tests.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/testsuite/__init__.py b/libs/jinja2/testsuite/__init__.py deleted file mode 100644 index 1f10ef68..00000000 --- a/libs/jinja2/testsuite/__init__.py +++ /dev/null @@ -1,95 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite - ~~~~~~~~~~~~~~~~ - - All the unittests of Jinja2. These tests can be executed by - either running run-tests.py using multiple Python versions at - the same time. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import os -import re -import sys -import unittest -from traceback import format_exception -from jinja2 import loaders - - -here = os.path.dirname(os.path.abspath(__file__)) - -dict_loader = loaders.DictLoader({ - 'justdict.html': 'FOO' -}) -package_loader = loaders.PackageLoader('jinja2.testsuite.res', 'templates') -filesystem_loader = loaders.FileSystemLoader(here + '/res/templates') -function_loader = loaders.FunctionLoader({'justfunction.html': 'FOO'}.get) -choice_loader = loaders.ChoiceLoader([dict_loader, package_loader]) -prefix_loader = loaders.PrefixLoader({ - 'a': filesystem_loader, - 'b': dict_loader -}) - - -class JinjaTestCase(unittest.TestCase): - - ### use only these methods for testing. If you need standard - ### unittest method, wrap them! - - def setup(self): - pass - - def teardown(self): - pass - - def setUp(self): - self.setup() - - def tearDown(self): - self.teardown() - - def assert_equal(self, a, b): - return self.assertEqual(a, b) - - def assert_raises(self, *args, **kwargs): - return self.assertRaises(*args, **kwargs) - - def assert_traceback_matches(self, callback, expected_tb): - try: - callback() - except Exception, e: - tb = format_exception(*sys.exc_info()) - if re.search(expected_tb.strip(), ''.join(tb)) is None: - raise self.fail('Traceback did not match:\n\n%s\nexpected:\n%s' - % (''.join(tb), expected_tb)) - else: - self.fail('Expected exception') - - -def suite(): - from jinja2.testsuite import ext, filters, tests, core_tags, \ - loader, inheritance, imports, lexnparse, security, api, \ - regression, debug, utils, doctests - suite = unittest.TestSuite() - suite.addTest(ext.suite()) - suite.addTest(filters.suite()) - suite.addTest(tests.suite()) - suite.addTest(core_tags.suite()) - suite.addTest(loader.suite()) - suite.addTest(inheritance.suite()) - suite.addTest(imports.suite()) - suite.addTest(lexnparse.suite()) - suite.addTest(security.suite()) - suite.addTest(api.suite()) - suite.addTest(regression.suite()) - suite.addTest(debug.suite()) - suite.addTest(utils.suite()) - - # doctests will not run on python 3 currently. Too many issues - # with that, do not test that on that platform. - if sys.version_info < (3, 0): - suite.addTest(doctests.suite()) - - return suite diff --git a/libs/jinja2/testsuite/api.py b/libs/jinja2/testsuite/api.py deleted file mode 100644 index c8f96347..00000000 --- a/libs/jinja2/testsuite/api.py +++ /dev/null @@ -1,245 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.api - ~~~~~~~~~~~~~~~~~~~~ - - Tests the public API and related stuff. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import unittest - -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Environment, Undefined, DebugUndefined, \ - StrictUndefined, UndefinedError, meta, \ - is_undefined, Template, DictLoader -from jinja2.utils import Cycler - -env = Environment() - - -class ExtendedAPITestCase(JinjaTestCase): - - def test_item_and_attribute(self): - from jinja2.sandbox import SandboxedEnvironment - - for env in Environment(), SandboxedEnvironment(): - # the |list is necessary for python3 - tmpl = env.from_string('{{ foo.items()|list }}') - assert tmpl.render(foo={'items': 42}) == "[('items', 42)]" - tmpl = env.from_string('{{ foo|attr("items")()|list }}') - assert tmpl.render(foo={'items': 42}) == "[('items', 42)]" - tmpl = env.from_string('{{ foo["items"] }}') - assert tmpl.render(foo={'items': 42}) == '42' - - def test_finalizer(self): - def finalize_none_empty(value): - if value is None: - value = u'' - return value - env = Environment(finalize=finalize_none_empty) - tmpl = env.from_string('{% for item in seq %}|{{ item }}{% endfor %}') - assert tmpl.render(seq=(None, 1, "foo")) == '||1|foo' - tmpl = env.from_string('<{{ none }}>') - assert tmpl.render() == '<>' - - def test_cycler(self): - items = 1, 2, 3 - c = Cycler(*items) - for item in items + items: - assert c.current == item - assert c.next() == item - c.next() - assert c.current == 2 - c.reset() - assert c.current == 1 - - def test_expressions(self): - expr = env.compile_expression("foo") - assert expr() is None - assert expr(foo=42) == 42 - expr2 = env.compile_expression("foo", undefined_to_none=False) - assert is_undefined(expr2()) - - expr = env.compile_expression("42 + foo") - assert expr(foo=42) == 84 - - def test_template_passthrough(self): - t = Template('Content') - assert env.get_template(t) is t - assert env.select_template([t]) is t - assert env.get_or_select_template([t]) is t - assert env.get_or_select_template(t) is t - - def test_autoescape_autoselect(self): - def select_autoescape(name): - if name is None or '.' not in name: - return False - return name.endswith('.html') - env = Environment(autoescape=select_autoescape, - loader=DictLoader({ - 'test.txt': '{{ foo }}', - 'test.html': '{{ foo }}' - })) - t = env.get_template('test.txt') - assert t.render(foo='') == '' - t = env.get_template('test.html') - assert t.render(foo='') == '<foo>' - t = env.from_string('{{ foo }}') - assert t.render(foo='') == '' - - -class MetaTestCase(JinjaTestCase): - - def test_find_undeclared_variables(self): - ast = env.parse('{% set foo = 42 %}{{ bar + foo }}') - x = meta.find_undeclared_variables(ast) - assert x == set(['bar']) - - ast = env.parse('{% set foo = 42 %}{{ bar + foo }}' - '{% macro meh(x) %}{{ x }}{% endmacro %}' - '{% for item in seq %}{{ muh(item) + meh(seq) }}{% endfor %}') - x = meta.find_undeclared_variables(ast) - assert x == set(['bar', 'seq', 'muh']) - - def test_find_refererenced_templates(self): - ast = env.parse('{% extends "layout.html" %}{% include helper %}') - i = meta.find_referenced_templates(ast) - assert i.next() == 'layout.html' - assert i.next() is None - assert list(i) == [] - - ast = env.parse('{% extends "layout.html" %}' - '{% from "test.html" import a, b as c %}' - '{% import "meh.html" as meh %}' - '{% include "muh.html" %}') - i = meta.find_referenced_templates(ast) - assert list(i) == ['layout.html', 'test.html', 'meh.html', 'muh.html'] - - def test_find_included_templates(self): - ast = env.parse('{% include ["foo.html", "bar.html"] %}') - i = meta.find_referenced_templates(ast) - assert list(i) == ['foo.html', 'bar.html'] - - ast = env.parse('{% include ("foo.html", "bar.html") %}') - i = meta.find_referenced_templates(ast) - assert list(i) == ['foo.html', 'bar.html'] - - ast = env.parse('{% include ["foo.html", "bar.html", foo] %}') - i = meta.find_referenced_templates(ast) - assert list(i) == ['foo.html', 'bar.html', None] - - ast = env.parse('{% include ("foo.html", "bar.html", foo) %}') - i = meta.find_referenced_templates(ast) - assert list(i) == ['foo.html', 'bar.html', None] - - -class StreamingTestCase(JinjaTestCase): - - def test_basic_streaming(self): - tmpl = env.from_string("
    {% for item in seq %}
  • {{ loop.index " - "}} - {{ item }}
  • {%- endfor %}
") - stream = tmpl.stream(seq=range(4)) - self.assert_equal(stream.next(), '
    ') - self.assert_equal(stream.next(), '
  • 1 - 0
  • ') - self.assert_equal(stream.next(), '
  • 2 - 1
  • ') - self.assert_equal(stream.next(), '
  • 3 - 2
  • ') - self.assert_equal(stream.next(), '
  • 4 - 3
  • ') - self.assert_equal(stream.next(), '
') - - def test_buffered_streaming(self): - tmpl = env.from_string("
    {% for item in seq %}
  • {{ loop.index " - "}} - {{ item }}
  • {%- endfor %}
") - stream = tmpl.stream(seq=range(4)) - stream.enable_buffering(size=3) - self.assert_equal(stream.next(), u'
  • 1 - 0
  • 2 - 1
  • ') - self.assert_equal(stream.next(), u'
  • 3 - 2
  • 4 - 3
') - - def test_streaming_behavior(self): - tmpl = env.from_string("") - stream = tmpl.stream() - assert not stream.buffered - stream.enable_buffering(20) - assert stream.buffered - stream.disable_buffering() - assert not stream.buffered - - -class UndefinedTestCase(JinjaTestCase): - - def test_stopiteration_is_undefined(self): - def test(): - raise StopIteration() - t = Template('A{{ test() }}B') - assert t.render(test=test) == 'AB' - t = Template('A{{ test().missingattribute }}B') - self.assert_raises(UndefinedError, t.render, test=test) - - def test_undefined_and_special_attributes(self): - try: - Undefined('Foo').__dict__ - except AttributeError: - pass - else: - assert False, "Expected actual attribute error" - - def test_default_undefined(self): - env = Environment(undefined=Undefined) - self.assert_equal(env.from_string('{{ missing }}').render(), u'') - self.assert_raises(UndefinedError, - env.from_string('{{ missing.attribute }}').render) - self.assert_equal(env.from_string('{{ missing|list }}').render(), '[]') - self.assert_equal(env.from_string('{{ missing is not defined }}').render(), 'True') - self.assert_equal(env.from_string('{{ foo.missing }}').render(foo=42), '') - self.assert_equal(env.from_string('{{ not missing }}').render(), 'True') - - def test_debug_undefined(self): - env = Environment(undefined=DebugUndefined) - self.assert_equal(env.from_string('{{ missing }}').render(), '{{ missing }}') - self.assert_raises(UndefinedError, - env.from_string('{{ missing.attribute }}').render) - self.assert_equal(env.from_string('{{ missing|list }}').render(), '[]') - self.assert_equal(env.from_string('{{ missing is not defined }}').render(), 'True') - self.assert_equal(env.from_string('{{ foo.missing }}').render(foo=42), - u"{{ no such element: int object['missing'] }}") - self.assert_equal(env.from_string('{{ not missing }}').render(), 'True') - - def test_strict_undefined(self): - env = Environment(undefined=StrictUndefined) - self.assert_raises(UndefinedError, env.from_string('{{ missing }}').render) - self.assert_raises(UndefinedError, env.from_string('{{ missing.attribute }}').render) - self.assert_raises(UndefinedError, env.from_string('{{ missing|list }}').render) - self.assert_equal(env.from_string('{{ missing is not defined }}').render(), 'True') - self.assert_raises(UndefinedError, env.from_string('{{ foo.missing }}').render, foo=42) - self.assert_raises(UndefinedError, env.from_string('{{ not missing }}').render) - - def test_indexing_gives_undefined(self): - t = Template("{{ var[42].foo }}") - self.assert_raises(UndefinedError, t.render, var=0) - - def test_none_gives_proper_error(self): - try: - Environment().getattr(None, 'split')() - except UndefinedError, e: - assert e.message == "'None' has no attribute 'split'" - else: - assert False, 'expected exception' - - def test_object_repr(self): - try: - Undefined(obj=42, name='upper')() - except UndefinedError, e: - assert e.message == "'int object' has no attribute 'upper'" - else: - assert False, 'expected exception' - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ExtendedAPITestCase)) - suite.addTest(unittest.makeSuite(MetaTestCase)) - suite.addTest(unittest.makeSuite(StreamingTestCase)) - suite.addTest(unittest.makeSuite(UndefinedTestCase)) - return suite diff --git a/libs/jinja2/testsuite/core_tags.py b/libs/jinja2/testsuite/core_tags.py deleted file mode 100644 index 2b5f5801..00000000 --- a/libs/jinja2/testsuite/core_tags.py +++ /dev/null @@ -1,285 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.core_tags - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Test the core tags like for and if. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import unittest - -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Environment, TemplateSyntaxError, UndefinedError, \ - DictLoader - -env = Environment() - - -class ForLoopTestCase(JinjaTestCase): - - def test_simple(self): - tmpl = env.from_string('{% for item in seq %}{{ item }}{% endfor %}') - assert tmpl.render(seq=range(10)) == '0123456789' - - def test_else(self): - tmpl = env.from_string('{% for item in seq %}XXX{% else %}...{% endfor %}') - assert tmpl.render() == '...' - - def test_empty_blocks(self): - tmpl = env.from_string('<{% for item in seq %}{% else %}{% endfor %}>') - assert tmpl.render() == '<>' - - def test_context_vars(self): - tmpl = env.from_string('''{% for item in seq -%} - {{ loop.index }}|{{ loop.index0 }}|{{ loop.revindex }}|{{ - loop.revindex0 }}|{{ loop.first }}|{{ loop.last }}|{{ - loop.length }}###{% endfor %}''') - one, two, _ = tmpl.render(seq=[0, 1]).split('###') - (one_index, one_index0, one_revindex, one_revindex0, one_first, - one_last, one_length) = one.split('|') - (two_index, two_index0, two_revindex, two_revindex0, two_first, - two_last, two_length) = two.split('|') - - assert int(one_index) == 1 and int(two_index) == 2 - assert int(one_index0) == 0 and int(two_index0) == 1 - assert int(one_revindex) == 2 and int(two_revindex) == 1 - assert int(one_revindex0) == 1 and int(two_revindex0) == 0 - assert one_first == 'True' and two_first == 'False' - assert one_last == 'False' and two_last == 'True' - assert one_length == two_length == '2' - - def test_cycling(self): - tmpl = env.from_string('''{% for item in seq %}{{ - loop.cycle('<1>', '<2>') }}{% endfor %}{% - for item in seq %}{{ loop.cycle(*through) }}{% endfor %}''') - output = tmpl.render(seq=range(4), through=('<1>', '<2>')) - assert output == '<1><2>' * 4 - - def test_scope(self): - tmpl = env.from_string('{% for item in seq %}{% endfor %}{{ item }}') - output = tmpl.render(seq=range(10)) - assert not output - - def test_varlen(self): - def inner(): - for item in range(5): - yield item - tmpl = env.from_string('{% for item in iter %}{{ item }}{% endfor %}') - output = tmpl.render(iter=inner()) - assert output == '01234' - - def test_noniter(self): - tmpl = env.from_string('{% for item in none %}...{% endfor %}') - self.assert_raises(TypeError, tmpl.render) - - def test_recursive(self): - tmpl = env.from_string('''{% for item in seq recursive -%} - [{{ item.a }}{% if item.b %}<{{ loop(item.b) }}>{% endif %}] - {%- endfor %}''') - assert tmpl.render(seq=[ - dict(a=1, b=[dict(a=1), dict(a=2)]), - dict(a=2, b=[dict(a=1), dict(a=2)]), - dict(a=3, b=[dict(a='a')]) - ]) == '[1<[1][2]>][2<[1][2]>][3<[a]>]' - - def test_looploop(self): - tmpl = env.from_string('''{% for row in table %} - {%- set rowloop = loop -%} - {% for cell in row -%} - [{{ rowloop.index }}|{{ loop.index }}] - {%- endfor %} - {%- endfor %}''') - assert tmpl.render(table=['ab', 'cd']) == '[1|1][1|2][2|1][2|2]' - - def test_reversed_bug(self): - tmpl = env.from_string('{% for i in items %}{{ i }}' - '{% if not loop.last %}' - ',{% endif %}{% endfor %}') - assert tmpl.render(items=reversed([3, 2, 1])) == '1,2,3' - - def test_loop_errors(self): - tmpl = env.from_string('''{% for item in [1] if loop.index - == 0 %}...{% endfor %}''') - self.assert_raises(UndefinedError, tmpl.render) - tmpl = env.from_string('''{% for item in [] %}...{% else - %}{{ loop }}{% endfor %}''') - assert tmpl.render() == '' - - def test_loop_filter(self): - tmpl = env.from_string('{% for item in range(10) if item ' - 'is even %}[{{ item }}]{% endfor %}') - assert tmpl.render() == '[0][2][4][6][8]' - tmpl = env.from_string(''' - {%- for item in range(10) if item is even %}[{{ - loop.index }}:{{ item }}]{% endfor %}''') - assert tmpl.render() == '[1:0][2:2][3:4][4:6][5:8]' - - def test_loop_unassignable(self): - self.assert_raises(TemplateSyntaxError, env.from_string, - '{% for loop in seq %}...{% endfor %}') - - def test_scoped_special_var(self): - t = env.from_string('{% for s in seq %}[{{ loop.first }}{% for c in s %}' - '|{{ loop.first }}{% endfor %}]{% endfor %}') - assert t.render(seq=('ab', 'cd')) == '[True|True|False][False|True|False]' - - def test_scoped_loop_var(self): - t = env.from_string('{% for x in seq %}{{ loop.first }}' - '{% for y in seq %}{% endfor %}{% endfor %}') - assert t.render(seq='ab') == 'TrueFalse' - t = env.from_string('{% for x in seq %}{% for y in seq %}' - '{{ loop.first }}{% endfor %}{% endfor %}') - assert t.render(seq='ab') == 'TrueFalseTrueFalse' - - def test_recursive_empty_loop_iter(self): - t = env.from_string(''' - {%- for item in foo recursive -%}{%- endfor -%} - ''') - assert t.render(dict(foo=[])) == '' - - def test_call_in_loop(self): - t = env.from_string(''' - {%- macro do_something() -%} - [{{ caller() }}] - {%- endmacro %} - - {%- for i in [1, 2, 3] %} - {%- call do_something() -%} - {{ i }} - {%- endcall %} - {%- endfor -%} - ''') - assert t.render() == '[1][2][3]' - - def test_scoping_bug(self): - t = env.from_string(''' - {%- for item in foo %}...{{ item }}...{% endfor %} - {%- macro item(a) %}...{{ a }}...{% endmacro %} - {{- item(2) -}} - ''') - assert t.render(foo=(1,)) == '...1......2...' - - def test_unpacking(self): - tmpl = env.from_string('{% for a, b, c in [[1, 2, 3]] %}' - '{{ a }}|{{ b }}|{{ c }}{% endfor %}') - assert tmpl.render() == '1|2|3' - - -class IfConditionTestCase(JinjaTestCase): - - def test_simple(self): - tmpl = env.from_string('''{% if true %}...{% endif %}''') - assert tmpl.render() == '...' - - def test_elif(self): - tmpl = env.from_string('''{% if false %}XXX{% elif true - %}...{% else %}XXX{% endif %}''') - assert tmpl.render() == '...' - - def test_else(self): - tmpl = env.from_string('{% if false %}XXX{% else %}...{% endif %}') - assert tmpl.render() == '...' - - def test_empty(self): - tmpl = env.from_string('[{% if true %}{% else %}{% endif %}]') - assert tmpl.render() == '[]' - - def test_complete(self): - tmpl = env.from_string('{% if a %}A{% elif b %}B{% elif c == d %}' - 'C{% else %}D{% endif %}') - assert tmpl.render(a=0, b=False, c=42, d=42.0) == 'C' - - def test_no_scope(self): - tmpl = env.from_string('{% if a %}{% set foo = 1 %}{% endif %}{{ foo }}') - assert tmpl.render(a=True) == '1' - tmpl = env.from_string('{% if true %}{% set foo = 1 %}{% endif %}{{ foo }}') - assert tmpl.render() == '1' - - -class MacrosTestCase(JinjaTestCase): - env = Environment(trim_blocks=True) - - def test_simple(self): - tmpl = self.env.from_string('''\ -{% macro say_hello(name) %}Hello {{ name }}!{% endmacro %} -{{ say_hello('Peter') }}''') - assert tmpl.render() == 'Hello Peter!' - - def test_scoping(self): - tmpl = self.env.from_string('''\ -{% macro level1(data1) %} -{% macro level2(data2) %}{{ data1 }}|{{ data2 }}{% endmacro %} -{{ level2('bar') }}{% endmacro %} -{{ level1('foo') }}''') - assert tmpl.render() == 'foo|bar' - - def test_arguments(self): - tmpl = self.env.from_string('''\ -{% macro m(a, b, c='c', d='d') %}{{ a }}|{{ b }}|{{ c }}|{{ d }}{% endmacro %} -{{ m() }}|{{ m('a') }}|{{ m('a', 'b') }}|{{ m(1, 2, 3) }}''') - assert tmpl.render() == '||c|d|a||c|d|a|b|c|d|1|2|3|d' - - def test_varargs(self): - tmpl = self.env.from_string('''\ -{% macro test() %}{{ varargs|join('|') }}{% endmacro %}\ -{{ test(1, 2, 3) }}''') - assert tmpl.render() == '1|2|3' - - def test_simple_call(self): - tmpl = self.env.from_string('''\ -{% macro test() %}[[{{ caller() }}]]{% endmacro %}\ -{% call test() %}data{% endcall %}''') - assert tmpl.render() == '[[data]]' - - def test_complex_call(self): - tmpl = self.env.from_string('''\ -{% macro test() %}[[{{ caller('data') }}]]{% endmacro %}\ -{% call(data) test() %}{{ data }}{% endcall %}''') - assert tmpl.render() == '[[data]]' - - def test_caller_undefined(self): - tmpl = self.env.from_string('''\ -{% set caller = 42 %}\ -{% macro test() %}{{ caller is not defined }}{% endmacro %}\ -{{ test() }}''') - assert tmpl.render() == 'True' - - def test_include(self): - self.env = Environment(loader=DictLoader({'include': - '{% macro test(foo) %}[{{ foo }}]{% endmacro %}'})) - tmpl = self.env.from_string('{% from "include" import test %}{{ test("foo") }}') - assert tmpl.render() == '[foo]' - - def test_macro_api(self): - tmpl = self.env.from_string('{% macro foo(a, b) %}{% endmacro %}' - '{% macro bar() %}{{ varargs }}{{ kwargs }}{% endmacro %}' - '{% macro baz() %}{{ caller() }}{% endmacro %}') - assert tmpl.module.foo.arguments == ('a', 'b') - assert tmpl.module.foo.defaults == () - assert tmpl.module.foo.name == 'foo' - assert not tmpl.module.foo.caller - assert not tmpl.module.foo.catch_kwargs - assert not tmpl.module.foo.catch_varargs - assert tmpl.module.bar.arguments == () - assert tmpl.module.bar.defaults == () - assert not tmpl.module.bar.caller - assert tmpl.module.bar.catch_kwargs - assert tmpl.module.bar.catch_varargs - assert tmpl.module.baz.caller - - def test_callself(self): - tmpl = self.env.from_string('{% macro foo(x) %}{{ x }}{% if x > 1 %}|' - '{{ foo(x - 1) }}{% endif %}{% endmacro %}' - '{{ foo(5) }}') - assert tmpl.render() == '5|4|3|2|1' - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ForLoopTestCase)) - suite.addTest(unittest.makeSuite(IfConditionTestCase)) - suite.addTest(unittest.makeSuite(MacrosTestCase)) - return suite diff --git a/libs/jinja2/testsuite/debug.py b/libs/jinja2/testsuite/debug.py deleted file mode 100644 index 7552dec3..00000000 --- a/libs/jinja2/testsuite/debug.py +++ /dev/null @@ -1,60 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.debug - ~~~~~~~~~~~~~~~~~~~~~~ - - Tests the debug system. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import sys -import unittest - -from jinja2.testsuite import JinjaTestCase, filesystem_loader - -from jinja2 import Environment, TemplateSyntaxError - -env = Environment(loader=filesystem_loader) - - -class DebugTestCase(JinjaTestCase): - - if sys.version_info[:2] != (2, 4): - def test_runtime_error(self): - def test(): - tmpl.render(fail=lambda: 1 / 0) - tmpl = env.get_template('broken.html') - self.assert_traceback_matches(test, r''' - File ".*?broken.html", line 2, in (top-level template code|) - \{\{ fail\(\) \}\} - File ".*?debug.pyc?", line \d+, in - tmpl\.render\(fail=lambda: 1 / 0\) -ZeroDivisionError: (int(eger)? )?division (or modulo )?by zero -''') - - def test_syntax_error(self): - # XXX: the .*? is necessary for python3 which does not hide - # some of the stack frames we don't want to show. Not sure - # what's up with that, but that is not that critical. Should - # be fixed though. - self.assert_traceback_matches(lambda: env.get_template('syntaxerror.html'), r'''(?sm) - File ".*?syntaxerror.html", line 4, in (template|) - \{% endif %\}.*? -(jinja2\.exceptions\.)?TemplateSyntaxError: Encountered unknown tag 'endif'. Jinja was looking for the following tags: 'endfor' or 'else'. The innermost block that needs to be closed is 'for'. - ''') - - def test_regular_syntax_error(self): - def test(): - raise TemplateSyntaxError('wtf', 42) - self.assert_traceback_matches(test, r''' - File ".*debug.pyc?", line \d+, in test - raise TemplateSyntaxError\('wtf', 42\) -(jinja2\.exceptions\.)?TemplateSyntaxError: wtf - line 42''') - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(DebugTestCase)) - return suite diff --git a/libs/jinja2/testsuite/doctests.py b/libs/jinja2/testsuite/doctests.py deleted file mode 100644 index 616d3b6e..00000000 --- a/libs/jinja2/testsuite/doctests.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.doctests - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - The doctests. Collects all tests we want to test from - the Jinja modules. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import unittest -import doctest - - -def suite(): - from jinja2 import utils, sandbox, runtime, meta, loaders, \ - ext, environment, bccache, nodes - suite = unittest.TestSuite() - suite.addTest(doctest.DocTestSuite(utils)) - suite.addTest(doctest.DocTestSuite(sandbox)) - suite.addTest(doctest.DocTestSuite(runtime)) - suite.addTest(doctest.DocTestSuite(meta)) - suite.addTest(doctest.DocTestSuite(loaders)) - suite.addTest(doctest.DocTestSuite(ext)) - suite.addTest(doctest.DocTestSuite(environment)) - suite.addTest(doctest.DocTestSuite(bccache)) - suite.addTest(doctest.DocTestSuite(nodes)) - return suite diff --git a/libs/jinja2/testsuite/ext.py b/libs/jinja2/testsuite/ext.py deleted file mode 100644 index 6ca6c228..00000000 --- a/libs/jinja2/testsuite/ext.py +++ /dev/null @@ -1,455 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.ext - ~~~~~~~~~~~~~~~~~~~~ - - Tests for the extensions. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import re -import unittest - -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Environment, DictLoader, contextfunction, nodes -from jinja2.exceptions import TemplateAssertionError -from jinja2.ext import Extension -from jinja2.lexer import Token, count_newlines -from jinja2.utils import next - -# 2.x / 3.x -try: - from io import BytesIO -except ImportError: - from StringIO import StringIO as BytesIO - - -importable_object = 23 - -_gettext_re = re.compile(r'_\((.*?)\)(?s)') - - -i18n_templates = { - 'master.html': '{{ page_title|default(_("missing")) }}' - '{% block body %}{% endblock %}', - 'child.html': '{% extends "master.html" %}{% block body %}' - '{% trans %}watch out{% endtrans %}{% endblock %}', - 'plural.html': '{% trans user_count %}One user online{% pluralize %}' - '{{ user_count }} users online{% endtrans %}', - 'stringformat.html': '{{ _("User: %(num)s")|format(num=user_count) }}' -} - -newstyle_i18n_templates = { - 'master.html': '{{ page_title|default(_("missing")) }}' - '{% block body %}{% endblock %}', - 'child.html': '{% extends "master.html" %}{% block body %}' - '{% trans %}watch out{% endtrans %}{% endblock %}', - 'plural.html': '{% trans user_count %}One user online{% pluralize %}' - '{{ user_count }} users online{% endtrans %}', - 'stringformat.html': '{{ _("User: %(num)s", num=user_count) }}', - 'ngettext.html': '{{ ngettext("%(num)s apple", "%(num)s apples", apples) }}', - 'ngettext_long.html': '{% trans num=apples %}{{ num }} apple{% pluralize %}' - '{{ num }} apples{% endtrans %}', - 'transvars1.html': '{% trans %}User: {{ num }}{% endtrans %}', - 'transvars2.html': '{% trans num=count %}User: {{ num }}{% endtrans %}', - 'transvars3.html': '{% trans count=num %}User: {{ count }}{% endtrans %}', - 'novars.html': '{% trans %}%(hello)s{% endtrans %}', - 'vars.html': '{% trans %}{{ foo }}%(foo)s{% endtrans %}', - 'explicitvars.html': '{% trans foo="42" %}%(foo)s{% endtrans %}' -} - - -languages = { - 'de': { - 'missing': u'fehlend', - 'watch out': u'pass auf', - 'One user online': u'Ein Benutzer online', - '%(user_count)s users online': u'%(user_count)s Benutzer online', - 'User: %(num)s': u'Benutzer: %(num)s', - 'User: %(count)s': u'Benutzer: %(count)s', - '%(num)s apple': u'%(num)s Apfel', - '%(num)s apples': u'%(num)s Äpfel' - } -} - - -@contextfunction -def gettext(context, string): - language = context.get('LANGUAGE', 'en') - return languages.get(language, {}).get(string, string) - - -@contextfunction -def ngettext(context, s, p, n): - language = context.get('LANGUAGE', 'en') - if n != 1: - return languages.get(language, {}).get(p, p) - return languages.get(language, {}).get(s, s) - - -i18n_env = Environment( - loader=DictLoader(i18n_templates), - extensions=['jinja2.ext.i18n'] -) -i18n_env.globals.update({ - '_': gettext, - 'gettext': gettext, - 'ngettext': ngettext -}) - -newstyle_i18n_env = Environment( - loader=DictLoader(newstyle_i18n_templates), - extensions=['jinja2.ext.i18n'] -) -newstyle_i18n_env.install_gettext_callables(gettext, ngettext, newstyle=True) - -class TestExtension(Extension): - tags = set(['test']) - ext_attr = 42 - - def parse(self, parser): - return nodes.Output([self.call_method('_dump', [ - nodes.EnvironmentAttribute('sandboxed'), - self.attr('ext_attr'), - nodes.ImportedName(__name__ + '.importable_object'), - nodes.ContextReference() - ])]).set_lineno(next(parser.stream).lineno) - - def _dump(self, sandboxed, ext_attr, imported_object, context): - return '%s|%s|%s|%s' % ( - sandboxed, - ext_attr, - imported_object, - context.blocks - ) - - -class PreprocessorExtension(Extension): - - def preprocess(self, source, name, filename=None): - return source.replace('[[TEST]]', '({{ foo }})') - - -class StreamFilterExtension(Extension): - - def filter_stream(self, stream): - for token in stream: - if token.type == 'data': - for t in self.interpolate(token): - yield t - else: - yield token - - def interpolate(self, token): - pos = 0 - end = len(token.value) - lineno = token.lineno - while 1: - match = _gettext_re.search(token.value, pos) - if match is None: - break - value = token.value[pos:match.start()] - if value: - yield Token(lineno, 'data', value) - lineno += count_newlines(token.value) - yield Token(lineno, 'variable_begin', None) - yield Token(lineno, 'name', 'gettext') - yield Token(lineno, 'lparen', None) - yield Token(lineno, 'string', match.group(1)) - yield Token(lineno, 'rparen', None) - yield Token(lineno, 'variable_end', None) - pos = match.end() - if pos < end: - yield Token(lineno, 'data', token.value[pos:]) - - -class ExtensionsTestCase(JinjaTestCase): - - def test_extend_late(self): - env = Environment() - env.add_extension('jinja2.ext.autoescape') - t = env.from_string('{% autoescape true %}{{ "" }}{% endautoescape %}') - assert t.render() == '<test>' - - def test_loop_controls(self): - env = Environment(extensions=['jinja2.ext.loopcontrols']) - - tmpl = env.from_string(''' - {%- for item in [1, 2, 3, 4] %} - {%- if item % 2 == 0 %}{% continue %}{% endif -%} - {{ item }} - {%- endfor %}''') - assert tmpl.render() == '13' - - tmpl = env.from_string(''' - {%- for item in [1, 2, 3, 4] %} - {%- if item > 2 %}{% break %}{% endif -%} - {{ item }} - {%- endfor %}''') - assert tmpl.render() == '12' - - def test_do(self): - env = Environment(extensions=['jinja2.ext.do']) - tmpl = env.from_string(''' - {%- set items = [] %} - {%- for char in "foo" %} - {%- do items.append(loop.index0 ~ char) %} - {%- endfor %}{{ items|join(', ') }}''') - assert tmpl.render() == '0f, 1o, 2o' - - def test_with(self): - env = Environment(extensions=['jinja2.ext.with_']) - tmpl = env.from_string('''\ - {% with a=42, b=23 -%} - {{ a }} = {{ b }} - {% endwith -%} - {{ a }} = {{ b }}\ - ''') - assert [x.strip() for x in tmpl.render(a=1, b=2).splitlines()] \ - == ['42 = 23', '1 = 2'] - - def test_extension_nodes(self): - env = Environment(extensions=[TestExtension]) - tmpl = env.from_string('{% test %}') - assert tmpl.render() == 'False|42|23|{}' - - def test_identifier(self): - assert TestExtension.identifier == __name__ + '.TestExtension' - - def test_rebinding(self): - original = Environment(extensions=[TestExtension]) - overlay = original.overlay() - for env in original, overlay: - for ext in env.extensions.itervalues(): - assert ext.environment is env - - def test_preprocessor_extension(self): - env = Environment(extensions=[PreprocessorExtension]) - tmpl = env.from_string('{[[TEST]]}') - assert tmpl.render(foo=42) == '{(42)}' - - def test_streamfilter_extension(self): - env = Environment(extensions=[StreamFilterExtension]) - env.globals['gettext'] = lambda x: x.upper() - tmpl = env.from_string('Foo _(bar) Baz') - out = tmpl.render() - assert out == 'Foo BAR Baz' - - def test_extension_ordering(self): - class T1(Extension): - priority = 1 - class T2(Extension): - priority = 2 - env = Environment(extensions=[T1, T2]) - ext = list(env.iter_extensions()) - assert ext[0].__class__ is T1 - assert ext[1].__class__ is T2 - - -class InternationalizationTestCase(JinjaTestCase): - - def test_trans(self): - tmpl = i18n_env.get_template('child.html') - assert tmpl.render(LANGUAGE='de') == 'fehlendpass auf' - - def test_trans_plural(self): - tmpl = i18n_env.get_template('plural.html') - assert tmpl.render(LANGUAGE='de', user_count=1) == 'Ein Benutzer online' - assert tmpl.render(LANGUAGE='de', user_count=2) == '2 Benutzer online' - - def test_complex_plural(self): - tmpl = i18n_env.from_string('{% trans foo=42, count=2 %}{{ count }} item{% ' - 'pluralize count %}{{ count }} items{% endtrans %}') - assert tmpl.render() == '2 items' - self.assert_raises(TemplateAssertionError, i18n_env.from_string, - '{% trans foo %}...{% pluralize bar %}...{% endtrans %}') - - def test_trans_stringformatting(self): - tmpl = i18n_env.get_template('stringformat.html') - assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5' - - def test_extract(self): - from jinja2.ext import babel_extract - source = BytesIO(''' - {{ gettext('Hello World') }} - {% trans %}Hello World{% endtrans %} - {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %} - '''.encode('ascii')) # make python 3 happy - assert list(babel_extract(source, ('gettext', 'ngettext', '_'), [], {})) == [ - (2, 'gettext', u'Hello World', []), - (3, 'gettext', u'Hello World', []), - (4, 'ngettext', (u'%(users)s user', u'%(users)s users', None), []) - ] - - def test_comment_extract(self): - from jinja2.ext import babel_extract - source = BytesIO(''' - {# trans first #} - {{ gettext('Hello World') }} - {% trans %}Hello World{% endtrans %}{# trans second #} - {#: third #} - {% trans %}{{ users }} user{% pluralize %}{{ users }} users{% endtrans %} - '''.encode('utf-8')) # make python 3 happy - assert list(babel_extract(source, ('gettext', 'ngettext', '_'), ['trans', ':'], {})) == [ - (3, 'gettext', u'Hello World', ['first']), - (4, 'gettext', u'Hello World', ['second']), - (6, 'ngettext', (u'%(users)s user', u'%(users)s users', None), ['third']) - ] - - -class NewstyleInternationalizationTestCase(JinjaTestCase): - - def test_trans(self): - tmpl = newstyle_i18n_env.get_template('child.html') - assert tmpl.render(LANGUAGE='de') == 'fehlendpass auf' - - def test_trans_plural(self): - tmpl = newstyle_i18n_env.get_template('plural.html') - assert tmpl.render(LANGUAGE='de', user_count=1) == 'Ein Benutzer online' - assert tmpl.render(LANGUAGE='de', user_count=2) == '2 Benutzer online' - - def test_complex_plural(self): - tmpl = newstyle_i18n_env.from_string('{% trans foo=42, count=2 %}{{ count }} item{% ' - 'pluralize count %}{{ count }} items{% endtrans %}') - assert tmpl.render() == '2 items' - self.assert_raises(TemplateAssertionError, i18n_env.from_string, - '{% trans foo %}...{% pluralize bar %}...{% endtrans %}') - - def test_trans_stringformatting(self): - tmpl = newstyle_i18n_env.get_template('stringformat.html') - assert tmpl.render(LANGUAGE='de', user_count=5) == 'Benutzer: 5' - - def test_newstyle_plural(self): - tmpl = newstyle_i18n_env.get_template('ngettext.html') - assert tmpl.render(LANGUAGE='de', apples=1) == '1 Apfel' - assert tmpl.render(LANGUAGE='de', apples=5) == u'5 Äpfel' - - def test_autoescape_support(self): - env = Environment(extensions=['jinja2.ext.autoescape', - 'jinja2.ext.i18n']) - env.install_gettext_callables(lambda x: u'Wert: %(name)s', - lambda s, p, n: s, newstyle=True) - t = env.from_string('{% autoescape ae %}{{ gettext("foo", name=' - '"") }}{% endautoescape %}') - assert t.render(ae=True) == 'Wert: <test>' - assert t.render(ae=False) == 'Wert: ' - - def test_num_used_twice(self): - tmpl = newstyle_i18n_env.get_template('ngettext_long.html') - assert tmpl.render(apples=5, LANGUAGE='de') == u'5 Äpfel' - - def test_num_called_num(self): - source = newstyle_i18n_env.compile(''' - {% trans num=3 %}{{ num }} apple{% pluralize - %}{{ num }} apples{% endtrans %} - ''', raw=True) - # quite hacky, but the only way to properly test that. The idea is - # that the generated code does not pass num twice (although that - # would work) for better performance. This only works on the - # newstyle gettext of course - assert re.search(r"l_ngettext, u?'\%\(num\)s apple', u?'\%\(num\)s " - r"apples', 3", source) is not None - - def test_trans_vars(self): - t1 = newstyle_i18n_env.get_template('transvars1.html') - t2 = newstyle_i18n_env.get_template('transvars2.html') - t3 = newstyle_i18n_env.get_template('transvars3.html') - assert t1.render(num=1, LANGUAGE='de') == 'Benutzer: 1' - assert t2.render(count=23, LANGUAGE='de') == 'Benutzer: 23' - assert t3.render(num=42, LANGUAGE='de') == 'Benutzer: 42' - - def test_novars_vars_escaping(self): - t = newstyle_i18n_env.get_template('novars.html') - assert t.render() == '%(hello)s' - t = newstyle_i18n_env.get_template('vars.html') - assert t.render(foo='42') == '42%(foo)s' - t = newstyle_i18n_env.get_template('explicitvars.html') - assert t.render() == '%(foo)s' - - -class AutoEscapeTestCase(JinjaTestCase): - - def test_scoped_setting(self): - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=True) - tmpl = env.from_string(''' - {{ "" }} - {% autoescape false %} - {{ "" }} - {% endautoescape %} - {{ "" }} - ''') - assert tmpl.render().split() == \ - [u'<HelloWorld>', u'', u'<HelloWorld>'] - - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=False) - tmpl = env.from_string(''' - {{ "" }} - {% autoescape true %} - {{ "" }} - {% endautoescape %} - {{ "" }} - ''') - assert tmpl.render().split() == \ - [u'', u'<HelloWorld>', u''] - - def test_nonvolatile(self): - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=True) - tmpl = env.from_string('{{ {"foo": ""}|xmlattr|escape }}') - assert tmpl.render() == ' foo="<test>"' - tmpl = env.from_string('{% autoescape false %}{{ {"foo": ""}' - '|xmlattr|escape }}{% endautoescape %}') - assert tmpl.render() == ' foo="&lt;test&gt;"' - - def test_volatile(self): - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=True) - tmpl = env.from_string('{% autoescape foo %}{{ {"foo": ""}' - '|xmlattr|escape }}{% endautoescape %}') - assert tmpl.render(foo=False) == ' foo="&lt;test&gt;"' - assert tmpl.render(foo=True) == ' foo="<test>"' - - def test_scoping(self): - env = Environment(extensions=['jinja2.ext.autoescape']) - tmpl = env.from_string('{% autoescape true %}{% set x = "" %}{{ x }}' - '{% endautoescape %}{{ x }}{{ "" }}') - assert tmpl.render(x=1) == '<x>1' - - def test_volatile_scoping(self): - env = Environment(extensions=['jinja2.ext.autoescape']) - tmplsource = ''' - {% autoescape val %} - {% macro foo(x) %} - [{{ x }}] - {% endmacro %} - {{ foo().__class__.__name__ }} - {% endautoescape %} - {{ '' }} - ''' - tmpl = env.from_string(tmplsource) - assert tmpl.render(val=True).split()[0] == 'Markup' - assert tmpl.render(val=False).split()[0] == unicode.__name__ - - # looking at the source we should see there in raw - # (and then escaped as well) - env = Environment(extensions=['jinja2.ext.autoescape']) - pysource = env.compile(tmplsource, raw=True) - assert '\\n' in pysource - - env = Environment(extensions=['jinja2.ext.autoescape'], - autoescape=True) - pysource = env.compile(tmplsource, raw=True) - assert '<testing>\\n' in pysource - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ExtensionsTestCase)) - suite.addTest(unittest.makeSuite(InternationalizationTestCase)) - suite.addTest(unittest.makeSuite(NewstyleInternationalizationTestCase)) - suite.addTest(unittest.makeSuite(AutoEscapeTestCase)) - return suite diff --git a/libs/jinja2/testsuite/filters.py b/libs/jinja2/testsuite/filters.py deleted file mode 100644 index aefe7682..00000000 --- a/libs/jinja2/testsuite/filters.py +++ /dev/null @@ -1,356 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.filters - ~~~~~~~~~~~~~~~~~~~~~~~~ - - Tests for the jinja filters. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import unittest -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Markup, Environment - -env = Environment() - - -class FilterTestCase(JinjaTestCase): - - def test_capitalize(self): - tmpl = env.from_string('{{ "foo bar"|capitalize }}') - assert tmpl.render() == 'Foo bar' - - def test_center(self): - tmpl = env.from_string('{{ "foo"|center(9) }}') - assert tmpl.render() == ' foo ' - - def test_default(self): - tmpl = env.from_string( - "{{ missing|default('no') }}|{{ false|default('no') }}|" - "{{ false|default('no', true) }}|{{ given|default('no') }}" - ) - assert tmpl.render(given='yes') == 'no|False|no|yes' - - def test_dictsort(self): - tmpl = env.from_string( - '{{ foo|dictsort }}|' - '{{ foo|dictsort(true) }}|' - '{{ foo|dictsort(false, "value") }}' - ) - out = tmpl.render(foo={"aa": 0, "b": 1, "c": 2, "AB": 3}) - assert out == ("[('aa', 0), ('AB', 3), ('b', 1), ('c', 2)]|" - "[('AB', 3), ('aa', 0), ('b', 1), ('c', 2)]|" - "[('aa', 0), ('b', 1), ('c', 2), ('AB', 3)]") - - def test_batch(self): - tmpl = env.from_string("{{ foo|batch(3)|list }}|" - "{{ foo|batch(3, 'X')|list }}") - out = tmpl.render(foo=range(10)) - assert out == ("[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]|" - "[[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 'X', 'X']]") - - def test_slice(self): - tmpl = env.from_string('{{ foo|slice(3)|list }}|' - '{{ foo|slice(3, "X")|list }}') - out = tmpl.render(foo=range(10)) - assert out == ("[[0, 1, 2, 3], [4, 5, 6], [7, 8, 9]]|" - "[[0, 1, 2, 3], [4, 5, 6, 'X'], [7, 8, 9, 'X']]") - - def test_escape(self): - tmpl = env.from_string('''{{ '<">&'|escape }}''') - out = tmpl.render() - assert out == '<">&' - - def test_striptags(self): - tmpl = env.from_string('''{{ foo|striptags }}''') - out = tmpl.render(foo='

just a small \n ' - 'example link

\n

to a webpage

' - '') - assert out == 'just a small example link to a webpage' - - def test_filesizeformat(self): - tmpl = env.from_string( - '{{ 100|filesizeformat }}|' - '{{ 1000|filesizeformat }}|' - '{{ 1000000|filesizeformat }}|' - '{{ 1000000000|filesizeformat }}|' - '{{ 1000000000000|filesizeformat }}|' - '{{ 100|filesizeformat(true) }}|' - '{{ 1000|filesizeformat(true) }}|' - '{{ 1000000|filesizeformat(true) }}|' - '{{ 1000000000|filesizeformat(true) }}|' - '{{ 1000000000000|filesizeformat(true) }}' - ) - out = tmpl.render() - assert out == ( - '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): - tmpl = env.from_string('{{ foo|first }}') - out = tmpl.render(foo=range(10)) - assert out == '0' - - def test_float(self): - tmpl = env.from_string('{{ "42"|float }}|' - '{{ "ajsghasjgd"|float }}|' - '{{ "32.32"|float }}') - out = tmpl.render() - assert out == '42.0|0.0|32.32' - - def test_format(self): - tmpl = env.from_string('''{{ "%s|%s"|format("a", "b") }}''') - out = tmpl.render() - assert out == 'a|b' - - def test_indent(self): - tmpl = env.from_string('{{ foo|indent(2) }}|{{ foo|indent(2, true) }}') - text = '\n'.join([' '.join(['foo', 'bar'] * 2)] * 2) - out = tmpl.render(foo=text) - assert out == ('foo bar foo bar\n foo bar foo bar| ' - 'foo bar foo bar\n foo bar foo bar') - - def test_int(self): - tmpl = env.from_string('{{ "42"|int }}|{{ "ajsghasjgd"|int }}|' - '{{ "32.32"|int }}') - out = tmpl.render() - assert out == '42|0|32' - - def test_join(self): - tmpl = env.from_string('{{ [1, 2, 3]|join("|") }}') - out = tmpl.render() - assert out == '1|2|3' - - env2 = Environment(autoescape=True) - tmpl = env2.from_string('{{ ["", "foo"|safe]|join }}') - assert tmpl.render() == '<foo>foo' - - def test_join_attribute(self): - class User(object): - def __init__(self, username): - self.username = username - tmpl = env.from_string('''{{ users|join(', ', 'username') }}''') - assert tmpl.render(users=map(User, ['foo', 'bar'])) == 'foo, bar' - - def test_last(self): - tmpl = env.from_string('''{{ foo|last }}''') - out = tmpl.render(foo=range(10)) - assert out == '9' - - def test_length(self): - tmpl = env.from_string('''{{ "hello world"|length }}''') - out = tmpl.render() - assert out == '11' - - def test_lower(self): - tmpl = env.from_string('''{{ "FOO"|lower }}''') - out = tmpl.render() - assert out == 'foo' - - def test_pprint(self): - from pprint import pformat - tmpl = env.from_string('''{{ data|pprint }}''') - data = range(1000) - assert tmpl.render(data=data) == pformat(data) - - def test_random(self): - tmpl = env.from_string('''{{ seq|random }}''') - seq = range(100) - for _ in range(10): - assert int(tmpl.render(seq=seq)) in seq - - def test_reverse(self): - tmpl = env.from_string('{{ "foobar"|reverse|join }}|' - '{{ [1, 2, 3]|reverse|list }}') - assert tmpl.render() == 'raboof|[3, 2, 1]' - - def test_string(self): - x = [1, 2, 3, 4, 5] - tmpl = env.from_string('''{{ obj|string }}''') - assert tmpl.render(obj=x) == unicode(x) - - def test_title(self): - tmpl = env.from_string('''{{ "foo bar"|title }}''') - assert tmpl.render() == "Foo Bar" - - def test_truncate(self): - tmpl = env.from_string( - '{{ data|truncate(15, true, ">>>") }}|' - '{{ data|truncate(15, false, ">>>") }}|' - '{{ smalldata|truncate(15) }}' - ) - out = tmpl.render(data='foobar baz bar' * 1000, - smalldata='foobar baz bar') - assert out == 'foobar baz barf>>>|foobar baz >>>|foobar baz bar' - - def test_upper(self): - tmpl = env.from_string('{{ "foo"|upper }}') - assert tmpl.render() == 'FOO' - - def test_urlize(self): - tmpl = env.from_string('{{ "foo http://www.example.com/ bar"|urlize }}') - assert tmpl.render() == 'foo '\ - 'http://www.example.com/ bar' - - def test_wordcount(self): - tmpl = env.from_string('{{ "foo bar baz"|wordcount }}') - assert tmpl.render() == '3' - - def test_block(self): - tmpl = env.from_string('{% filter lower|escape %}{% endfilter %}') - assert tmpl.render() == '<hehe>' - - def test_chaining(self): - tmpl = env.from_string('''{{ ['', '']|first|upper|escape }}''') - assert tmpl.render() == '<FOO>' - - def test_sum(self): - tmpl = env.from_string('''{{ [1, 2, 3, 4, 5, 6]|sum }}''') - assert tmpl.render() == '21' - - def test_sum_attributes(self): - tmpl = env.from_string('''{{ values|sum('value') }}''') - assert tmpl.render(values=[ - {'value': 23}, - {'value': 1}, - {'value': 18}, - ]) == '42' - - def test_sum_attributes_nested(self): - tmpl = env.from_string('''{{ values|sum('real.value') }}''') - assert tmpl.render(values=[ - {'real': {'value': 23}}, - {'real': {'value': 1}}, - {'real': {'value': 18}}, - ]) == '42' - - def test_abs(self): - tmpl = env.from_string('''{{ -1|abs }}|{{ 1|abs }}''') - assert tmpl.render() == '1|1', tmpl.render() - - def test_round_positive(self): - tmpl = env.from_string('{{ 2.7|round }}|{{ 2.1|round }}|' - "{{ 2.1234|round(3, 'floor') }}|" - "{{ 2.1|round(0, 'ceil') }}") - assert tmpl.render() == '3.0|2.0|2.123|3.0', tmpl.render() - - def test_round_negative(self): - tmpl = env.from_string('{{ 21.3|round(-1)}}|' - "{{ 21.3|round(-1, 'ceil')}}|" - "{{ 21.3|round(-1, 'floor')}}") - assert tmpl.render() == '20.0|30.0|20.0',tmpl.render() - - def test_xmlattr(self): - tmpl = env.from_string("{{ {'foo': 42, 'bar': 23, 'fish': none, " - "'spam': missing, 'blub:blub': ''}|xmlattr }}") - out = tmpl.render().split() - assert len(out) == 3 - assert 'foo="42"' in out - assert 'bar="23"' in out - assert 'blub:blub="<?>"' in out - - def test_sort1(self): - tmpl = env.from_string('{{ [2, 3, 1]|sort }}|{{ [2, 3, 1]|sort(true) }}') - assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]' - - def test_sort2(self): - tmpl = env.from_string('{{ "".join(["c", "A", "b", "D"]|sort) }}') - assert tmpl.render() == 'AbcD' - - def test_sort3(self): - tmpl = env.from_string('''{{ ['foo', 'Bar', 'blah']|sort }}''') - assert tmpl.render() == "['Bar', 'blah', 'foo']" - - def test_sort4(self): - class Magic(object): - def __init__(self, value): - self.value = value - def __unicode__(self): - return unicode(self.value) - tmpl = env.from_string('''{{ items|sort(attribute='value')|join }}''') - assert tmpl.render(items=map(Magic, [3, 2, 4, 1])) == '1234' - - def test_groupby(self): - tmpl = env.from_string(''' - {%- for grouper, list in [{'foo': 1, 'bar': 2}, - {'foo': 2, 'bar': 3}, - {'foo': 1, 'bar': 1}, - {'foo': 3, 'bar': 4}]|groupby('foo') -%} - {{ grouper }}{% for x in list %}: {{ x.foo }}, {{ x.bar }}{% endfor %}| - {%- endfor %}''') - assert tmpl.render().split('|') == [ - "1: 1, 2: 1, 1", - "2: 2, 3", - "3: 3, 4", - "" - ] - - 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): - self.day = day - self.month = month - self.year = year - class Article(object): - def __init__(self, title, *date): - self.date = Date(*date) - self.title = title - articles = [ - Article('aha', 1, 1, 1970), - Article('interesting', 2, 1, 1970), - Article('really?', 3, 1, 1970), - Article('totally not', 1, 1, 1971) - ] - tmpl = env.from_string(''' - {%- for year, list in articles|groupby('date.year') -%} - {{ year }}{% for x in list %}[{{ x.title }}]{% endfor %}| - {%- endfor %}''') - assert tmpl.render(articles=articles).split('|') == [ - '1970[aha][interesting][really?]', - '1971[totally not]', - '' - ] - - def test_filtertag(self): - tmpl = env.from_string("{% filter upper|replace('FOO', 'foo') %}" - "foobar{% endfilter %}") - assert tmpl.render() == 'fooBAR' - - def test_replace(self): - env = Environment() - tmpl = env.from_string('{{ string|replace("o", 42) }}') - assert tmpl.render(string='') == '' - env = Environment(autoescape=True) - tmpl = env.from_string('{{ string|replace("o", 42) }}') - assert tmpl.render(string='') == '<f4242>' - tmpl = env.from_string('{{ string|replace("<", 42) }}') - assert tmpl.render(string='') == '42foo>' - tmpl = env.from_string('{{ string|replace("o", ">x<") }}') - assert tmpl.render(string=Markup('foo')) == 'f>x<>x<' - - def test_forceescape(self): - tmpl = env.from_string('{{ x|forceescape }}') - assert tmpl.render(x=Markup('
')) == u'<div />' - - def test_safe(self): - env = Environment(autoescape=True) - tmpl = env.from_string('{{ "
foo
"|safe }}') - assert tmpl.render() == '
foo
' - tmpl = env.from_string('{{ "
foo
" }}') - assert tmpl.render() == '<div>foo</div>' - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(FilterTestCase)) - return suite diff --git a/libs/jinja2/testsuite/imports.py b/libs/jinja2/testsuite/imports.py deleted file mode 100644 index 1cb12cbd..00000000 --- a/libs/jinja2/testsuite/imports.py +++ /dev/null @@ -1,141 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.imports - ~~~~~~~~~~~~~~~~~~~~~~~~ - - Tests the import features (with includes). - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import unittest - -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Environment, DictLoader -from jinja2.exceptions import TemplateNotFound, TemplatesNotFound - - -test_env = Environment(loader=DictLoader(dict( - module='{% macro test() %}[{{ foo }}|{{ bar }}]{% endmacro %}', - header='[{{ foo }}|{{ 23 }}]', - o_printer='({{ o }})' -))) -test_env.globals['bar'] = 23 - - -class ImportsTestCase(JinjaTestCase): - - def test_context_imports(self): - t = test_env.from_string('{% import "module" as m %}{{ m.test() }}') - assert t.render(foo=42) == '[|23]' - t = test_env.from_string('{% import "module" as m without context %}{{ m.test() }}') - assert t.render(foo=42) == '[|23]' - t = test_env.from_string('{% import "module" as m with context %}{{ m.test() }}') - assert t.render(foo=42) == '[42|23]' - t = test_env.from_string('{% from "module" import test %}{{ test() }}') - assert t.render(foo=42) == '[|23]' - t = test_env.from_string('{% from "module" import test without context %}{{ test() }}') - assert t.render(foo=42) == '[|23]' - t = test_env.from_string('{% from "module" import test with context %}{{ test() }}') - assert t.render(foo=42) == '[42|23]' - - def test_trailing_comma(self): - test_env.from_string('{% from "foo" import bar, baz with context %}') - test_env.from_string('{% from "foo" import bar, baz, with context %}') - test_env.from_string('{% from "foo" import bar, with context %}') - test_env.from_string('{% from "foo" import bar, with, context %}') - test_env.from_string('{% from "foo" import bar, with with context %}') - - def test_exports(self): - m = test_env.from_string(''' - {% macro toplevel() %}...{% endmacro %} - {% macro __private() %}...{% endmacro %} - {% set variable = 42 %} - {% for item in [1] %} - {% macro notthere() %}{% endmacro %} - {% endfor %} - ''').module - assert m.toplevel() == '...' - assert not hasattr(m, '__missing') - assert m.variable == 42 - assert not hasattr(m, 'notthere') - - -class IncludesTestCase(JinjaTestCase): - - def test_context_include(self): - t = test_env.from_string('{% include "header" %}') - assert t.render(foo=42) == '[42|23]' - t = test_env.from_string('{% include "header" with context %}') - assert t.render(foo=42) == '[42|23]' - t = test_env.from_string('{% include "header" without context %}') - assert t.render(foo=42) == '[|23]' - - def test_choice_includes(self): - t = test_env.from_string('{% include ["missing", "header"] %}') - assert t.render(foo=42) == '[42|23]' - - t = test_env.from_string('{% include ["missing", "missing2"] ignore missing %}') - assert t.render(foo=42) == '' - - t = test_env.from_string('{% include ["missing", "missing2"] %}') - self.assert_raises(TemplateNotFound, t.render) - try: - t.render() - except TemplatesNotFound, e: - assert e.templates == ['missing', 'missing2'] - assert e.name == 'missing2' - else: - assert False, 'thou shalt raise' - - def test_includes(t, **ctx): - ctx['foo'] = 42 - assert t.render(ctx) == '[42|23]' - - t = test_env.from_string('{% include ["missing", "header"] %}') - test_includes(t) - t = test_env.from_string('{% include x %}') - test_includes(t, x=['missing', 'header']) - t = test_env.from_string('{% include [x, "header"] %}') - test_includes(t, x='missing') - t = test_env.from_string('{% include x %}') - test_includes(t, x='header') - t = test_env.from_string('{% include x %}') - test_includes(t, x='header') - t = test_env.from_string('{% include [x] %}') - test_includes(t, x='header') - - def test_include_ignoring_missing(self): - t = test_env.from_string('{% include "missing" %}') - self.assert_raises(TemplateNotFound, t.render) - for extra in '', 'with context', 'without context': - t = test_env.from_string('{% include "missing" ignore missing ' + - extra + ' %}') - assert t.render() == '' - - def test_context_include_with_overrides(self): - env = Environment(loader=DictLoader(dict( - main="{% for item in [1, 2, 3] %}{% include 'item' %}{% endfor %}", - item="{{ item }}" - ))) - assert env.get_template("main").render() == "123" - - def test_unoptimized_scopes(self): - t = test_env.from_string(""" - {% macro outer(o) %} - {% macro inner() %} - {% include "o_printer" %} - {% endmacro %} - {{ inner() }} - {% endmacro %} - {{ outer("FOO") }} - """) - assert t.render().strip() == '(FOO)' - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(ImportsTestCase)) - suite.addTest(unittest.makeSuite(IncludesTestCase)) - return suite diff --git a/libs/jinja2/testsuite/inheritance.py b/libs/jinja2/testsuite/inheritance.py deleted file mode 100644 index 355aa0c9..00000000 --- a/libs/jinja2/testsuite/inheritance.py +++ /dev/null @@ -1,227 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.inheritance - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Tests the template inheritance feature. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import unittest - -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Environment, DictLoader - - -LAYOUTTEMPLATE = '''\ -|{% block block1 %}block 1 from layout{% endblock %} -|{% block block2 %}block 2 from layout{% endblock %} -|{% block block3 %} -{% block block4 %}nested block 4 from layout{% endblock %} -{% endblock %}|''' - -LEVEL1TEMPLATE = '''\ -{% extends "layout" %} -{% block block1 %}block 1 from level1{% endblock %}''' - -LEVEL2TEMPLATE = '''\ -{% extends "level1" %} -{% block block2 %}{% block block5 %}nested block 5 from level2{% -endblock %}{% endblock %}''' - -LEVEL3TEMPLATE = '''\ -{% extends "level2" %} -{% block block5 %}block 5 from level3{% endblock %} -{% block block4 %}block 4 from level3{% endblock %} -''' - -LEVEL4TEMPLATE = '''\ -{% extends "level3" %} -{% block block3 %}block 3 from level4{% endblock %} -''' - -WORKINGTEMPLATE = '''\ -{% extends "layout" %} -{% block block1 %} - {% if false %} - {% block block2 %} - this should workd - {% endblock %} - {% endif %} -{% endblock %} -''' - -env = Environment(loader=DictLoader({ - 'layout': LAYOUTTEMPLATE, - 'level1': LEVEL1TEMPLATE, - 'level2': LEVEL2TEMPLATE, - 'level3': LEVEL3TEMPLATE, - 'level4': LEVEL4TEMPLATE, - 'working': WORKINGTEMPLATE -}), trim_blocks=True) - - -class InheritanceTestCase(JinjaTestCase): - - def test_layout(self): - tmpl = env.get_template('layout') - assert tmpl.render() == ('|block 1 from layout|block 2 from ' - 'layout|nested block 4 from layout|') - - def test_level1(self): - tmpl = env.get_template('level1') - assert tmpl.render() == ('|block 1 from level1|block 2 from ' - 'layout|nested block 4 from layout|') - - def test_level2(self): - tmpl = env.get_template('level2') - assert tmpl.render() == ('|block 1 from level1|nested block 5 from ' - 'level2|nested block 4 from layout|') - - def test_level3(self): - tmpl = env.get_template('level3') - assert tmpl.render() == ('|block 1 from level1|block 5 from level3|' - 'block 4 from level3|') - - def test_level4(sel): - tmpl = env.get_template('level4') - assert tmpl.render() == ('|block 1 from level1|block 5 from ' - 'level3|block 3 from level4|') - - def test_super(self): - env = Environment(loader=DictLoader({ - 'a': '{% block intro %}INTRO{% endblock %}|' - 'BEFORE|{% block data %}INNER{% endblock %}|AFTER', - 'b': '{% extends "a" %}{% block data %}({{ ' - 'super() }}){% endblock %}', - 'c': '{% extends "b" %}{% block intro %}--{{ ' - 'super() }}--{% endblock %}\n{% block data ' - '%}[{{ super() }}]{% endblock %}' - })) - tmpl = env.get_template('c') - assert tmpl.render() == '--INTRO--|BEFORE|[(INNER)]|AFTER' - - def test_working(self): - tmpl = env.get_template('working') - - def test_reuse_blocks(self): - tmpl = env.from_string('{{ self.foo() }}|{% block foo %}42' - '{% endblock %}|{{ self.foo() }}') - assert tmpl.render() == '42|42|42' - - def test_preserve_blocks(self): - env = Environment(loader=DictLoader({ - 'a': '{% if false %}{% block x %}A{% endblock %}{% endif %}{{ self.x() }}', - 'b': '{% extends "a" %}{% block x %}B{{ super() }}{% endblock %}' - })) - tmpl = env.get_template('b') - assert tmpl.render() == 'BA' - - def test_dynamic_inheritance(self): - env = Environment(loader=DictLoader({ - 'master1': 'MASTER1{% block x %}{% endblock %}', - 'master2': 'MASTER2{% block x %}{% endblock %}', - 'child': '{% extends master %}{% block x %}CHILD{% endblock %}' - })) - tmpl = env.get_template('child') - for m in range(1, 3): - assert tmpl.render(master='master%d' % m) == 'MASTER%dCHILD' % m - - def test_multi_inheritance(self): - env = Environment(loader=DictLoader({ - 'master1': 'MASTER1{% block x %}{% endblock %}', - 'master2': 'MASTER2{% block x %}{% endblock %}', - 'child': '''{% if master %}{% extends master %}{% else %}{% extends - 'master1' %}{% endif %}{% block x %}CHILD{% endblock %}''' - })) - tmpl = env.get_template('child') - assert tmpl.render(master='master2') == 'MASTER2CHILD' - assert tmpl.render(master='master1') == 'MASTER1CHILD' - assert tmpl.render() == 'MASTER1CHILD' - - def test_scoped_block(self): - env = Environment(loader=DictLoader({ - 'master.html': '{% for item in seq %}[{% block item scoped %}' - '{% endblock %}]{% endfor %}' - })) - t = env.from_string('{% extends "master.html" %}{% block item %}' - '{{ item }}{% endblock %}') - assert t.render(seq=range(5)) == '[0][1][2][3][4]' - - def test_super_in_scoped_block(self): - env = Environment(loader=DictLoader({ - 'master.html': '{% for item in seq %}[{% block item scoped %}' - '{{ item }}{% endblock %}]{% endfor %}' - })) - t = env.from_string('{% extends "master.html" %}{% block item %}' - '{{ super() }}|{{ item * 2 }}{% endblock %}') - assert t.render(seq=range(5)) == '[0|0][1|2][2|4][3|6][4|8]' - - def test_scoped_block_after_inheritance(self): - env = Environment(loader=DictLoader({ - 'layout.html': ''' - {% block useless %}{% endblock %} - ''', - 'index.html': ''' - {%- extends 'layout.html' %} - {% from 'helpers.html' import foo with context %} - {% block useless %} - {% for x in [1, 2, 3] %} - {% block testing scoped %} - {{ foo(x) }} - {% endblock %} - {% endfor %} - {% endblock %} - ''', - 'helpers.html': ''' - {% macro foo(x) %}{{ the_foo + x }}{% endmacro %} - ''' - })) - rv = env.get_template('index.html').render(the_foo=42).split() - assert rv == ['43', '44', '45'] - - -class BugFixTestCase(JinjaTestCase): - - def test_fixed_macro_scoping_bug(self): - assert Environment(loader=DictLoader({ - 'test.html': '''\ - {% extends 'details.html' %} - - {% macro my_macro() %} - my_macro - {% endmacro %} - - {% block inner_box %} - {{ my_macro() }} - {% endblock %} - ''', - 'details.html': '''\ - {% extends 'standard.html' %} - - {% macro my_macro() %} - my_macro - {% endmacro %} - - {% block content %} - {% block outer_box %} - outer_box - {% block inner_box %} - inner_box - {% endblock %} - {% endblock %} - {% endblock %} - ''', - 'standard.html': ''' - {% block content %} {% endblock %} - ''' - })).get_template("test.html").render().split() == [u'outer_box', u'my_macro'] - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(InheritanceTestCase)) - suite.addTest(unittest.makeSuite(BugFixTestCase)) - return suite diff --git a/libs/jinja2/testsuite/lexnparse.py b/libs/jinja2/testsuite/lexnparse.py deleted file mode 100644 index 562df624..00000000 --- a/libs/jinja2/testsuite/lexnparse.py +++ /dev/null @@ -1,387 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.lexnparse - ~~~~~~~~~~~~~~~~~~~~~~~~~~ - - All the unittests regarding lexing, parsing and syntax. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import sys -import unittest - -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Environment, Template, TemplateSyntaxError, \ - UndefinedError, nodes - -env = Environment() - - -# how does a string look like in jinja syntax? -if sys.version_info < (3, 0): - def jinja_string_repr(string): - return repr(string)[1:] -else: - jinja_string_repr = repr - - -class LexerTestCase(JinjaTestCase): - - def test_raw1(self): - tmpl = env.from_string('{% raw %}foo{% endraw %}|' - '{%raw%}{{ bar }}|{% baz %}{% endraw %}') - assert tmpl.render() == 'foo|{{ bar }}|{% baz %}' - - def test_raw2(self): - tmpl = env.from_string('1 {%- raw -%} 2 {%- endraw -%} 3') - assert tmpl.render() == '123' - - def test_balancing(self): - env = Environment('{%', '%}', '${', '}') - tmpl = env.from_string('''{% for item in seq - %}${{'foo': item}|upper}{% endfor %}''') - assert tmpl.render(seq=range(3)) == "{'FOO': 0}{'FOO': 1}{'FOO': 2}" - - def test_comments(self): - env = Environment('', '{', '}') - tmpl = env.from_string('''\ -
    - -
  • {item}
  • - -
''') - assert tmpl.render(seq=range(3)) == ("
    \n
  • 0
  • \n " - "
  • 1
  • \n
  • 2
  • \n
") - - def test_string_escapes(self): - for char in u'\0', u'\u2668', u'\xe4', u'\t', u'\r', u'\n': - tmpl = env.from_string('{{ %s }}' % jinja_string_repr(char)) - assert tmpl.render() == char - assert env.from_string('{{ "\N{HOT SPRINGS}" }}').render() == u'\u2668' - - def test_bytefallback(self): - from pprint import pformat - tmpl = env.from_string(u'''{{ 'foo'|pprint }}|{{ 'bär'|pprint }}''') - assert tmpl.render() == pformat('foo') + '|' + pformat(u'bär') - - def test_operators(self): - from jinja2.lexer import operators - for test, expect in operators.iteritems(): - if test in '([{}])': - continue - stream = env.lexer.tokenize('{{ %s }}' % test) - stream.next() - assert stream.current.type == expect - - def test_normalizing(self): - for seq in '\r', '\r\n', '\n': - env = Environment(newline_sequence=seq) - tmpl = env.from_string('1\n2\r\n3\n4\n') - result = tmpl.render() - assert result.replace(seq, 'X') == '1X2X3X4' - - -class ParserTestCase(JinjaTestCase): - - def test_php_syntax(self): - env = Environment('', '', '') - tmpl = env.from_string('''\ -\ - - -''') - assert tmpl.render(seq=range(5)) == '01234' - - def test_erb_syntax(self): - env = Environment('<%', '%>', '<%=', '%>', '<%#', '%>') - tmpl = env.from_string('''\ -<%# I'm a comment, I'm not interesting %>\ -<% for item in seq -%> - <%= item %> -<%- endfor %>''') - assert tmpl.render(seq=range(5)) == '01234' - - def test_comment_syntax(self): - env = Environment('', '${', '}', '') - tmpl = env.from_string('''\ -\ - - ${item} -''') - assert tmpl.render(seq=range(5)) == '01234' - - def test_balancing(self): - tmpl = env.from_string('''{{{'foo':'bar'}.foo}}''') - assert tmpl.render() == 'bar' - - def test_start_comment(self): - tmpl = env.from_string('''{# foo comment -and bar comment #} -{% macro blub() %}foo{% endmacro %} -{{ blub() }}''') - assert tmpl.render().strip() == 'foo' - - def test_line_syntax(self): - env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%') - tmpl = env.from_string('''\ -<%# regular comment %> -% for item in seq: - ${item} -% endfor''') - assert [int(x.strip()) for x in tmpl.render(seq=range(5)).split()] == \ - range(5) - - env = Environment('<%', '%>', '${', '}', '<%#', '%>', '%', '##') - tmpl = env.from_string('''\ -<%# regular comment %> -% for item in seq: - ${item} ## the rest of the stuff -% endfor''') - assert [int(x.strip()) for x in tmpl.render(seq=range(5)).split()] == \ - range(5) - - def test_line_syntax_priority(self): - # XXX: why is the whitespace there in front of the newline? - env = Environment('{%', '%}', '${', '}', '/*', '*/', '##', '#') - tmpl = env.from_string('''\ -/* ignore me. - I'm a multiline comment */ -## for item in seq: -* ${item} # this is just extra stuff -## endfor''') - assert tmpl.render(seq=[1, 2]).strip() == '* 1\n* 2' - env = Environment('{%', '%}', '${', '}', '/*', '*/', '#', '##') - tmpl = env.from_string('''\ -/* ignore me. - I'm a multiline comment */ -# for item in seq: -* ${item} ## this is just extra stuff - ## extra stuff i just want to ignore -# endfor''') - assert tmpl.render(seq=[1, 2]).strip() == '* 1\n\n* 2' - - def test_error_messages(self): - def assert_error(code, expected): - try: - Template(code) - except TemplateSyntaxError, e: - assert str(e) == expected, 'unexpected error message' - else: - assert False, 'that was suposed to be an error' - - assert_error('{% for item in seq %}...{% endif %}', - "Encountered unknown tag 'endif'. Jinja was looking " - "for the following tags: 'endfor' or 'else'. The " - "innermost block that needs to be closed is 'for'.") - assert_error('{% if foo %}{% for item in seq %}...{% endfor %}{% endfor %}', - "Encountered unknown tag 'endfor'. Jinja was looking for " - "the following tags: 'elif' or 'else' or 'endif'. The " - "innermost block that needs to be closed is 'if'.") - assert_error('{% if foo %}', - "Unexpected end of template. Jinja was looking for the " - "following tags: 'elif' or 'else' or 'endif'. The " - "innermost block that needs to be closed is 'if'.") - assert_error('{% for item in seq %}', - "Unexpected end of template. Jinja was looking for the " - "following tags: 'endfor' or 'else'. The innermost block " - "that needs to be closed is 'for'.") - assert_error('{% block foo-bar-baz %}', - "Block names in Jinja have to be valid Python identifiers " - "and may not contain hypens, use an underscore instead.") - assert_error('{% unknown_tag %}', - "Encountered unknown tag 'unknown_tag'.") - - -class SyntaxTestCase(JinjaTestCase): - - def test_call(self): - env = Environment() - env.globals['foo'] = lambda a, b, c, e, g: a + b + c + e + g - tmpl = env.from_string("{{ foo('a', c='d', e='f', *['b'], **{'g': 'h'}) }}") - assert tmpl.render() == 'abdfh' - - def test_slicing(self): - tmpl = env.from_string('{{ [1, 2, 3][:] }}|{{ [1, 2, 3][::-1] }}') - assert tmpl.render() == '[1, 2, 3]|[3, 2, 1]' - - def test_attr(self): - tmpl = env.from_string("{{ foo.bar }}|{{ foo['bar'] }}") - assert tmpl.render(foo={'bar': 42}) == '42|42' - - def test_subscript(self): - tmpl = env.from_string("{{ foo[0] }}|{{ foo[-1] }}") - assert tmpl.render(foo=[0, 1, 2]) == '0|2' - - def test_tuple(self): - tmpl = env.from_string('{{ () }}|{{ (1,) }}|{{ (1, 2) }}') - assert tmpl.render() == '()|(1,)|(1, 2)' - - def test_math(self): - tmpl = env.from_string('{{ (1 + 1 * 2) - 3 / 2 }}|{{ 2**3 }}') - assert tmpl.render() == '1.5|8' - - def test_div(self): - tmpl = env.from_string('{{ 3 // 2 }}|{{ 3 / 2 }}|{{ 3 % 2 }}') - assert tmpl.render() == '1|1.5|1' - - def test_unary(self): - tmpl = env.from_string('{{ +3 }}|{{ -3 }}') - assert tmpl.render() == '3|-3' - - def test_concat(self): - tmpl = env.from_string("{{ [1, 2] ~ 'foo' }}") - assert tmpl.render() == '[1, 2]foo' - - def test_compare(self): - tmpl = env.from_string('{{ 1 > 0 }}|{{ 1 >= 1 }}|{{ 2 < 3 }}|' - '{{ 2 == 2 }}|{{ 1 <= 1 }}') - assert tmpl.render() == 'True|True|True|True|True' - - def test_inop(self): - tmpl = env.from_string('{{ 1 in [1, 2, 3] }}|{{ 1 not in [1, 2, 3] }}') - assert tmpl.render() == 'True|False' - - def test_literals(self): - tmpl = env.from_string('{{ [] }}|{{ {} }}|{{ () }}') - assert tmpl.render().lower() == '[]|{}|()' - - def test_bool(self): - tmpl = env.from_string('{{ true and false }}|{{ false ' - 'or true }}|{{ not false }}') - assert tmpl.render() == 'False|True|True' - - def test_grouping(self): - tmpl = env.from_string('{{ (true and false) or (false and true) and not false }}') - assert tmpl.render() == 'False' - - def test_django_attr(self): - tmpl = env.from_string('{{ [1, 2, 3].0 }}|{{ [[1]].0.0 }}') - assert tmpl.render() == '1|1' - - def test_conditional_expression(self): - tmpl = env.from_string('''{{ 0 if true else 1 }}''') - assert tmpl.render() == '0' - - def test_short_conditional_expression(self): - tmpl = env.from_string('<{{ 1 if false }}>') - assert tmpl.render() == '<>' - - tmpl = env.from_string('<{{ (1 if false).bar }}>') - self.assert_raises(UndefinedError, tmpl.render) - - def test_filter_priority(self): - tmpl = env.from_string('{{ "foo"|upper + "bar"|upper }}') - assert tmpl.render() == 'FOOBAR' - - def test_function_calls(self): - tests = [ - (True, '*foo, bar'), - (True, '*foo, *bar'), - (True, '*foo, bar=42'), - (True, '**foo, *bar'), - (True, '**foo, bar'), - (False, 'foo, bar'), - (False, 'foo, bar=42'), - (False, 'foo, bar=23, *args'), - (False, 'a, b=c, *d, **e'), - (False, '*foo, **bar') - ] - for should_fail, sig in tests: - if should_fail: - self.assert_raises(TemplateSyntaxError, - env.from_string, '{{ foo(%s) }}' % sig) - else: - env.from_string('foo(%s)' % sig) - - def test_tuple_expr(self): - for tmpl in [ - '{{ () }}', - '{{ (1, 2) }}', - '{{ (1, 2,) }}', - '{{ 1, }}', - '{{ 1, 2 }}', - '{% for foo, bar in seq %}...{% endfor %}', - '{% for x in foo, bar %}...{% endfor %}', - '{% for x in foo, %}...{% endfor %}' - ]: - assert env.from_string(tmpl) - - def test_trailing_comma(self): - tmpl = env.from_string('{{ (1, 2,) }}|{{ [1, 2,] }}|{{ {1: 2,} }}') - assert tmpl.render().lower() == '(1, 2)|[1, 2]|{1: 2}' - - def test_block_end_name(self): - env.from_string('{% block foo %}...{% endblock foo %}') - self.assert_raises(TemplateSyntaxError, env.from_string, - '{% block x %}{% endblock y %}') - - def test_contant_casing(self): - for const in True, False, None: - tmpl = env.from_string('{{ %s }}|{{ %s }}|{{ %s }}' % ( - str(const), str(const).lower(), str(const).upper() - )) - assert tmpl.render() == '%s|%s|' % (const, const) - - def test_test_chaining(self): - self.assert_raises(TemplateSyntaxError, env.from_string, - '{{ foo is string is sequence }}') - env.from_string('{{ 42 is string or 42 is number }}' - ).render() == 'True' - - def test_string_concatenation(self): - tmpl = env.from_string('{{ "foo" "bar" "baz" }}') - assert tmpl.render() == 'foobarbaz' - - def test_notin(self): - bar = xrange(100) - tmpl = env.from_string('''{{ not 42 in bar }}''') - assert tmpl.render(bar=bar) == unicode(not 42 in bar) - - def test_implicit_subscribed_tuple(self): - class Foo(object): - def __getitem__(self, x): - return x - t = env.from_string('{{ foo[1, 2] }}') - assert t.render(foo=Foo()) == u'(1, 2)' - - def test_raw2(self): - tmpl = env.from_string('{% raw %}{{ FOO }} and {% BAR %}{% endraw %}') - assert tmpl.render() == '{{ FOO }} and {% BAR %}' - - def test_const(self): - tmpl = env.from_string('{{ true }}|{{ false }}|{{ none }}|' - '{{ none is defined }}|{{ missing is defined }}') - assert tmpl.render() == 'True|False|None|True|False' - - def test_neg_filter_priority(self): - node = env.parse('{{ -1|foo }}') - assert isinstance(node.body[0].nodes[0], nodes.Filter) - assert isinstance(node.body[0].nodes[0].node, nodes.Neg) - - def test_const_assign(self): - constass1 = '''{% set true = 42 %}''' - constass2 = '''{% for none in seq %}{% endfor %}''' - for tmpl in constass1, constass2: - self.assert_raises(TemplateSyntaxError, env.from_string, tmpl) - - def test_localset(self): - tmpl = env.from_string('''{% set foo = 0 %}\ -{% for item in [1, 2] %}{% set foo = 1 %}{% endfor %}\ -{{ foo }}''') - assert tmpl.render() == '0' - - def test_parse_unary(self): - tmpl = env.from_string('{{ -foo["bar"] }}') - assert tmpl.render(foo={'bar': 42}) == '-42' - tmpl = env.from_string('{{ -foo["bar"]|abs }}') - assert tmpl.render(foo={'bar': 42}) == '42' - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(LexerTestCase)) - suite.addTest(unittest.makeSuite(ParserTestCase)) - suite.addTest(unittest.makeSuite(SyntaxTestCase)) - return suite diff --git a/libs/jinja2/testsuite/loader.py b/libs/jinja2/testsuite/loader.py deleted file mode 100644 index fb1e53d4..00000000 --- a/libs/jinja2/testsuite/loader.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.loader - ~~~~~~~~~~~~~~~~~~~~~~~ - - Test the loaders. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import os -import sys -import tempfile -import shutil -import unittest - -from jinja2.testsuite import JinjaTestCase, dict_loader, \ - package_loader, filesystem_loader, function_loader, \ - choice_loader, prefix_loader - -from jinja2 import Environment, loaders -from jinja2.loaders import split_template_path -from jinja2.exceptions import TemplateNotFound - - -class LoaderTestCase(JinjaTestCase): - - def test_dict_loader(self): - env = Environment(loader=dict_loader) - tmpl = env.get_template('justdict.html') - assert tmpl.render().strip() == 'FOO' - self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') - - def test_package_loader(self): - env = Environment(loader=package_loader) - tmpl = env.get_template('test.html') - assert tmpl.render().strip() == 'BAR' - self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') - - def test_filesystem_loader(self): - env = Environment(loader=filesystem_loader) - tmpl = env.get_template('test.html') - assert tmpl.render().strip() == 'BAR' - tmpl = env.get_template('foo/test.html') - assert tmpl.render().strip() == 'FOO' - self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') - - def test_choice_loader(self): - env = Environment(loader=choice_loader) - tmpl = env.get_template('justdict.html') - assert tmpl.render().strip() == 'FOO' - tmpl = env.get_template('test.html') - assert tmpl.render().strip() == 'BAR' - self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') - - def test_function_loader(self): - env = Environment(loader=function_loader) - tmpl = env.get_template('justfunction.html') - assert tmpl.render().strip() == 'FOO' - self.assert_raises(TemplateNotFound, env.get_template, 'missing.html') - - def test_prefix_loader(self): - env = Environment(loader=prefix_loader) - tmpl = env.get_template('a/test.html') - assert tmpl.render().strip() == 'BAR' - tmpl = env.get_template('b/justdict.html') - assert tmpl.render().strip() == 'FOO' - self.assert_raises(TemplateNotFound, env.get_template, 'missing') - - def test_caching(self): - changed = False - class TestLoader(loaders.BaseLoader): - def get_source(self, environment, template): - return u'foo', None, lambda: not changed - env = Environment(loader=TestLoader(), cache_size=-1) - tmpl = env.get_template('template') - assert tmpl is env.get_template('template') - changed = True - assert tmpl is not env.get_template('template') - changed = False - - env = Environment(loader=TestLoader(), cache_size=0) - assert env.get_template('template') \ - is not env.get_template('template') - - env = Environment(loader=TestLoader(), cache_size=2) - t1 = env.get_template('one') - t2 = env.get_template('two') - assert t2 is env.get_template('two') - assert t1 is env.get_template('one') - t3 = env.get_template('three') - assert 'one' in env.cache - assert 'two' not in env.cache - assert 'three' in env.cache - - def test_split_template_path(self): - assert split_template_path('foo/bar') == ['foo', 'bar'] - assert split_template_path('./foo/bar') == ['foo', 'bar'] - self.assert_raises(TemplateNotFound, split_template_path, '../foo') - - -class ModuleLoaderTestCase(JinjaTestCase): - archive = None - - def compile_down(self, zip='deflated', py_compile=False): - super(ModuleLoaderTestCase, self).setup() - log = [] - self.reg_env = Environment(loader=prefix_loader) - if zip is not None: - self.archive = tempfile.mkstemp(suffix='.zip')[1] - else: - self.archive = tempfile.mkdtemp() - self.reg_env.compile_templates(self.archive, zip=zip, - log_function=log.append, - py_compile=py_compile) - self.mod_env = Environment(loader=loaders.ModuleLoader(self.archive)) - return ''.join(log) - - def teardown(self): - super(ModuleLoaderTestCase, self).teardown() - if hasattr(self, 'mod_env'): - if os.path.isfile(self.archive): - os.remove(self.archive) - else: - shutil.rmtree(self.archive) - self.archive = None - - def test_log(self): - log = self.compile_down() - assert 'Compiled "a/foo/test.html" as ' \ - 'tmpl_a790caf9d669e39ea4d280d597ec891c4ef0404a' in log - assert 'Finished compiling templates' in log - assert 'Could not compile "a/syntaxerror.html": ' \ - 'Encountered unknown tag \'endif\'' in log - - def _test_common(self): - tmpl1 = self.reg_env.get_template('a/test.html') - tmpl2 = self.mod_env.get_template('a/test.html') - assert tmpl1.render() == tmpl2.render() - - tmpl1 = self.reg_env.get_template('b/justdict.html') - tmpl2 = self.mod_env.get_template('b/justdict.html') - assert tmpl1.render() == tmpl2.render() - - def test_deflated_zip_compile(self): - self.compile_down(zip='deflated') - self._test_common() - - def test_stored_zip_compile(self): - self.compile_down(zip='stored') - self._test_common() - - def test_filesystem_compile(self): - self.compile_down(zip=None) - self._test_common() - - def test_weak_references(self): - self.compile_down() - tmpl = self.mod_env.get_template('a/test.html') - key = loaders.ModuleLoader.get_template_key('a/test.html') - name = self.mod_env.loader.module.__name__ - - assert hasattr(self.mod_env.loader.module, key) - assert name in sys.modules - - # unset all, ensure the module is gone from sys.modules - self.mod_env = tmpl = None - - try: - import gc - gc.collect() - except: - pass - - assert name not in sys.modules - - def test_byte_compilation(self): - log = self.compile_down(py_compile=True) - assert 'Byte-compiled "a/test.html"' in log - tmpl1 = self.mod_env.get_template('a/test.html') - mod = self.mod_env.loader.module. \ - tmpl_3c4ddf650c1a73df961a6d3d2ce2752f1b8fd490 - assert mod.__file__.endswith('.pyc') - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(LoaderTestCase)) - suite.addTest(unittest.makeSuite(ModuleLoaderTestCase)) - return suite diff --git a/libs/jinja2/testsuite/regression.py b/libs/jinja2/testsuite/regression.py deleted file mode 100644 index 4db90769..00000000 --- a/libs/jinja2/testsuite/regression.py +++ /dev/null @@ -1,255 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.regression - ~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - Tests corner cases and bugs. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import unittest - -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Template, Environment, DictLoader, TemplateSyntaxError, \ - TemplateNotFound, PrefixLoader - -env = Environment() - - -class CornerTestCase(JinjaTestCase): - - def test_assigned_scoping(self): - t = env.from_string(''' - {%- for item in (1, 2, 3, 4) -%} - [{{ item }}] - {%- endfor %} - {{- item -}} - ''') - assert t.render(item=42) == '[1][2][3][4]42' - - t = env.from_string(''' - {%- for item in (1, 2, 3, 4) -%} - [{{ item }}] - {%- endfor %} - {%- set item = 42 %} - {{- item -}} - ''') - assert t.render() == '[1][2][3][4]42' - - t = env.from_string(''' - {%- set item = 42 %} - {%- for item in (1, 2, 3, 4) -%} - [{{ item }}] - {%- endfor %} - {{- item -}} - ''') - assert t.render() == '[1][2][3][4]42' - - def test_closure_scoping(self): - t = env.from_string(''' - {%- set wrapper = "" %} - {%- for item in (1, 2, 3, 4) %} - {%- macro wrapper() %}[{{ item }}]{% endmacro %} - {{- wrapper() }} - {%- endfor %} - {{- wrapper -}} - ''') - assert t.render() == '[1][2][3][4]' - - t = env.from_string(''' - {%- for item in (1, 2, 3, 4) %} - {%- macro wrapper() %}[{{ item }}]{% endmacro %} - {{- wrapper() }} - {%- endfor %} - {%- set wrapper = "" %} - {{- wrapper -}} - ''') - assert t.render() == '[1][2][3][4]' - - t = env.from_string(''' - {%- for item in (1, 2, 3, 4) %} - {%- macro wrapper() %}[{{ item }}]{% endmacro %} - {{- wrapper() }} - {%- endfor %} - {{- wrapper -}} - ''') - assert t.render(wrapper=23) == '[1][2][3][4]23' - - -class BugTestCase(JinjaTestCase): - - def test_keyword_folding(self): - env = Environment() - env.filters['testing'] = lambda value, some: value + some - assert env.from_string("{{ 'test'|testing(some='stuff') }}") \ - .render() == 'teststuff' - - def test_extends_output_bugs(self): - env = Environment(loader=DictLoader({ - 'parent.html': '(({% block title %}{% endblock %}))' - })) - - t = env.from_string('{% if expr %}{% extends "parent.html" %}{% endif %}' - '[[{% block title %}title{% endblock %}]]' - '{% for item in [1, 2, 3] %}({{ item }}){% endfor %}') - assert t.render(expr=False) == '[[title]](1)(2)(3)' - assert t.render(expr=True) == '((title))' - - def test_urlize_filter_escaping(self): - tmpl = env.from_string('{{ "http://www.example.org/http://www.example.org/<foo' - - def test_loop_call_loop(self): - tmpl = env.from_string(''' - - {% macro test() %} - {{ caller() }} - {% endmacro %} - - {% for num1 in range(5) %} - {% call test() %} - {% for num2 in range(10) %} - {{ loop.index }} - {% endfor %} - {% endcall %} - {% endfor %} - - ''') - - assert tmpl.render().split() == map(unicode, range(1, 11)) * 5 - - def test_weird_inline_comment(self): - env = Environment(line_statement_prefix='%') - self.assert_raises(TemplateSyntaxError, env.from_string, - '% for item in seq {# missing #}\n...% endfor') - - def test_old_macro_loop_scoping_bug(self): - tmpl = env.from_string('{% for i in (1, 2) %}{{ i }}{% endfor %}' - '{% macro i() %}3{% endmacro %}{{ i() }}') - assert tmpl.render() == '123' - - def test_partial_conditional_assignments(self): - tmpl = env.from_string('{% if b %}{% set a = 42 %}{% endif %}{{ a }}') - assert tmpl.render(a=23) == '23' - assert tmpl.render(b=True) == '42' - - def test_stacked_locals_scoping_bug(self): - env = Environment(line_statement_prefix='#') - t = env.from_string('''\ -# for j in [1, 2]: -# set x = 1 -# for i in [1, 2]: -# print x -# if i % 2 == 0: -# set x = x + 1 -# endif -# endfor -# endfor -# if a -# print 'A' -# elif b -# print 'B' -# elif c == d -# print 'C' -# else -# print 'D' -# endif - ''') - assert t.render(a=0, b=False, c=42, d=42.0) == '1111C' - - def test_stacked_locals_scoping_bug_twoframe(self): - t = Template(''' - {% set x = 1 %} - {% for item in foo %} - {% if item == 1 %} - {% set x = 2 %} - {% endif %} - {% endfor %} - {{ x }} - ''') - rv = t.render(foo=[1]).strip() - assert rv == u'1' - - def test_call_with_args(self): - t = Template("""{% macro dump_users(users) -%} -
    - {%- for user in users -%} -
  • {{ user.username|e }}

    {{ caller(user) }}
  • - {%- endfor -%} -
- {%- endmacro -%} - - {% call(user) dump_users(list_of_user) -%} -
-
Realname
-
{{ user.realname|e }}
-
Description
-
{{ user.description }}
-
- {% endcall %}""") - - assert [x.strip() for x in t.render(list_of_user=[{ - 'username':'apo', - 'realname':'something else', - 'description':'test' - }]).splitlines()] == [ - u'
  • apo

    ', - u'
    Realname
    ', - u'
    something else
    ', - u'
    Description
    ', - u'
    test
    ', - u'
    ', - u'
' - ] - - def test_empty_if_condition_fails(self): - self.assert_raises(TemplateSyntaxError, Template, '{% if %}....{% endif %}') - self.assert_raises(TemplateSyntaxError, Template, '{% if foo %}...{% elif %}...{% endif %}') - self.assert_raises(TemplateSyntaxError, Template, '{% for x in %}..{% endfor %}') - - def test_recursive_loop_bug(self): - tpl1 = Template(""" - {% for p in foo recursive%} - {{p.bar}} - {% for f in p.fields recursive%} - {{f.baz}} - {{p.bar}} - {% if f.rec %} - {{ loop(f.sub) }} - {% endif %} - {% endfor %} - {% endfor %} - """) - - tpl2 = Template(""" - {% for p in foo%} - {{p.bar}} - {% for f in p.fields recursive%} - {{f.baz}} - {{p.bar}} - {% if f.rec %} - {{ loop(f.sub) }} - {% endif %} - {% endfor %} - {% endfor %} - """) - - def test_correct_prefix_loader_name(self): - env = Environment(loader=PrefixLoader({ - 'foo': DictLoader({}) - })) - try: - env.get_template('foo/bar.html') - except TemplateNotFound, e: - assert e.name == 'foo/bar.html' - else: - assert False, 'expected error here' - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(CornerTestCase)) - suite.addTest(unittest.makeSuite(BugTestCase)) - return suite diff --git a/libs/jinja2/testsuite/res/__init__.py b/libs/jinja2/testsuite/res/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/libs/jinja2/testsuite/res/templates/broken.html b/libs/jinja2/testsuite/res/templates/broken.html deleted file mode 100644 index 77669fae..00000000 --- a/libs/jinja2/testsuite/res/templates/broken.html +++ /dev/null @@ -1,3 +0,0 @@ -Before -{{ fail() }} -After diff --git a/libs/jinja2/testsuite/res/templates/foo/test.html b/libs/jinja2/testsuite/res/templates/foo/test.html deleted file mode 100644 index b7d6715e..00000000 --- a/libs/jinja2/testsuite/res/templates/foo/test.html +++ /dev/null @@ -1 +0,0 @@ -FOO diff --git a/libs/jinja2/testsuite/res/templates/syntaxerror.html b/libs/jinja2/testsuite/res/templates/syntaxerror.html deleted file mode 100644 index f21b8179..00000000 --- a/libs/jinja2/testsuite/res/templates/syntaxerror.html +++ /dev/null @@ -1,4 +0,0 @@ -Foo -{% for item in broken %} - ... -{% endif %} diff --git a/libs/jinja2/testsuite/res/templates/test.html b/libs/jinja2/testsuite/res/templates/test.html deleted file mode 100644 index ba578e48..00000000 --- a/libs/jinja2/testsuite/res/templates/test.html +++ /dev/null @@ -1 +0,0 @@ -BAR diff --git a/libs/jinja2/testsuite/security.py b/libs/jinja2/testsuite/security.py deleted file mode 100644 index 4518eac6..00000000 --- a/libs/jinja2/testsuite/security.py +++ /dev/null @@ -1,165 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.security - ~~~~~~~~~~~~~~~~~~~~~~~~~ - - Checks the sandbox and other security features. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import unittest - -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Environment -from jinja2.sandbox import SandboxedEnvironment, \ - ImmutableSandboxedEnvironment, unsafe -from jinja2 import Markup, escape -from jinja2.exceptions import SecurityError, TemplateSyntaxError, \ - TemplateRuntimeError - - -class PrivateStuff(object): - - def bar(self): - return 23 - - @unsafe - def foo(self): - return 42 - - def __repr__(self): - return 'PrivateStuff' - - -class PublicStuff(object): - bar = lambda self: 23 - _foo = lambda self: 42 - - def __repr__(self): - return 'PublicStuff' - - -class SandboxTestCase(JinjaTestCase): - - def test_unsafe(self): - env = SandboxedEnvironment() - self.assert_raises(SecurityError, env.from_string("{{ foo.foo() }}").render, - foo=PrivateStuff()) - self.assert_equal(env.from_string("{{ foo.bar() }}").render(foo=PrivateStuff()), '23') - - self.assert_raises(SecurityError, env.from_string("{{ foo._foo() }}").render, - foo=PublicStuff()) - self.assert_equal(env.from_string("{{ foo.bar() }}").render(foo=PublicStuff()), '23') - self.assert_equal(env.from_string("{{ foo.__class__ }}").render(foo=42), '') - self.assert_equal(env.from_string("{{ foo.func_code }}").render(foo=lambda:None), '') - # security error comes from __class__ already. - self.assert_raises(SecurityError, env.from_string( - "{{ foo.__class__.__subclasses__() }}").render, foo=42) - - def test_immutable_environment(self): - env = ImmutableSandboxedEnvironment() - self.assert_raises(SecurityError, env.from_string( - '{{ [].append(23) }}').render) - self.assert_raises(SecurityError, env.from_string( - '{{ {1:2}.clear() }}').render) - - def test_restricted(self): - env = SandboxedEnvironment() - self.assert_raises(TemplateSyntaxError, env.from_string, - "{% for item.attribute in seq %}...{% endfor %}") - self.assert_raises(TemplateSyntaxError, env.from_string, - "{% for foo, bar.baz in seq %}...{% endfor %}") - - def test_markup_operations(self): - # adding two strings should escape the unsafe one - unsafe = '' - safe = Markup('username') - assert unsafe + safe == unicode(escape(unsafe)) + unicode(safe) - - # string interpolations are safe to use too - assert Markup('%s') % '' == \ - '<bad user>' - assert Markup('%(username)s') % { - 'username': '' - } == '<bad user>' - - # an escaped object is markup too - assert type(Markup('foo') + 'bar') is Markup - - # and it implements __html__ by returning itself - x = Markup("foo") - assert x.__html__() is x - - # it also knows how to treat __html__ objects - class Foo(object): - def __html__(self): - return 'awesome' - def __unicode__(self): - return 'awesome' - assert Markup(Foo()) == 'awesome' - assert Markup('%s') % Foo() == \ - 'awesome' - - # escaping and unescaping - assert escape('"<>&\'') == '"<>&'' - assert Markup("Foo & Bar").striptags() == "Foo & Bar" - assert Markup("<test>").unescape() == "" - - def test_template_data(self): - env = Environment(autoescape=True) - t = env.from_string('{% macro say_hello(name) %}' - '

Hello {{ name }}!

{% endmacro %}' - '{{ say_hello("foo") }}') - escaped_out = '

Hello <blink>foo</blink>!

' - assert t.render() == escaped_out - assert unicode(t.module) == escaped_out - assert escape(t.module) == escaped_out - assert t.module.say_hello('foo') == escaped_out - assert escape(t.module.say_hello('foo')) == escaped_out - - def test_attr_filter(self): - env = SandboxedEnvironment() - tmpl = env.from_string('{{ cls|attr("__subclasses__")() }}') - self.assert_raises(SecurityError, tmpl.render, cls=int) - - def test_binary_operator_intercepting(self): - def disable_op(left, right): - raise TemplateRuntimeError('that operator so does not work') - for expr, ctx, rv in ('1 + 2', {}, '3'), ('a + 2', {'a': 2}, '4'): - env = SandboxedEnvironment() - env.binop_table['+'] = disable_op - t = env.from_string('{{ %s }}' % expr) - assert t.render(ctx) == rv - env.intercepted_binops = frozenset(['+']) - t = env.from_string('{{ %s }}' % expr) - try: - t.render(ctx) - except TemplateRuntimeError, e: - pass - else: - self.fail('expected runtime error') - - def test_unary_operator_intercepting(self): - def disable_op(arg): - raise TemplateRuntimeError('that operator so does not work') - for expr, ctx, rv in ('-1', {}, '-1'), ('-a', {'a': 2}, '-2'): - env = SandboxedEnvironment() - env.unop_table['-'] = disable_op - t = env.from_string('{{ %s }}' % expr) - assert t.render(ctx) == rv - env.intercepted_unops = frozenset(['-']) - t = env.from_string('{{ %s }}' % expr) - try: - t.render(ctx) - except TemplateRuntimeError, e: - pass - else: - self.fail('expected runtime error') - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(SandboxTestCase)) - return suite diff --git a/libs/jinja2/testsuite/tests.py b/libs/jinja2/testsuite/tests.py deleted file mode 100644 index 3ece7a8f..00000000 --- a/libs/jinja2/testsuite/tests.py +++ /dev/null @@ -1,93 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.tests - ~~~~~~~~~~~~~~~~~~~~~~ - - Who tests the tests? - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import unittest -from jinja2.testsuite import JinjaTestCase - -from jinja2 import Markup, Environment - -env = Environment() - - -class TestsTestCase(JinjaTestCase): - - def test_defined(self): - tmpl = env.from_string('{{ missing is defined }}|{{ true is defined }}') - assert tmpl.render() == 'False|True' - - def test_even(self): - tmpl = env.from_string('''{{ 1 is even }}|{{ 2 is even }}''') - assert tmpl.render() == 'False|True' - - def test_odd(self): - tmpl = env.from_string('''{{ 1 is odd }}|{{ 2 is odd }}''') - assert tmpl.render() == 'True|False' - - def test_lower(self): - tmpl = env.from_string('''{{ "foo" is lower }}|{{ "FOO" is lower }}''') - assert tmpl.render() == 'True|False' - - def test_typechecks(self): - tmpl = env.from_string(''' - {{ 42 is undefined }} - {{ 42 is defined }} - {{ 42 is none }} - {{ none is none }} - {{ 42 is number }} - {{ 42 is string }} - {{ "foo" is string }} - {{ "foo" is sequence }} - {{ [1] is sequence }} - {{ range is callable }} - {{ 42 is callable }} - {{ range(5) is iterable }} - {{ {} is mapping }} - {{ mydict is mapping }} - {{ [] is mapping }} - ''') - class MyDict(dict): - pass - assert tmpl.render(mydict=MyDict()).split() == [ - 'False', 'True', 'False', 'True', 'True', 'False', - 'True', 'True', 'True', 'True', 'False', 'True', - 'True', 'True', 'False' - ] - - def test_sequence(self): - tmpl = env.from_string( - '{{ [1, 2, 3] is sequence }}|' - '{{ "foo" is sequence }}|' - '{{ 42 is sequence }}' - ) - assert tmpl.render() == 'True|True|False' - - def test_upper(self): - tmpl = env.from_string('{{ "FOO" is upper }}|{{ "foo" is upper }}') - assert tmpl.render() == 'True|False' - - def test_sameas(self): - tmpl = env.from_string('{{ foo is sameas false }}|' - '{{ 0 is sameas false }}') - assert tmpl.render(foo=False) == 'True|False' - - def test_no_paren_for_arg1(self): - tmpl = env.from_string('{{ foo is sameas none }}') - assert tmpl.render(foo=None) == 'True' - - def test_escaped(self): - env = Environment(autoescape=True) - tmpl = env.from_string('{{ x is escaped }}|{{ y is escaped }}') - assert tmpl.render(x='foo', y=Markup('foo')) == 'False|True' - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(TestsTestCase)) - return suite diff --git a/libs/jinja2/testsuite/utils.py b/libs/jinja2/testsuite/utils.py deleted file mode 100644 index be2e902f..00000000 --- a/libs/jinja2/testsuite/utils.py +++ /dev/null @@ -1,82 +0,0 @@ -# -*- coding: utf-8 -*- -""" - jinja2.testsuite.utils - ~~~~~~~~~~~~~~~~~~~~~~ - - Tests utilities jinja uses. - - :copyright: (c) 2010 by the Jinja Team. - :license: BSD, see LICENSE for more details. -""" -import gc -import unittest - -import pickle - -from jinja2.testsuite import JinjaTestCase - -from jinja2.utils import LRUCache, escape, object_type_repr - - -class LRUCacheTestCase(JinjaTestCase): - - def test_simple(self): - d = LRUCache(3) - d["a"] = 1 - d["b"] = 2 - d["c"] = 3 - d["a"] - d["d"] = 4 - assert len(d) == 3 - assert 'a' in d and 'c' in d and 'd' in d and 'b' not in d - - def test_pickleable(self): - cache = LRUCache(2) - cache["foo"] = 42 - cache["bar"] = 23 - cache["foo"] - - for protocol in range(3): - copy = pickle.loads(pickle.dumps(cache, protocol)) - assert copy.capacity == cache.capacity - assert copy._mapping == cache._mapping - assert copy._queue == cache._queue - - -class HelpersTestCase(JinjaTestCase): - - def test_object_type_repr(self): - class X(object): - pass - self.assert_equal(object_type_repr(42), 'int object') - self.assert_equal(object_type_repr([]), 'list object') - self.assert_equal(object_type_repr(X()), - 'jinja2.testsuite.utils.X object') - self.assert_equal(object_type_repr(None), 'None') - self.assert_equal(object_type_repr(Ellipsis), 'Ellipsis') - - -class MarkupLeakTestCase(JinjaTestCase): - - def test_markup_leaks(self): - counts = set() - for count in xrange(20): - for item in xrange(1000): - escape("foo") - escape("") - escape(u"foo") - escape(u"") - counts.add(len(gc.get_objects())) - assert len(counts) == 1, 'ouch, c extension seems to leak objects' - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(LRUCacheTestCase)) - suite.addTest(unittest.makeSuite(HelpersTestCase)) - - # this test only tests the c extension - if not hasattr(escape, 'func_code'): - suite.addTest(unittest.makeSuite(MarkupLeakTestCase)) - - return suite diff --git a/libs/jinja2/utils.py b/libs/jinja2/utils.py old mode 100644 new mode 100755 diff --git a/libs/jinja2/visitor.py b/libs/jinja2/visitor.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/__init__.py b/libs/werkzeug/__init__.py old mode 100644 new mode 100755 index fea2c985..efe0eb7d --- a/libs/werkzeug/__init__.py +++ b/libs/werkzeug/__init__.py @@ -19,7 +19,7 @@ import sys # the version. Usually set automatically by a script. -__version__ = '0.8-dev' +__version__ = '0.9-dev' # This import magic raises concerns quite often which is why the implementation diff --git a/libs/werkzeug/_internal.py b/libs/werkzeug/_internal.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/contrib/__init__.py b/libs/werkzeug/contrib/__init__.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/contrib/atom.py b/libs/werkzeug/contrib/atom.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/contrib/cache.py b/libs/werkzeug/contrib/cache.py old mode 100644 new mode 100755 index 613319f9..17bccf29 --- a/libs/werkzeug/contrib/cache.py +++ b/libs/werkzeug/contrib/cache.py @@ -65,9 +65,14 @@ except ImportError: from md5 import new as md5 from itertools import izip from time import time -from cPickle import loads, dumps, load, dump, HIGHEST_PROTOCOL from werkzeug.posixemulation import rename +try: + import cPickle as pickle +except ImportError: + import pickle + + def _items(mappingorseq): """Wrapper for efficient iteration over mappings represented by dicts or sequences:: @@ -82,6 +87,7 @@ def _items(mappingorseq): return mappingorseq.iteritems() if hasattr(mappingorseq, 'iteritems') \ else mappingorseq + class BaseCache(object): """Baseclass for the cache systems. All the cache systems implement this API or a superset of it. @@ -242,20 +248,22 @@ class SimpleCache(BaseCache): now = time() expires, value = self._cache.get(key, (0, None)) if expires > time(): - return loads(value) + return pickle.loads(value) def set(self, key, value, timeout=None): if timeout is None: timeout = self.default_timeout self._prune() - self._cache[key] = (time() + timeout, dumps(value, HIGHEST_PROTOCOL)) + self._cache[key] = (time() + timeout, pickle.dumps(value, + pickle.HIGHEST_PROTOCOL)) def add(self, key, value, timeout=None): if timeout is None: timeout = self.default_timeout if len(self._cache) > self._threshold: self._prune() - item = (time() + timeout, dumps(value, HIGHEST_PROTOCOL)) + item = (time() + timeout, pickle.dumps(value, + pickle.HIGHEST_PROTOCOL)) self._cache.setdefault(key, item) def delete(self, key): @@ -267,9 +275,10 @@ _test_memcached_key = re.compile(r'[^\x00-\x21\xff]{1,250}$').match class MemcachedCache(BaseCache): """A cache that uses memcached as backend. - The first argument can either be a list or tuple of server addresses - in which case Werkzeug tries to import the memcache module and connect - to it, or an object that resembles the API of a :class:`memcache.Client`. + The first argument can either be an object that resembles the API of a + :class:`memcache.Client` or a tuple/list of server addresses. In the + event that a tuple/list is passed, Werkzeug tries to import the best + available memcache library. Implementation notes: This cache backend works around some limitations in memcached to simplify the interface. For example unicode keys are encoded @@ -289,47 +298,19 @@ class MemcachedCache(BaseCache): different prefix. """ - def __init__(self, servers, default_timeout=300, key_prefix=None): + def __init__(self, servers=None, 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 cmemcached as memcache - is_cmemcached = True - except ImportError: - try: - import cmemcache as memcache - is_cmemcache = True - except ImportError: - try: - import memcache - is_cmemcache = False - is_pylibmc = False - except ImportError: - try: - import pylibmc as memcache - is_cmemcache = False - is_pylibmc = True - except ImportError: - raise RuntimeError('no memcache module found') - - 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: - client = memcache.Client(servers, False, HIGHEST_PROTOCOL) + if servers is None or isinstance(servers, (list, tuple)): + if servers is None: + servers = ['127.0.0.1:11211'] + self._client = self.import_preferred_memcache_lib(servers) + if self._client is None: + raise RuntimeError('no memcache module found') else: - client = servers + # NOTE: servers is actually an already initialized memcache + # client. + self._client = servers - self._client = client self.key_prefix = key_prefix def get(self, key): @@ -356,9 +337,6 @@ class MemcachedCache(BaseCache): encoded_key = self.key_prefix + encoded_key if _test_memcached_key(key): key_mapping[encoded_key] = key - # the keys call here is important because otherwise cmemcache - # does ugly things. What exactly I don't know, I think it does - # Py_DECREF but quite frankly I don't care. d = rv = self._client.get_multi(key_mapping.keys()) if have_encoded_keys or self.key_prefix: rv = {} @@ -440,23 +418,32 @@ class MemcachedCache(BaseCache): key = self.key_prefix + key self._client.decr(key, delta) + def import_preferred_memcache_lib(self, servers): + """Returns an initialized memcache client. Used by the constructor.""" + try: + import pylibmc + except ImportError: + pass + else: + return pylibmc.Client(servers) -class GAEMemcachedCache(MemcachedCache): - """Connects to the Google appengine memcached Cache. + try: + from google.appengine.api import memcache + except ImportError: + pass + else: + return memcache.Client() - :param default_timeout: the default timeout that is used if no timeout is - specified on :meth:`~BaseCache.set`. - :param key_prefix: a prefix that is added before all keys. This makes it - possible to use the same memcached server for different - applications. Keep in mind that - :meth:`~BaseCache.clear` will also clear keys with a - different prefix. - """ + try: + import memcache + except ImportError: + pass + else: + return memcache.Client(servers) - def __init__(self, default_timeout=300, key_prefix=None): - from google.appengine.api import memcache - MemcachedCache.__init__(self, memcache.Client(), - default_timeout, key_prefix) + +# backwards compatibility +GAEMemcachedCache = MemcachedCache class RedisCache(BaseCache): @@ -470,64 +457,109 @@ class RedisCache(BaseCache): .. versionadded:: 0.7 + .. versionadded:: 0.8 + `key_prefix` was added. + + .. versionchanged:: 0.8 + This cache backend now properly serializes objects. + :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`. - + :param key_prefix: A prefix that should be added to all keys. """ - def __init__(self, host='localhost', port=6379, default_timeout=300): + + def __init__(self, host='localhost', port=6379, password=None, + default_timeout=300, key_prefix=None): 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) + self._client = redis.Redis(host=host, port=port, password=password) else: self._client = host + self.key_prefix = key_prefix or '' + + def dump_object(self, value): + """Dumps an object into a string for redis. By default it serializes + integers as regular string and pickle dumps everything else. + """ + if isinstance(value, (int, long)): + return str(value) + return '!' + pickle.dumps(value) + + def load_object(self, value): + """The reversal of :meth:`dump_object`. This might be callde with + None. + """ + if value is None: + return None + if value.startswith('!'): + return pickle.loads(value[1:]) + try: + return int(value) + except ValueError: + # before 0.8 we did not have serialization. Still support that. + return value def get(self, key): - return self._client.get(key) + return self.load_object(self._client.get(self.key_prefix + key)) def get_many(self, *keys): - return self._client.mget(keys) + if self.key_prefix: + keys = [self.key_prefix + key for key in keys] + return [self.load_object(x) for x in 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) + dump = self.dump_object(value) + self._client.setex(self.key_prefix + key, dump, timeout) def add(self, key, value, timeout=None): if timeout is None: timeout = self.default_timeout - added = self._client.setnx(key, value) + dump = self.dump_object(value) + added = self._client.setnx(self.key_prefix + key, dump) if added: - self._client.expire(key, timeout) + self._client.expire(self.key_prefix + 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) + dump = self.dump_object(value) + pipe.setex(self.key_prefix + key, dump, timeout) pipe.execute() def delete(self, key): - self._client.delete(key) + self._client.delete(self.key_prefix + key) def delete_many(self, *keys): + if not keys: + return + if self.key_prefix: + keys = [self.key_prefix + key for key in keys] self._client.delete(*keys) def clear(self): - self._client.flushdb() + if self.key_prefix: + keys = self._client.keys(self.key_prefix + '*') + if keys: + self._client.delete(*keys) + else: + self._client.flushdb() def inc(self, key, delta=1): - return self._client.incr(key, delta) + return self._client.incr(self.key_prefix + key, delta) def dec(self, key, delta=1): - return self._client.decr(key, delta) + return self._client.decr(self.key_prefix + key, delta) class FileSystemCache(BaseCache): @@ -571,7 +603,7 @@ class FileSystemCache(BaseCache): try: try: f = open(fname, 'rb') - expires = load(f) + expires = pickle.load(f) remove = expires <= now or idx % 3 == 0 finally: if f is not None: @@ -600,8 +632,8 @@ class FileSystemCache(BaseCache): try: f = open(filename, 'rb') try: - if load(f) >= time(): - return load(f) + if pickle.load(f) >= time(): + return pickle.load(f) finally: f.close() os.remove(filename) @@ -623,8 +655,8 @@ class FileSystemCache(BaseCache): dir=self._path) f = os.fdopen(fd, 'wb') try: - dump(int(time() + timeout), f, 1) - dump(value, f, HIGHEST_PROTOCOL) + pickle.dump(int(time() + timeout), f, 1) + pickle.dump(value, f, pickle.HIGHEST_PROTOCOL) finally: f.close() rename(tmp, filename) @@ -637,4 +669,3 @@ class FileSystemCache(BaseCache): os.remove(self._get_filename(key)) except (IOError, OSError): pass - diff --git a/libs/werkzeug/contrib/fixers.py b/libs/werkzeug/contrib/fixers.py old mode 100644 new mode 100755 index 07302491..6286e6c0 --- a/libs/werkzeug/contrib/fixers.py +++ b/libs/werkzeug/contrib/fixers.py @@ -95,18 +95,33 @@ class ProxyFix(object): def __init__(self, app): self.app = app + def get_remote_addr(self, forwarded_for): + """Selects the new remote addr from the given list of ips in + X-Forwarded-For. By default the first one is picked. + + .. versionadded:: 0.8 + """ + if forwarded_for: + return forwarded_for[0] + def __call__(self, environ, start_response): getter = environ.get + forwarded_proto = getter('HTTP_X_FORWARDED_PROTO', '') forwarded_for = getter('HTTP_X_FORWARDED_FOR', '').split(',') forwarded_host = getter('HTTP_X_FORWARDED_HOST', '') environ.update({ - 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), - 'werkzeug.proxy_fix.orig_http_host': getter('HTTP_HOST') + 'werkzeug.proxy_fix.orig_wsgi_url_scheme': getter('wsgi.url_scheme'), + 'werkzeug.proxy_fix.orig_remote_addr': getter('REMOTE_ADDR'), + 'werkzeug.proxy_fix.orig_http_host': getter('HTTP_HOST') }) - if forwarded_for: - environ['REMOTE_ADDR'] = forwarded_for[0].strip() + forwarded_for = [x for x in [x.strip() for x in forwarded_for] if x] + remote_addr = self.get_remote_addr(forwarded_for) + if remote_addr is not None: + environ['REMOTE_ADDR'] = remote_addr if forwarded_host: environ['HTTP_HOST'] = forwarded_host + if forwarded_proto: + environ['wsgi.url_scheme'] = forwarded_proto return self.app(environ, start_response) diff --git a/libs/werkzeug/contrib/iterio.py b/libs/werkzeug/contrib/iterio.py old mode 100644 new mode 100755 index 4bdb5296..07186599 --- a/libs/werkzeug/contrib/iterio.py +++ b/libs/werkzeug/contrib/iterio.py @@ -127,25 +127,22 @@ class IterI(IterIO): if greenlet is None: raise RuntimeError('IterI requires greenlet support') stream = object.__new__(cls) - stream.__init__(greenlet.getcurrent()) + stream._parent = greenlet.getcurrent() + stream._buffer = [] + stream.closed = False + stream.pos = 0 def run(): func(stream) stream.flush() - g = greenlet(run, stream._parent) + g = greenlet.greenlet(run, stream._parent) while 1: rv = g.switch() if not rv: return yield rv[0] - def __init__(self, parent): - self._parent = parent - self._buffer = [] - self.closed = False - self.pos = 0 - def close(self): if not self.closed: self.closed = True @@ -171,13 +168,12 @@ class IterO(IterIO): """Iter output. Wrap an iterator and give it a stream like interface.""" def __new__(cls, gen): - return object.__new__(cls) - - def __init__(self, gen): + self = object.__new__(cls) self._gen = gen self._buf = '' self.closed = False self.pos = 0 + return self def __iter__(self): return self diff --git a/libs/werkzeug/contrib/jsrouting.py b/libs/werkzeug/contrib/jsrouting.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/contrib/kickstart.py b/libs/werkzeug/contrib/kickstart.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/contrib/limiter.py b/libs/werkzeug/contrib/limiter.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/contrib/lint.py b/libs/werkzeug/contrib/lint.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/contrib/profiler.py b/libs/werkzeug/contrib/profiler.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/contrib/securecookie.py b/libs/werkzeug/contrib/securecookie.py old mode 100644 new mode 100755 index 9e6feebd..733b0532 --- a/libs/werkzeug/contrib/securecookie.py +++ b/libs/werkzeug/contrib/securecookie.py @@ -88,7 +88,6 @@ r""" :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 time import time @@ -98,21 +97,7 @@ from werkzeug.contrib.sessions import ModificationTrackingDict from werkzeug.security import safe_str_cmp -# rather ugly way to import the correct hash method. Because -# hmac either accepts modules with a new method (sha, md5 etc.) -# or a hashlib factory function we have to figure out what to -# pass to it. If we have 2.5 or higher (so not 2.4 with a -# custom hashlib) we import from hashlib and fail if it does -# not exist (have seen that in old OS X versions). -# in all other cases the now deprecated sha module is used. -_default_hash = None -if sys.version_info >= (2, 5): - try: - from hashlib import sha1 as _default_hash - except ImportError: - pass -if _default_hash is None: - import sha as _default_hash +from hashlib import sha1 as _default_hash class UnquoteError(Exception): diff --git a/libs/werkzeug/contrib/sessions.py b/libs/werkzeug/contrib/sessions.py old mode 100644 new mode 100755 index b81351ac..7f6a5435 --- a/libs/werkzeug/contrib/sessions.py +++ b/libs/werkzeug/contrib/sessions.py @@ -58,10 +58,7 @@ import tempfile from os import path from time import time from random import random -try: - from hashlib import sha1 -except ImportError: - from sha import new as sha1 +from hashlib import sha1 from cPickle import dump, load, HIGHEST_PROTOCOL from werkzeug.datastructures import CallbackDict diff --git a/libs/werkzeug/contrib/testtools.py b/libs/werkzeug/contrib/testtools.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/contrib/wrappers.py b/libs/werkzeug/contrib/wrappers.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/datastructures.py b/libs/werkzeug/datastructures.py old mode 100644 new mode 100755 index aac2a6c8..3aaeb5f4 --- a/libs/werkzeug/datastructures.py +++ b/libs/werkzeug/datastructures.py @@ -50,6 +50,14 @@ class ImmutableListMixin(object): :private: """ + _hash_cache = None + + def __hash__(self): + if self._hash_cache is not None: + return self._hash_cache + rv = self._hash_cache = hash(tuple(self)) + return rv + def __reduce_ex__(self, protocol): return type(self), (list(self),) @@ -107,6 +115,8 @@ class ImmutableDictMixin(object): :private: """ + _hash_cache = None + @classmethod def fromkeys(cls, keys, value=None): instance = super(cls, cls).__new__(cls) @@ -116,6 +126,15 @@ class ImmutableDictMixin(object): def __reduce_ex__(self, protocol): return type(self), (dict(self),) + def _iter_hashitems(self): + return self.iteritems() + + def __hash__(self): + if self._hash_cache is not None: + return self._hash_cache + rv = self._hash_cache = hash(frozenset(self._iter_hashitems())) + return rv + def setdefault(self, key, default=None): is_immutable(self) @@ -149,6 +168,9 @@ class ImmutableMultiDictMixin(ImmutableDictMixin): def __reduce_ex__(self, protocol): return type(self), (self.items(multi=True),) + def _iter_hashitems(self): + return self.iteritems(multi=True) + def add(self, key, value): is_immutable(self) @@ -561,6 +583,9 @@ class MultiDict(TypeConversionDict): except KeyError, e: raise BadRequestKeyError(str(e)) + def __copy__(self): + return self.copy() + def __repr__(self): return '%s(%r)' % (self.__class__.__name__, self.items(multi=True)) @@ -1032,8 +1057,14 @@ class Headers(object): """ if kw: _value = _options_header_vkw(_value, kw) + self._validate_value(_value) self._list.append((_key, _value)) + def _validate_value(self, value): + if isinstance(value, basestring) and ('\n' in value or '\r' in value): + raise ValueError('Detected newline in header value. This is ' + 'a potential security problem') + def add_header(self, _key, _value, **_kw): """Add a new header tuple to the list. @@ -1063,18 +1094,21 @@ class Headers(object): """ if kw: _value = _options_header_vkw(_value, kw) + self._validate_value(_value) if not self._list: - return self.add(_key, _value) - lc_key = _key.lower() - for idx, (old_key, old_value) in enumerate(self._list): - if old_key.lower() == lc_key: + self._list.append((_key, _value)) + return + listiter = iter(self._list) + ikey = _key.lower() + for idx, (old_key, old_value) in enumerate(listiter): + if old_key.lower() == ikey: # replace first ocurrence self._list[idx] = (_key, _value) break else: - return self.add(_key, _value) - self._list[idx + 1:] = [(k, v) for k, v in self._list[idx + 1:] - if k.lower() != lc_key] + self._list.append((_key, _value)) + return + self._list[idx + 1:] = [t for t in listiter if t[0].lower() != ikey] def setdefault(self, key, value): """Returns the value for the key if it is in the dict, otherwise it @@ -1092,6 +1126,7 @@ class Headers(object): def __setitem__(self, key, value): """Like :meth:`set` but also supports index/slice based setting.""" if isinstance(key, (slice, int, long)): + self._validate_value(value) self._list[key] = value else: self.set(key, value) @@ -1102,14 +1137,8 @@ class Headers(object): :return: list """ - result = [] - for k, v in self: - if isinstance(v, unicode): - v = v.encode(charset) - else: - v = str(v) - result.append((k, v)) - return result + return [(k, isinstance(v, unicode) and v.encode(charset) or str(v)) + for k, v in self] def copy(self): return self.__class__(self._list) @@ -1133,7 +1162,9 @@ class Headers(object): class ImmutableHeadersMixin(object): - """Makes a :class:`Headers` immutable. + """Makes a :class:`Headers` immutable. We do not mark them as + hashable though since the only usecase for this datastructure + in Werkzeug is a view on a mutable structure. .. versionadded:: 0.5 @@ -1426,6 +1457,9 @@ class ImmutableOrderedMultiDict(ImmutableMultiDictMixin, OrderedMultiDict): .. versionadded:: 0.6 """ + def _iter_hashitems(self): + return enumerate(self.iteritems(multi=True)) + def copy(self): """Return a shallow mutable copy of this object. Keep in mind that the standard library's :func:`copy` function is a no-op for this class @@ -2045,7 +2079,7 @@ class ETags(object): return etag in self._strong def __nonzero__(self): - return bool(self.star_tag or self._strong) + return bool(self.star_tag or self._strong or self._weak) def __str__(self): return self.to_header() @@ -2422,7 +2456,7 @@ class FileStorage(object): """ def __init__(self, stream=None, filename=None, name=None, - content_type='application/octet-stream', content_length=-1, + content_type=None, content_length=None, headers=None): self.name = name self.stream = stream or _empty_stream @@ -2437,17 +2471,29 @@ class FileStorage(object): filename = None self.filename = filename - self.content_type = content_type - self.content_length = content_length if headers is None: headers = Headers() self.headers = headers + if content_type is not None: + headers['Content-Type'] = content_type + if content_length is not None: + headers['Content-Length'] = str(content_length) def _parse_content_type(self): if not hasattr(self, '_parsed_content_type'): self._parsed_content_type = \ parse_options_header(self.content_type) + @property + def content_type(self): + """The file's content type. Usually not available""" + return self.headers.get('content-type') + + @property + def content_length(self): + """The file's content length. Usually not available""" + return int(self.headers.get('content-length') or 0) + @property def mimetype(self): """Like :attr:`content_type` but without parameters (eg, without diff --git a/libs/werkzeug/debug/__init__.py b/libs/werkzeug/debug/__init__.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/console.py b/libs/werkzeug/debug/console.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/repr.py b/libs/werkzeug/debug/repr.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/shared/FONT_LICENSE b/libs/werkzeug/debug/shared/FONT_LICENSE old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/shared/console.png b/libs/werkzeug/debug/shared/console.png old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/shared/debugger.js b/libs/werkzeug/debug/shared/debugger.js old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/shared/jquery.js b/libs/werkzeug/debug/shared/jquery.js old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/shared/less.png b/libs/werkzeug/debug/shared/less.png old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/shared/more.png b/libs/werkzeug/debug/shared/more.png old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/shared/source.png b/libs/werkzeug/debug/shared/source.png old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/shared/style.css b/libs/werkzeug/debug/shared/style.css old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/shared/ubuntu.ttf b/libs/werkzeug/debug/shared/ubuntu.ttf old mode 100644 new mode 100755 diff --git a/libs/werkzeug/debug/tbtools.py b/libs/werkzeug/debug/tbtools.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/exceptions.py b/libs/werkzeug/exceptions.py old mode 100644 new mode 100755 index 42751035..b9b43c94 --- a/libs/werkzeug/exceptions.py +++ b/libs/werkzeug/exceptions.py @@ -179,6 +179,21 @@ class BadRequest(HTTPException): ) +class ClientDisconnected(BadRequest): + """Internal exception that is raised if Werkzeug detects a disconnected + client. Since the client is already gone at that point attempting to + send the error message to the client might not work and might ultimately + result in another exception in the server. Mainly this is here so that + it is silenced by default as far as Werkzeug is concerned. + + Since disconnections cannot be reliably detected and are unspecified + by WSGI to a large extend this might or might not be raised if a client + is gone. + + .. versionadded:: 0.8 + """ + + class Unauthorized(HTTPException): """*401* `Unauthorized` diff --git a/libs/werkzeug/formparser.py b/libs/werkzeug/formparser.py old mode 100644 new mode 100755 index c87fffdc..cd57620b --- a/libs/werkzeug/formparser.py +++ b/libs/werkzeug/formparser.py @@ -13,9 +13,10 @@ import re from cStringIO import StringIO from tempfile import TemporaryFile from itertools import chain, repeat +from functools import update_wrapper from werkzeug._internal import _decode_unicode, _empty_stream -from werkzeug.urls import url_decode +from werkzeug.urls import url_decode_stream from werkzeug.wsgi import LimitedStream, make_line_iter from werkzeug.exceptions import RequestEntityTooLarge from werkzeug.datastructures import Headers, FileStorage, MultiDict @@ -54,8 +55,7 @@ def parse_form_data(environ, stream_factory=None, charset='utf-8', mimetype is unknown the input stream is wrapped and returned as first argument, else the stream is empty. - This function does not raise exceptions, even if the input data is - malformed. + This is a shortcut for the common usage of :class:`FormDataParser`. Have a look at :ref:`dealing-with-request-data` for more details. @@ -86,56 +86,140 @@ def parse_form_data(environ, stream_factory=None, charset='utf-8', :param silent: If set to False parsing errors will not be caught. :return: A tuple in the form ``(stream, form, files)``. """ - content_type, extra = parse_options_header(environ.get('CONTENT_TYPE', '')) - try: - content_length = int(environ['CONTENT_LENGTH']) - except (KeyError, ValueError): - content_length = 0 + return FormDataParser(stream_factory, charset, errors, + max_form_memory_size, max_content_length, + cls, silent).parse_from_environ(environ) - if cls is None: - cls = MultiDict - if max_content_length is not None and content_length > max_content_length: - raise RequestEntityTooLarge() - - stream = _empty_stream - files = () - - if content_type == 'multipart/form-data': +def exhaust_stream(f): + """Helper decorator for methods that exhausts the stream on return.""" + def wrapper(self, stream, *args, **kwargs): try: - form, files = parse_multipart(environ['wsgi.input'], - extra.get('boundary'), - content_length, stream_factory, - charset, errors, - max_form_memory_size=max_form_memory_size) - except ValueError, e: - if not silent: - raise - form = cls() - else: - form = cls(form) - elif content_type == 'application/x-www-form-urlencoded' or \ - content_type == 'application/x-url-encoded': - if max_form_memory_size is not None and \ - content_length > max_form_memory_size: - raise RequestEntityTooLarge() - form = url_decode(environ['wsgi.input'].read(content_length), - charset, errors=errors, cls=cls) - else: - form = cls() - stream = LimitedStream(environ['wsgi.input'], content_length) - - return stream, form, cls(files) + return f(self, stream, *args, **kwargs) + finally: + stream.exhaust() + return update_wrapper(wrapper, f) -def _fix_ie_filename(filename): - """Internet Explorer 6 transmits the full file name if a file is - uploaded. This function strips the full path if it thinks the - filename is Windows-like absolute. +class FormDataParser(object): + """This class implements parsing of form data for Werkzeug. By itself + it can parse multipart and url encoded form data. It can be subclasses + and extended but for most mimetypes it is a better idea to use the + untouched stream and expose it as separate attributes on a request + object. + + .. versionadded:: 0.8 + + :param stream_factory: An optional callable that returns a new read and + writeable file descriptor. This callable works + the same as :meth:`~BaseResponse._get_file_stream`. + :param charset: The character set for URL and url encoded form data. + :param errors: The encoding error behavior. + :param max_form_memory_size: the maximum number of bytes to be accepted for + in-memory stored form data. If the data + exceeds the value specified an + :exc:`~exceptions.RequestURITooLarge` + exception is raised. + :param max_content_length: If this is provided and the transmitted data + is longer than this value an + :exc:`~exceptions.RequestEntityTooLarge` + exception is raised. + :param cls: an optional dict class to use. If this is not specified + or `None` the default :class:`MultiDict` is used. + :param silent: If set to False parsing errors will not be caught. """ - if filename[1:3] == ':\\' or filename[:2] == '\\\\': - return filename.split('\\')[-1] - return filename + + def __init__(self, stream_factory=None, charset='utf-8', + errors='replace', max_form_memory_size=None, + max_content_length=None, cls=None, + silent=True): + if stream_factory is None: + stream_factory = default_stream_factory + self.stream_factory = stream_factory + self.charset = charset + self.errors = errors + self.max_form_memory_size = max_form_memory_size + self.max_content_length = max_content_length + if cls is None: + cls = MultiDict + self.cls = cls + self.silent = silent + + def get_parse_func(self, mimetype, options): + return self.parse_functions.get(mimetype) + + def parse_from_environ(self, environ): + """Parses the information from the environment as form data. + + :param environ: the WSGI environment to be used for parsing. + :return: A tuple in the form ``(stream, form, files)``. + """ + content_type = environ.get('CONTENT_TYPE', '') + mimetype, options = parse_options_header(content_type) + try: + content_length = int(environ['CONTENT_LENGTH']) + except (KeyError, ValueError): + content_length = 0 + stream = environ['wsgi.input'] + return self.parse(stream, mimetype, content_length, options) + + def parse(self, stream, mimetype, content_length, options=None): + """Parses the information from the given stream, mimetype, + content length and mimetype parameters. + + :param stream: an input stream + :param mimetype: the mimetype of the data + :param content_length: the content length of the incoming data + :param options: optional mimetype parameters (used for + the multipart boundary for instance) + :return: A tuple in the form ``(stream, form, files)``. + """ + if self.max_content_length is not None and \ + content_length > self.max_content_length: + raise RequestEntityTooLarge() + if options is None: + options = {} + input_stream = LimitedStream(stream, content_length) + + parse_func = self.get_parse_func(mimetype, options) + if parse_func is not None: + try: + return parse_func(self, input_stream, mimetype, + content_length, options) + except ValueError: + if not self.silent: + raise + return input_stream, self.cls(), self.cls() + + @exhaust_stream + def _parse_multipart(self, stream, mimetype, content_length, options): + parser = MultiPartParser(self.stream_factory, self.charset, self.errors, + max_form_memory_size=self.max_form_memory_size, + cls=self.cls) + form, files = parser.parse(stream, options.get('boundary'), + content_length) + return _empty_stream, form, files + + @exhaust_stream + def _parse_urlencoded(self, stream, mimetype, content_length, options): + if self.max_form_memory_size is not None and \ + content_length > self.max_form_memory_size: + raise RequestEntityTooLarge() + form = url_decode_stream(stream, self.charset, + errors=self.errors, cls=self.cls) + return _empty_stream, form, self.cls() + + #: mapping of mimetypes to parsing functions + parse_functions = { + 'multipart/form-data': _parse_multipart, + 'application/x-www-form-urlencoded': _parse_urlencoded, + 'application/x-url-encoded': _parse_urlencoded + } + + +def is_valid_multipart_boundary(boundary): + """Checks if the string given is a valid multipart boundary.""" + return _multipart_boundary_re.match(boundary) is not None def _line_parse(line): @@ -149,97 +233,152 @@ def _line_parse(line): return line, False -def _find_terminator(iterator): - """The terminator might have some additional newlines before it. - There is at least one application that sends additional newlines - before headers (the python setuptools package). +def parse_multipart_headers(iterable): + """Parses multipart headers from an iterable that yields lines (including + the trailing newline symbol. The iterable has to be newline terminated: + + >>> parse_multipart_headers(['Foo: Bar\r\n', 'Test: Blub\r\n', + ... '\r\n', 'More data']) + Headers([('Foo', 'Bar'), ('Test', 'Blub')]) + + :param iterable: iterable of strings that are newline terminated """ - for line in iterator: + result = [] + for line in iterable: + line, line_terminated = _line_parse(line) + if not line_terminated: + raise ValueError('unexpected end of line in multipart header') if not line: break - line = line.strip() - if line: - return line - return '' + elif line[0] in ' \t' and result: + key, value = result[-1] + result[-1] = (key, value + '\n ' + line[1:]) + else: + parts = line.split(':', 1) + if len(parts) == 2: + result.append((parts[0].strip(), parts[1].strip())) + + # we link the list to the headers, no need to create a copy, the + # list was not shared anyways. + return Headers.linked(result) -def is_valid_multipart_boundary(boundary): - """Checks if the string given is a valid multipart boundary.""" - return _multipart_boundary_re.match(boundary) is not None +class MultiPartParser(object): + def __init__(self, stream_factory=None, charset='utf-8', errors='replace', + max_form_memory_size=None, cls=None, buffer_size=10 * 1024): + self.stream_factory = stream_factory + self.charset = charset + self.errors = errors + self.max_form_memory_size = max_form_memory_size + if stream_factory is None: + stream_factory = default_stream_factory + if cls is None: + cls = MultiDict + self.cls = cls -def parse_multipart(file, boundary, content_length, stream_factory=None, - 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 - exists for internal usage only, but could be exposed as separate - function if it turns out to be useful and if we consider the API stable. - """ - # XXX: this function does not support multipart/mixed. I don't know of - # any browser that supports this, but it should be implemented - # nonetheless. + # make sure the buffer size is divisible by four so that we can base64 + # decode chunk by chunk + assert buffer_size % 4 == 0, 'buffer size has to be divisible by 4' + # also the buffer size has to be at least 1024 bytes long or long headers + # will freak out the system + assert buffer_size >= 1024, 'buffer size has to be at least 1KB' - # make sure the buffer size is divisible by four so that we can base64 - # decode chunk by chunk - assert buffer_size % 4 == 0, 'buffer size has to be divisible by 4' - # also the buffer size has to be at least 1024 bytes long or long headers - # will freak out the system - assert buffer_size >= 1024, 'buffer size has to be at least 1KB' + self.buffer_size = buffer_size - if stream_factory is None: - stream_factory = default_stream_factory + def _fix_ie_filename(self, filename): + """Internet Explorer 6 transmits the full file name if a file is + uploaded. This function strips the full path if it thinks the + filename is Windows-like absolute. + """ + if filename[1:3] == ':\\' or filename[:2] == '\\\\': + return filename.split('\\')[-1] + return filename - if not boundary: - raise ValueError('Missing boundary') - if not is_valid_multipart_boundary(boundary): - raise ValueError('Invalid boundary: %s' % boundary) - if len(boundary) > buffer_size: # pragma: no cover - # this should never happen because we check for a minimum size - # of 1024 and boundaries may not be longer than 200. The only - # situation when this happen is for non debug builds where - # the assert i skipped. - raise ValueError('Boundary longer than buffer size') + def _find_terminator(self, iterator): + """The terminator might have some additional newlines before it. + There is at least one application that sends additional newlines + before headers (the python setuptools package). + """ + for line in iterator: + if not line: + break + line = line.strip() + if line: + return line + return '' - total_content_length = content_length - next_part = '--' + boundary - last_part = next_part + '--' + def fail(self, message): + raise ValueError(message) - form = [] - files = [] - in_memory = 0 + def get_part_encoding(self, headers): + transfer_encoding = headers.get('content-transfer-encoding') + if transfer_encoding is not None and \ + transfer_encoding in _supported_multipart_encodings: + return transfer_encoding - # convert the file into a limited stream with iteration capabilities - file = LimitedStream(file, content_length) - iterator = chain(make_line_iter(file, buffer_size=buffer_size), - _empty_string_iter) + def get_part_charset(self, headers): + # Figure out input charset for current part + content_type = headers.get('content-type') + if content_type: + mimetype, ct_params = parse_options_header(content_type) + return ct_params.get('charset', self.charset) + return self.charset - try: - terminator = _find_terminator(iterator) + def start_file_streaming(self, filename, headers, total_content_length): + filename = _decode_unicode(filename, self.charset, self.errors) + filename = self._fix_ie_filename(filename) + content_type = headers.get('content_type') + try: + content_length = int(headers['content-length']) + except (KeyError, ValueError): + content_length = 0 + container = self.stream_factory(total_content_length, content_type, + filename, content_length) + return filename, container + + def in_memory_threshold_reached(self, bytes): + raise RequestEntityTooLarge() + + def validate_boundary(self, boundary): + if not boundary: + self.fail('Missing boundary') + if not is_valid_multipart_boundary(boundary): + self.fail('Invalid boundary: %s' % boundary) + if len(boundary) > self.buffer_size: # pragma: no cover + # this should never happen because we check for a minimum size + # of 1024 and boundaries may not be longer than 200. The only + # situation when this happen is for non debug builds where + # the assert i skipped. + self.fail('Boundary longer than buffer size') + + def parse(self, file, boundary, content_length): + next_part = '--' + boundary + last_part = next_part + '--' + + form = [] + files = [] + in_memory = 0 + + iterator = chain(make_line_iter(file, limit=content_length, + buffer_size=self.buffer_size), + _empty_string_iter) + + terminator = self._find_terminator(iterator) if terminator != next_part: - raise ValueError('Expected boundary at start of multipart data') + self.fail('Expected boundary at start of multipart data') while terminator != last_part: headers = parse_multipart_headers(iterator) + disposition = headers.get('content-disposition') if disposition is None: - raise ValueError('Missing Content-Disposition header') + self.fail('Missing Content-Disposition header') disposition, extra = parse_options_header(disposition) + transfer_encoding = self.get_part_encoding(headers) name = extra.get('name') - - transfer_encoding = headers.get('content-transfer-encoding') - try_decode = transfer_encoding is not None and \ - transfer_encoding in _supported_multipart_encodings - filename = extra.get('filename') - - # Figure out input charset for current part - content_type = headers.get('content-type') - if content_type: - mimetype, ct_params = parse_options_header(content_type) - part_charset = ct_params.get("charset", charset) - else: - part_charset = charset + part_charset = self.get_part_charset(headers) # if no content type is given we stream into memory. A list is # used as a temporary container. @@ -247,41 +386,32 @@ def parse_multipart(file, boundary, content_length, stream_factory=None, is_file = False container = [] _write = container.append - guard_memory = max_form_memory_size is not None + guard_memory = self.max_form_memory_size is not None # otherwise we parse the rest of the headers and ask the stream # factory for something we can write in. else: is_file = True guard_memory = False - if filename is not None: - filename = _fix_ie_filename(_decode_unicode(filename, - charset, - errors)) - try: - content_length = int(headers['content-length']) - except (KeyError, ValueError): - content_length = 0 - container = stream_factory(total_content_length, content_type, - filename, content_length) + filename, container = self.start_file_streaming( + filename, headers, content_length) _write = container.write buf = '' for line in iterator: if not line: - raise ValueError('unexpected end of stream') + self.fail('unexpected end of stream') if line[:2] == '--': terminator = line.rstrip() if terminator in (next_part, last_part): break - if try_decode: + if transfer_encoding is not None: try: line = line.decode(transfer_encoding) except Exception: - raise ValueError('could not decode transfer ' - 'encoded chunk') + self.fail('could not decode transfer encoded chunk') # we have something in the buffer from the last iteration. # this is usually a newline delimiter. @@ -311,9 +441,8 @@ def parse_multipart(file, boundary, content_length, stream_factory=None, # there is too much data in memory. if guard_memory: in_memory += len(line) - if in_memory > max_form_memory_size: - from werkzeug.exceptions import RequestEntityTooLarge - raise RequestEntityTooLarge() + if in_memory > self.max_form_memory_size: + self.in_memory_threshold_reached(in_memory) else: # pragma: no cover raise ValueError('unexpected end of part') @@ -326,37 +455,9 @@ def parse_multipart(file, boundary, content_length, stream_factory=None, if is_file: container.seek(0) files.append((name, FileStorage(container, filename, name, - content_type, - content_length, headers))) + headers=headers))) else: form.append((name, _decode_unicode(''.join(container), - part_charset, errors))) - finally: - # make sure the whole input stream is read - file.exhaust() + part_charset, self.errors))) - return form, files - - -def parse_multipart_headers(iterable): - """Parses multipart headers from an iterable that yields lines (including - the trailing newline symbol. - """ - result = [] - for line in iterable: - line, line_terminated = _line_parse(line) - if not line_terminated: - raise ValueError('unexpected end of line in multipart header') - if not line: - break - elif line[0] in ' \t' and result: - key, value = result[-1] - result[-1] = (key, value + '\n ' + line[1:]) - else: - parts = line.split(':', 1) - if len(parts) == 2: - result.append((parts[0].strip(), parts[1].strip())) - - # we link the list to the headers, no need to create a copy, the - # list was not shared anyways. - return Headers.linked(result) + return self.cls(form), self.cls(files) diff --git a/libs/werkzeug/http.py b/libs/werkzeug/http.py old mode 100644 new mode 100755 index 32e03075..23e13286 --- a/libs/werkzeug/http.py +++ b/libs/werkzeug/http.py @@ -50,7 +50,7 @@ _entity_headers = frozenset([ 'content-location', 'content-md5', 'content-range', 'content-type', 'expires', 'last-modified' ]) -_hop_by_pop_headers = frozenset([ +_hop_by_hop_headers = frozenset([ 'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade' @@ -357,10 +357,12 @@ def parse_authorization_header(value): 'password': password}) elif auth_type == 'digest': auth_map = parse_dict_header(auth_info) - for key in 'username', 'realm', 'nonce', 'uri', 'nc', 'cnonce', \ - 'response': + for key in 'username', 'realm', 'nonce', 'uri', 'response': if not key in auth_map: return + if 'qop' in auth_map: + if not auth_map.get('nc') or not auth_map.get('cnonce'): + return return Authorization('digest', auth_map) @@ -708,7 +710,7 @@ def is_hop_by_hop_header(header): :param header: the header to test. :return: `True` if it's an entity header, `False` otherwise. """ - return header.lower() in _hop_by_pop_headers + return header.lower() in _hop_by_hop_headers def parse_cookie(header, charset='utf-8', errors='replace', diff --git a/libs/werkzeug/local.py b/libs/werkzeug/local.py old mode 100644 new mode 100755 index a5046f70..c5507bbd --- a/libs/werkzeug/local.py +++ b/libs/werkzeug/local.py @@ -8,26 +8,19 @@ :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 - get_current_greenlet = int -try: - from thread import get_ident as get_current_thread, allocate_lock -except ImportError: # pragma: no cover - from dummy_thread import get_ident as get_current_thread, allocate_lock - from werkzeug.wsgi import ClosingIterator from werkzeug._internal import _patch_wrapper - -# get the best ident function. if greenlets are not installed we can -# safely just use the builtin thread function and save a python methodcall -# and the cost of calculating a hash. -if get_current_greenlet is int: # pragma: no cover - get_ident = get_current_thread -else: - get_ident = lambda: (get_current_thread(), get_current_greenlet()) +# since each thread has its own greenlet we can just use those as identifiers +# for the context. If greenlets are not available we fall back to the +# current thread ident. +try: + from greenlet import getcurrent as get_ident +except ImportError: # pragma: no cover + try: + from thread import get_ident + except ImportError: # pragma: no cover + from dummy_thread import get_ident def release_local(local): @@ -54,15 +47,14 @@ def release_local(local): class Local(object): - __slots__ = ('__storage__', '__lock__', '__ident_func__') + __slots__ = ('__storage__', '__ident_func__') def __init__(self): object.__setattr__(self, '__storage__', {}) - object.__setattr__(self, '__lock__', allocate_lock()) object.__setattr__(self, '__ident_func__', get_ident) def __iter__(self): - return self.__storage__.iteritems() + return iter(self.__storage__.items()) def __call__(self, proxy): """Create a proxy for a name.""" @@ -79,15 +71,11 @@ class Local(object): def __setattr__(self, name, value): ident = self.__ident_func__() - self.__lock__.acquire() + storage = self.__storage__ try: - storage = self.__storage__ - if ident in storage: - storage[ident][name] = value - else: - storage[ident] = {name: value} - finally: - self.__lock__.release() + storage[ident][name] = value + except KeyError: + storage[ident] = {name: value} def __delattr__(self, name): try: @@ -125,7 +113,6 @@ class LocalStack(object): def __init__(self): self._local = Local() - self._lock = allocate_lock() def __release_local__(self): self._local.__release_local__() @@ -147,32 +134,24 @@ class LocalStack(object): def push(self, obj): """Pushes a new item to the stack""" - self._lock.acquire() - try: - rv = getattr(self._local, 'stack', None) - if rv is None: - self._local.stack = rv = [] - rv.append(obj) - return rv - finally: - self._lock.release() + rv = getattr(self._local, 'stack', None) + if rv is None: + self._local.stack = rv = [] + rv.append(obj) + return rv def pop(self): """Removes the topmost item from the stack, will return the old value or `None` if the stack was already empty. """ - self._lock.acquire() - try: - stack = getattr(self._local, 'stack', None) - if stack is None: - return None - elif len(stack) == 1: - release_local(self._local) - return stack[-1] - else: - return stack.pop() - finally: - self._lock.release() + stack = getattr(self._local, 'stack', None) + if stack is None: + return None + elif len(stack) == 1: + release_local(self._local) + return stack[-1] + else: + return stack.pop() @property def top(self): diff --git a/libs/werkzeug/posixemulation.py b/libs/werkzeug/posixemulation.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/routing.py b/libs/werkzeug/routing.py old mode 100644 new mode 100755 index e847e200..381ce2be --- a/libs/werkzeug/routing.py +++ b/libs/werkzeug/routing.py @@ -100,7 +100,7 @@ import posixpath from pprint import pformat from urlparse import urljoin -from werkzeug.urls import url_encode, url_decode, url_quote +from werkzeug.urls import url_encode, url_quote from werkzeug.utils import redirect, format_string from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed from werkzeug._internal import _get_environ @@ -115,7 +115,7 @@ _rule_re = re.compile(r''' (?:\((?P.*?)\))? # converter arguments \: # variable delimiter )? - (?P[a-zA-Z][a-zA-Z0-9_]*) # variable name + (?P[a-zA-Z_][a-zA-Z0-9_]*) # variable name > ''', re.VERBOSE) _simple_rule_re = re.compile(r'<([^>]+)>') @@ -240,7 +240,7 @@ class RequestRedirect(HTTPException, RoutingException): self.new_url = new_url def get_response(self, environ): - return redirect(self.new_url, 301) + return redirect(self.new_url, self.code) class RequestSlash(RoutingException): @@ -715,7 +715,7 @@ class Rule(RuleFactory): return processed.add(data) else: - add(data) + add(url_quote(data, self.map.charset, safe='/:|+')) domain_part, url = (u''.join(tmp)).split('|', 1) if append_unknown: @@ -1112,6 +1112,7 @@ class Map(object): .. versionadded:: 0.8 `query_args` can now also be a string. """ + server_name = server_name.lower() if self.host_matching: if subdomain is not None: raise RuntimeError('host matching enabled and a ' @@ -1120,6 +1121,8 @@ class Map(object): subdomain = self.default_subdomain if script_name is None: script_name = '/' + if isinstance(server_name, unicode): + server_name = server_name.encode('idna') return MapAdapter(self, server_name, script_name, subdomain, url_scheme, path_info, default_method, query_args) @@ -1149,6 +1152,10 @@ class Map(object): parameter that did not have any effect. It was removed because of that. + .. versionchanged:: 0.8 + This will no longer raise a ValueError when an unexpected server + name was passed. + :param environ: a WSGI environment. :param server_name: an optional server name hint (see above). :param subdomain: optionally the current subdomain (see above). @@ -1163,6 +1170,7 @@ class Map(object): in (('https', '443'), ('http', '80')): server_name += ':' + environ['SERVER_PORT'] elif subdomain is None and not self.host_matching: + server_name = server_name.lower() if 'HTTP_HOST' in environ: wsgi_server_name = environ.get('HTTP_HOST') else: @@ -1170,15 +1178,19 @@ class Map(object): if (environ['wsgi.url_scheme'], environ['SERVER_PORT']) not \ in (('https', '443'), ('http', '80')): wsgi_server_name += ':' + environ['SERVER_PORT'] + wsgi_server_name = wsgi_server_name.lower() cur_server_name = wsgi_server_name.split('.') real_server_name = server_name.split('.') offset = -len(real_server_name) if cur_server_name[offset:] != real_server_name: - raise ValueError('the server name provided (%r) does not ' - 'match the server name from the WSGI ' - 'environment (%r)' % - (server_name, wsgi_server_name)) - subdomain = '.'.join(filter(None, cur_server_name[:offset])) + # This can happen even with valid configs if the server was + # accesssed directly by IP address under some situations. + # Instead of raising an exception like in Werkzeug 0.7 or + # earlier we go by an invalid subdomain which will result + # in a 404 error on matching. + subdomain = '' + else: + 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'), @@ -1491,7 +1503,8 @@ class MapAdapter(object): self.url_scheme, self.get_host(domain_part), posixpath.join(self.script_name[:-1].lstrip('/'), - url_quote(path_info.lstrip('/'), self.map.charset)), + url_quote(path_info.lstrip('/'), self.map.charset, + safe='/:|+')), suffix )) diff --git a/libs/werkzeug/script.py b/libs/werkzeug/script.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/security.py b/libs/werkzeug/security.py old mode 100644 new mode 100755 index ba78b4df..45527532 --- a/libs/werkzeug/security.py +++ b/libs/werkzeug/security.py @@ -10,27 +10,12 @@ """ 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 -# new hashlib module, we have to support both. This sets up a -# mapping to the digest factory functions and the digest modules -# (or factory functions with changed API) -try: - from hashlib import sha1, md5 - _hash_funcs = _hash_mods = {'sha1': sha1, 'md5': md5} - _sha1_mod = sha1 - _md5_mod = md5 -except ImportError: - import sha as _sha1_mod, md5 as _md5_mod - _hash_mods = {'sha1': _sha1_mod, 'md5': _md5_mod} - _hash_funcs = {'sha1': _sha1_mod.new, 'md5': _md5_mod.new} - -SALT_CHARS = string.letters + string.digits +SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' _sys_rng = SystemRandom() @@ -38,6 +23,20 @@ _os_alt_seps = list(sep for sep in [os.path.sep, os.path.altsep] if sep not in (None, '/')) +def _find_hashlib_algorithms(): + import hashlib + algos = getattr(hashlib, 'algorithms', None) + if algos is None: + algos = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512') + rv = {} + for algo in algos: + func = getattr(hashlib, algo, None) + if func is not None: + rv[algo] = func + return rv +_hash_funcs = _find_hashlib_algorithms() + + 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. @@ -69,11 +68,11 @@ def _hash_internal(method, salt, password): if method == 'plain': return password if salt: - if method not in _hash_mods: + if method not in _hash_funcs: return None if isinstance(salt, unicode): salt = salt.encode('utf-8') - h = hmac.new(salt, None, _hash_mods[method]) + h = hmac.new(salt, None, _hash_funcs[method]) else: if method not in _hash_funcs: return None @@ -98,7 +97,7 @@ def generate_password_hash(password, method='sha1', salt_length=8): is used, hmac is used internally to salt the password. :param password: the password to hash - :param method: the hash method to use (``'md5'`` or ``'sha1'``) + :param method: the hash method to use (one that hashlib supports) :param salt_length: the lengt of the salt in letters """ salt = method != 'plain' and gen_salt(salt_length) or '' diff --git a/libs/werkzeug/serving.py b/libs/werkzeug/serving.py old mode 100644 new mode 100755 index fd7286c6..fcca3b52 --- a/libs/werkzeug/serving.py +++ b/libs/werkzeug/serving.py @@ -35,6 +35,8 @@ :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ +from __future__ import with_statement + import os import socket import sys @@ -177,6 +179,7 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object): def handle(self): """Handles a request ignoring dropped connections.""" + rv = None try: rv = BaseHTTPRequestHandler.handle(self) except (socket.error, socket.timeout), e: @@ -192,9 +195,11 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object): """A horrible, horrible way to kill the server for Python 2.6 and later. It's the best we can do. """ + # Windows does not provide SIGKILL, go with SIGTERM then. + sig = getattr(signal, 'SIGKILL', signal.SIGTERM) # reloader active if os.environ.get('WERKZEUG_RUN_MAIN') == 'true': - os.kill(os.getpid(), signal.SIGKILL) + os.kill(os.getpid(), sig) # python 2.7 self.server._BaseServer__shutdown_request = True # python 2.6 @@ -247,10 +252,13 @@ class WSGIRequestHandler(BaseHTTPRequestHandler, object): BaseRequestHandler = WSGIRequestHandler -def generate_adhoc_ssl_context(): - """Generates an adhoc SSL context for the development server.""" +def generate_adhoc_ssl_pair(cn=None): from random import random - from OpenSSL import crypto, SSL + from OpenSSL import crypto + + # pretty damn sure that this is not actually accepted by anyone + if cn is None: + cn = '*' cert = crypto.X509() cert.set_serial_number(int(random() * sys.maxint)) @@ -258,7 +266,7 @@ def generate_adhoc_ssl_context(): cert.gmtime_adj_notAfter(60 * 60 * 24 * 365) subject = cert.get_subject() - subject.CN = '*' + subject.CN = cn subject.O = 'Dummy Certificate' issuer = cert.get_issuer() @@ -270,10 +278,59 @@ def generate_adhoc_ssl_context(): cert.set_pubkey(pkey) cert.sign(pkey, 'md5') + return cert, pkey + + +def make_ssl_devcert(base_path, host=None, cn=None): + """Creates an SSL key for development. This should be used instead of + the ``'adhoc'`` key which generates a new cert on each server start. + It accepts a path for where it should store the key and cert and + either a host or CN. If a host is given it will use the CN + ``*.host/CN=host``. + + For more information see :func:`run_simple`. + + .. versionadded:: 0.9 + + :param base_path: the path to the certificate and key. The extension + ``.crt`` is added for the certificate, ``.key`` is + added for the key. + :param host: the name of the host. This can be used as an alternative + for the `cn`. + :param cn: the `CN` to use. + """ + from OpenSSL import crypto + if host is not None: + cn = '*.%s/CN=%s' % (host, host) + cert, pkey = generate_adhoc_ssl_pair(cn=cn) + + cert_file = base_path + '.crt' + pkey_file = base_path + '.key' + + with open(cert_file, 'w') as f: + f.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + with open(pkey_file, 'w') as f: + f.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)) + + return cert_file, pkey_file + + +def generate_adhoc_ssl_context(): + """Generates an adhoc SSL context for the development server.""" + from OpenSSL import SSL + pkey, cert = generate_adhoc_ssl_pair() ctx = SSL.Context(SSL.SSLv23_METHOD) ctx.use_privatekey(pkey) ctx.use_certificate(cert) + return ctx + +def load_ssl_context(cert_file, pkey_file): + """Loads an SSL context from a certificate and private key file.""" + from OpenSSL import SSL + ctx = SSL.Context(SSL.SSLv23_METHOD) + ctx.use_certificate_file(cert_file) + ctx.use_privatekey_file(pkey_file) return ctx @@ -297,6 +354,9 @@ class _SSLConnectionFix(object): def __getattr__(self, attrib): return getattr(self._con, attrib) + def shutdown(self, arg=None): + self._con.shutdown() + def select_ip_version(host, port): """Returns AF_INET4 or AF_INET6 depending on where to connect to.""" @@ -339,6 +399,8 @@ class BaseWSGIServer(HTTPServer, object): except ImportError: raise TypeError('SSL is not available if the OpenSSL ' 'library is not installed.') + if isinstance(ssl_context, tuple): + ssl_context = load_ssl_context(*ssl_context) if ssl_context == 'adhoc': ssl_context = generate_adhoc_ssl_context() self.socket = tsafe.Connection(ssl_context, self.socket) @@ -405,7 +467,23 @@ def make_server(host, port, app=None, threaded=False, processes=1, passthrough_errors, ssl_context) -def reloader_loop(extra_files=None, interval=1): +def _iter_module_files(): + for module in sys.modules.values(): + filename = getattr(module, '__file__', None) + if filename: + old = None + while not os.path.isfile(filename): + old = filename + filename = os.path.dirname(filename) + if filename == old: + break + else: + if filename[-4:] in ('.pyc', '.pyo'): + filename = filename[:-1] + yield filename + + +def _reloader_stat_loop(extra_files=None, interval=1): """When this function is run from the main thread, it will force other threads to exit when any modules currently loaded change. @@ -414,32 +492,10 @@ def reloader_loop(extra_files=None, interval=1): :param extra_files: a list of additional files it should watch. """ - def iter_module_files(): - for module in sys.modules.values(): - filename = getattr(module, '__file__', None) - if filename: - old = None - while not os.path.isfile(filename): - old = filename - filename = os.path.dirname(filename) - if filename == old: - break - else: - if filename[-4:] in ('.pyc', '.pyo'): - filename = filename[:-1] - yield filename - - fnames = [] - fnames.extend(iter_module_files()) - fnames.extend(extra_files or ()) - - reloader(fnames, interval=interval) - - -def _reloader_stat_loop(fnames, interval=1): + from itertools import chain mtimes = {} while 1: - for filename in fnames: + for filename in chain(_iter_module_files(), extra_files or ()): try: mtime = os.stat(filename).st_mtime except OSError: @@ -455,7 +511,7 @@ def _reloader_stat_loop(fnames, interval=1): time.sleep(interval) -def _reloader_inotify(fnames, interval=None): +def _reloader_inotify(extra_files=None, interval=None): # Mutated by inotify loop when changes occur. changed = [False] @@ -478,13 +534,16 @@ def _reloader_inotify(fnames, interval=None): _log('info', ' * Detected change in %r, reloading' % event.path) changed[:] = [True] - for fname in fnames: + for fname in extra_files or (): wm.add_watch(fname, mask, signal_changed) # ... And now we wait... notif = Notifier(wm) try: while not changed[0]: + # always reiterate through sys.modules, adding them + for fname in _iter_module_files(): + wm.add_watch(fname, mask, signal_changed) notif.process_events() if notif.check_events(timeout=interval): notif.read_events() @@ -497,7 +556,7 @@ def _reloader_inotify(fnames, interval=None): # 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 +reloader_loop = _reloader_stat_loop def restart_with_reloader(): @@ -555,6 +614,10 @@ def run_simple(hostname, port, application, use_reloader=False, .. versionadded:: 0.6 support for SSL was added. + .. versionadded:: 0.8 + Added support for automatically loading a SSL context from certificate + file and private key. + :param hostname: The host for the application. eg: ``'localhost'`` :param port: The port for the server. eg: ``8080`` :param application: the WSGI application to execute @@ -582,7 +645,8 @@ def run_simple(hostname, port, application, use_reloader=False, This means that the server will die on errors but it can be useful to hook debuggers in (pdb etc.) :param ssl_context: an SSL context for the connection. Either an OpenSSL - context, the string ``'adhoc'`` if the server should + context, a tuple in the form ``(cert_file, pkey_file)``, + the string ``'adhoc'`` if the server should automatically create one, or `None` to disable SSL (which is the default). """ @@ -607,7 +671,8 @@ def run_simple(hostname, port, application, use_reloader=False, if use_reloader: # Create and destroy a socket so that any exceptions are raised before # we spawn a separate Python interpreter and lose this ability. - test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + address_family = select_ip_version(hostname, port) + test_socket = socket.socket(address_family, socket.SOCK_STREAM) test_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) test_socket.bind((hostname, port)) test_socket.close() diff --git a/libs/werkzeug/templates.py b/libs/werkzeug/templates.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/test.py b/libs/werkzeug/test.py old mode 100644 new mode 100755 index 62153d41..314b7e56 --- a/libs/werkzeug/test.py +++ b/libs/werkzeug/test.py @@ -474,7 +474,10 @@ class EnvironBuilder(object): return 80 def __del__(self): - self.close() + try: + self.close() + except Exception: + pass def close(self): """Closes all files. If you put real :class:`file` objects into the @@ -600,14 +603,11 @@ class Client(object): def __init__(self, application, response_wrapper=None, use_cookies=True, allow_subdomain_redirects=False): self.application = application - if response_wrapper is None: - response_wrapper = lambda a, s, h: (a, s, h) self.response_wrapper = response_wrapper if use_cookies: self.cookie_jar = _TestCookieJar() else: self.cookie_jar = None - self.redirect_client = None self.allow_subdomain_redirects = allow_subdomain_redirects def set_cookie(self, server_name, key, value='', max_age=None, @@ -629,6 +629,46 @@ class Client(object): self.set_cookie(server_name, key, expires=0, max_age=0, path=path, domain=domain) + def run_wsgi_app(self, environ, buffered=False): + """Runs the wrapped WSGI app with the given environment.""" + if self.cookie_jar is not None: + self.cookie_jar.inject_wsgi(environ) + rv = run_wsgi_app(self.application, environ, buffered=buffered) + if self.cookie_jar is not None: + self.cookie_jar.extract_wsgi(environ, rv[2]) + return rv + + def resolve_redirect(self, response, new_location, environ, buffered=False): + """Resolves a single redirect and triggers the request again + directly on this redirect client. + """ + scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(new_location) + base_url = urlparse.urlunsplit((scheme, netloc, '', '', '')).rstrip('/') + '/' + + cur_server_name = netloc.split(':', 1)[0].split('.') + real_server_name = get_host(environ).rsplit(':', 1)[0].split('.') + + if self.allow_subdomain_redirects: + allowed = cur_server_name[-len(real_server_name):] == real_server_name + else: + allowed = cur_server_name == real_server_name + + if not allowed: + raise RuntimeError('%r does not support redirect to ' + 'external targets' % self.__class__) + + # For redirect handling we temporarily disable the response + # wrapper. This is not threadsafe but not a real concern + # since the test client must not be shared anyways. + old_response_wrapper = self.response_wrapper + self.response_wrapper = None + try: + return self.open(path=script_root, base_url=base_url, + query_string=qs, as_tuple=True, + buffered=buffered) + finally: + self.response_wrapper = old_response_wrapper + 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 @@ -670,61 +710,25 @@ class Client(object): finally: builder.close() - if self.cookie_jar is not None: - self.cookie_jar.inject_wsgi(environ) - rv = run_wsgi_app(self.application, environ, buffered=buffered) - if self.cookie_jar is not None: - self.cookie_jar.extract_wsgi(environ, rv[2]) + response = self.run_wsgi_app(environ, buffered=buffered) # handle redirects redirect_chain = [] - status_code = int(rv[1].split(None, 1)[0]) - while status_code in (301, 302, 303, 305, 307) and follow_redirects: - if not self.redirect_client: - # assume that we're not using the user defined response wrapper - # so that we don't need any ugly hacks to get the status - # code from the response. - self.redirect_client = Client(self.application) - self.redirect_client.cookie_jar = self.cookie_jar + while 1: + status_code = int(response[1].split(None, 1)[0]) + if status_code not in (301, 302, 303, 305, 307) \ + or not follow_redirects: + break + new_location = Headers.linked(response[2])['location'] + new_redirect_entry = (new_location, status_code) + if new_redirect_entry in redirect_chain: + raise ClientRedirectError('loop detected') + redirect_chain.append(new_redirect_entry) + environ, response = self.resolve_redirect(response, new_location, + environ, buffered=buffered) - redirect = dict(rv[2])['Location'] - - scheme, netloc, script_root, qs, anchor = urlparse.urlsplit(redirect) - base_url = urlparse.urlunsplit((scheme, netloc, '', '', '')).rstrip('/') + '/' - - cur_server_name = netloc.split(':', 1)[0].split('.') - real_server_name = get_host(environ).split(':', 1)[0].split('.') - - if self.allow_subdomain_redirects: - allowed = cur_server_name[-len(real_server_name):] == real_server_name - else: - allowed = cur_server_name == real_server_name - - if not allowed: - raise RuntimeError('%r does not support redirect to ' - 'external targets' % self.__class__) - - redirect_chain.append((redirect, status_code)) - - # the redirect request should be a new request, and not be based on - # the old request - - redirect_kwargs = { - 'path': script_root, - 'base_url': base_url, - 'query_string': qs, - 'as_tuple': True, - 'buffered': buffered, - 'follow_redirects': False, - } - environ, rv = self.redirect_client.open(**redirect_kwargs) - status_code = int(rv[1].split(None, 1)[0]) - - # Prevent loops - if redirect_chain[-1] in redirect_chain[:-1]: - raise ClientRedirectError("loop detected") - - response = self.response_wrapper(*rv) + if self.response_wrapper is not None: + response = self.response_wrapper(*response) if as_tuple: return environ, response return response diff --git a/libs/werkzeug/testapp.py b/libs/werkzeug/testapp.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/urls.py b/libs/werkzeug/urls.py old mode 100644 new mode 100755 index 352b6c7a..267e8cde --- a/libs/werkzeug/urls.py +++ b/libs/werkzeug/urls.py @@ -12,6 +12,7 @@ import urlparse from werkzeug._internal import _decode_unicode from werkzeug.datastructures import MultiDict, iter_multi_items +from werkzeug.wsgi import make_chunk_iter #: list of characters that are always safe in URLs. @@ -237,20 +238,62 @@ def url_decode(s, charset='utf-8', decode_keys=False, include_empty=True, """ if cls is None: cls = MultiDict - result = [] - for pair in str(s).split(separator): + return cls(_url_decode_impl(str(s).split(separator), charset, decode_keys, + include_empty, errors)) + + +def url_decode_stream(stream, charset='utf-8', decode_keys=False, + include_empty=True, errors='replace', separator='&', + cls=None, limit=None, return_iterator=False): + """Works like :func:`url_decode` but decodes a stream. The behavior + of stream and limit follows functions like + :func:`~werkzeug.wsgi.make_line_iter`. The generator of pairs is + directly fed to the `cls` so you can consume the data while it's + parsed. + + .. versionadded:: 0.8 + + :param stream: a stream with the encoded querystring + :param charset: the charset of the query string. + :param decode_keys: set to `True` if you want the keys to be decoded + as well. + :param include_empty: Set to `False` if you don't want empty values to + appear in the dict. + :param errors: the decoding error behavior. + :param separator: the pair separator to be used, defaults to ``&`` + :param cls: an optional dict class to use. If this is not specified + or `None` the default :class:`MultiDict` is used. + :param limit: the content length of the URL data. Not necessary if + a limited stream is provided. + :param return_iterator: if set to `True` the `cls` argument is ignored + and an iterator over all decoded pairs is + returned + """ + if return_iterator: + cls = lambda x: x + elif cls is None: + cls = MultiDict + pair_iter = make_chunk_iter(stream, separator, limit) + return cls(_url_decode_impl(pair_iter, charset, decode_keys, + include_empty, errors)) + + +def _url_decode_impl(pair_iter, charset, decode_keys, include_empty, + errors): + for pair in pair_iter: if not pair: continue if '=' in pair: key, value = pair.split('=', 1) else: + if not include_empty: + continue key = pair value = '' key = _unquote_plus(key) if decode_keys: key = _decode_unicode(key, charset, errors) - result.append((key, url_unquote_plus(value, charset, errors))) - return cls(result) + yield key, url_unquote_plus(value, charset, errors) def url_encode(obj, charset='utf-8', encode_keys=False, sort=False, key=None, @@ -274,10 +317,41 @@ def url_encode(obj, charset='utf-8', encode_keys=False, sort=False, key=None, :param key: an optional function to be used for sorting. For more details check out the :func:`sorted` documentation. """ + return separator.join(_url_encode_impl(obj, charset, encode_keys, sort, key)) + + +def url_encode_stream(obj, stream=None, charset='utf-8', encode_keys=False, + sort=False, key=None, separator='&'): + """Like :meth:`url_encode` but writes the results to a stream + object. If the stream is `None` a generator over all encoded + pairs is returned. + + .. versionadded:: 0.8 + + :param obj: the object to encode into a query string. + :param stream: a stream to write the encoded object into or `None` if + an iterator over the encoded pairs should be returned. In + that case the separator argument is ignored. + :param charset: the charset of the query string. + :param encode_keys: set to `True` if you have unicode keys. + :param sort: set to `True` if you want parameters to be sorted by `key`. + :param separator: the separator to be used for the pairs. + :param key: an optional function to be used for sorting. For more details + check out the :func:`sorted` documentation. + """ + gen = _url_encode_impl(obj, charset, encode_keys, sort, key) + if stream is None: + return gen + for idx, chunk in enumerate(gen): + if idx: + stream.write(separator) + stream.write(chunk) + + +def _url_encode_impl(obj, charset, encode_keys, sort, key): iterable = iter_multi_items(obj) if sort: iterable = sorted(iterable, key=key) - tmp = [] for key, value in iterable: if value is None: continue @@ -289,9 +363,7 @@ def url_encode(obj, charset='utf-8', encode_keys=False, sort=False, key=None, value = value.encode(charset) else: value = str(value) - tmp.append('%s=%s' % (_quote(key), - _quote_plus(value))) - return separator.join(tmp) + yield '%s=%s' % (_quote(key), _quote_plus(value)) def url_quote(s, charset='utf-8', safe='/:'): diff --git a/libs/werkzeug/useragents.py b/libs/werkzeug/useragents.py old mode 100644 new mode 100755 diff --git a/libs/werkzeug/utils.py b/libs/werkzeug/utils.py old mode 100644 new mode 100755 index aedd86d4..491f4089 --- a/libs/werkzeug/utils.py +++ b/libs/werkzeug/utils.py @@ -189,10 +189,10 @@ class HTMLBuilder(object): buffer += '>' return buffer buffer += '>' - + children_as_string = ''.join([unicode(x) for x in children if x is not None]) - + if children_as_string: if tag in self._plaintext_elements: children_as_string = escape(children_as_string) @@ -352,7 +352,6 @@ def redirect(location, code=302): :param location: the location the response should redirect to. :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 display_location = location if isinstance(location, unicode): @@ -392,6 +391,9 @@ def import_string(import_name, silent=False): If `silent` is True the return value will be `None` if the import fails. + For better debugging we recommend the new :func:`import_module` + function to be used instead. + :param import_name: the dotted name for the object to import. :param silent: if set to `True` import errors are ignored and `None` is returned instead. @@ -584,7 +586,7 @@ class ImportStringError(ImportError): name += (name and '.') + part imported = import_string(name, silent=True) if imported: - tracked.append((name, imported.__file__)) + tracked.append((name, getattr(imported, '__file__', None))) else: track = ['- %r found in %r.' % (n, i) for n, i in tracked] track.append('- %r not found.' % name) diff --git a/libs/werkzeug/wrappers.py b/libs/werkzeug/wrappers.py old mode 100644 new mode 100755 index 1692dc47..5c575d08 --- a/libs/werkzeug/wrappers.py +++ b/libs/werkzeug/wrappers.py @@ -32,7 +32,7 @@ from werkzeug.http import HTTP_STATUS_CODES, \ 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.formparser import FormDataParser, default_stream_factory from werkzeug.utils import cached_property, environ_property, \ header_property, get_content_type from werkzeug.wsgi import get_current_url, get_host, LimitedStream, \ @@ -167,6 +167,10 @@ class BaseRequest(object): #: .. versionadded:: 0.6 dict_storage_class = ImmutableTypeConversionDict + #: The form data parser that shoud be used. Can be replaced to customize + #: the form date parsing. + form_data_parser_class = FormDataParser + def __init__(self, environ, populate_request=True, shallow=False): self.environ = environ if populate_request and not shallow: @@ -246,7 +250,7 @@ class BaseRequest(object): return _patch_wrapper(f, lambda *a: f(*a[:-2]+(cls(a[-2]),))(*a[-2:])) def _get_file_stream(self, total_content_length, content_type, filename=None, - content_length=None): + content_length=None): """Called to get a stream for the file upload. This must provide a file-like class with `read()`, `readline()` @@ -257,11 +261,6 @@ class BaseRequest(object): provide a content length for the files only the total content length matters. - .. versionchanged:: 0.5 - Previously this function was not passed any arguments. In 0.5 older - functions not accepting any arguments are still supported for - backwards compatibility. - :param total_content_length: the total content length of all the data in the request combined. This value is guaranteed to be there. @@ -274,6 +273,29 @@ class BaseRequest(object): return default_stream_factory(total_content_length, content_type, filename, content_length) + @property + def want_form_data_parsed(self): + """Returns True if the request method is ``POST``, ``PUT`` or + ``PATCH``. Can be overriden to support other HTTP methods that + should carry form data. + + .. versionadded:: 0.8 + """ + return self.environ['REQUEST_METHOD'] in ('POST', 'PUT', 'PATCH') + + def make_form_data_parser(self): + """Creates the form data parser. Instanciates the + :attr:`form_data_parser_class` with some parameters. + + .. versionadded:: 0.8 + """ + return self.form_data_parser_class(self._get_file_stream, + self.charset, + self.encoding_errors, + self.max_form_memory_size, + self.max_content_length, + self.parameter_storage_class) + def _load_form_data(self): """Method used internally to retrieve submitted data. After calling this sets `form` and `files` on the request object to multi dicts @@ -291,16 +313,9 @@ class BaseRequest(object): 'that, set `shallow` to False.') data = None stream = _empty_stream - 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, - self.max_form_memory_size, - self.max_content_length, - cls=self.parameter_storage_class, - silent=False) - except ValueError, e: - self._form_parsing_failed(e) + if self.want_form_data_parsed: + parser = self.make_form_data_parser() + data = parser.parse_from_environ(self.environ) else: # if we have a content length header we are able to properly # guard the incoming stream, no matter what request method is @@ -319,17 +334,6 @@ class BaseRequest(object): d = self.__dict__ d['stream'], d['form'], d['files'] = data - def _form_parsing_failed(self, error): - """Called if parsing of form data failed. This is currently only - invoked for failed multipart uploads. By default this method does - nothing. - - :param error: a `ValueError` object with a message why the - parsing failed. - - .. versionadded:: 0.5.1 - """ - @cached_property def stream(self): """The parsed stream if the submitted data was not multipart or @@ -601,6 +605,18 @@ class BaseResponse(object): #: your code to the name change. implicit_sequence_conversion = True + #: Should this response object correct the location header to be RFC + #: conformant? This is true by default. + #: + #: .. versionadded:: 0.8 + autocorrect_location_header = True + + #: Should this response object automatically set the content-length + #: header if possible? This is true by default. + #: + #: .. versionadded:: 0.8 + automatically_set_content_length = True + def __init__(self, response=None, status=None, headers=None, mimetype=None, content_type=None, direct_passthrough=False): if isinstance(headers, Headers): @@ -721,7 +737,7 @@ class BaseResponse(object): except KeyError: self._status = '%d UNKNOWN' % code status_code = property(_get_status_code, _set_status_code, - 'The HTTP Status code as number') + doc='The HTTP Status code as number') del _get_status_code, _set_status_code def _get_status(self): @@ -732,7 +748,7 @@ class BaseResponse(object): self._status_code = int(self._status.split(None, 1)[0]) except ValueError: self._status_code = 0 - status = property(_get_status, _set_status, 'The HTTP Status code') + status = property(_get_status, _set_status, doc='The HTTP Status code') del _get_status, _set_status def _get_data(self): @@ -746,11 +762,13 @@ class BaseResponse(object): self._ensure_sequence() return ''.join(self.iter_encoded()) def _set_data(self, value): - # if an unicode string is set, it's encoded directly. this allows - # us to guess the content length automatically in `get_wsgi_headers`. + # if an unicode string is set, it's encoded directly so that we + # can set the content length if isinstance(value, unicode): value = value.encode(self.charset) self.response = [value] + if self.automatically_set_content_length: + self.headers['Content-Length'] = str(len(value)) data = property(_get_data, _set_data, doc=_get_data.__doc__) del _get_data, _set_data @@ -940,6 +958,7 @@ class BaseResponse(object): location = None content_location = None content_length = None + status = self.status_code # iterate over the headers to find all values in one go. Because # get_wsgi_headers is used each response that gives us a tiny @@ -955,12 +974,16 @@ class BaseResponse(object): # make sure the location header is an absolute URL if location is not None: + old_location = location if isinstance(location, unicode): location = iri_to_uri(location) - headers['Location'] = urlparse.urljoin( - get_current_url(environ, root_only=True), - location - ) + if self.autocorrect_location_header: + location = urlparse.urljoin( + get_current_url(environ, root_only=True), + location + ) + if location != old_location: + headers['Location'] = location # make sure the content location is a URL if content_location is not None and \ @@ -971,9 +994,9 @@ class BaseResponse(object): # Also update content_length accordingly so that the automatic # content length detection does not trigger in the following # code. - if 100 <= self.status_code < 200 or self.status_code == 204: + if 100 <= status < 200 or status == 204: headers['Content-Length'] = content_length = '0' - elif self.status_code == 304: + elif status == 304: remove_entity_headers(headers) # if we can determine the content length automatically, we @@ -981,8 +1004,8 @@ class BaseResponse(object): # flattening the iterator or encoding of unicode strings in # 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: + if self.automatically_set_content_length and \ + self.is_sequence and content_length is None and status != 304: try: content_length = sum(len(str(x)) for x in self.response) except UnicodeError: @@ -1008,8 +1031,9 @@ class BaseResponse(object): :param environ: the WSGI environment of the request. :return: a response iterable. """ + status = self.status_code if environ['REQUEST_METHOD'] == 'HEAD' or \ - 100 <= self.status_code < 200 or self.status_code in (204, 304): + 100 <= status < 200 or status in (204, 304): return () if self.direct_passthrough: if __debug__: @@ -1244,7 +1268,7 @@ class ETagResponseMixin(object): # wsgiref. if 'date' not in self.headers: self.headers['Date'] = http_date() - if 'content-length' in self.headers: + if 'content-length' not in self.headers: self.headers['Content-Length'] = len(self.data) if not is_resource_modified(environ, self.headers.get('etag'), None, self.headers.get('last-modified')): diff --git a/libs/werkzeug/wsgi.py b/libs/werkzeug/wsgi.py old mode 100644 new mode 100755 index 730ded77..7eb0ab53 --- a/libs/werkzeug/wsgi.py +++ b/libs/werkzeug/wsgi.py @@ -8,14 +8,17 @@ :copyright: (c) 2011 by the Werkzeug Team, see AUTHORS for more details. :license: BSD, see LICENSE for more details. """ +import re import os import urllib import urlparse import posixpath import mimetypes +from itertools import chain, repeat from zlib import adler32 from time import time, mktime from datetime import datetime +from functools import partial from werkzeug._internal import _patch_wrapper from werkzeug.http import is_resource_modified, http_date @@ -570,6 +573,22 @@ class FileWrapper(object): raise StopIteration() +def make_limited_stream(stream, limit): + """Makes a stream limited.""" + if not isinstance(stream, LimitedStream): + if limit is None: + raise TypeError('stream not limited and no limit provided.') + stream = LimitedStream(stream, limit) + return stream + + +def make_chunk_iter_func(stream, limit, buffer_size): + """Helper for the line and chunk iter functions.""" + if hasattr(stream, 'read'): + return partial(make_limited_stream(stream, limit).read, buffer_size) + return iter(chain(stream, repeat(''))).next + + def make_line_iter(stream, limit=None, buffer_size=10 * 1024): """Safely iterates line-based over an input stream. If the input stream is not a :class:`LimitedStream` the `limit` parameter is mandatory. @@ -583,40 +602,85 @@ def make_line_iter(stream, limit=None, buffer_size=10 * 1024): If you need line-by-line processing it's strongly recommended to iterate over the input stream using this helper function. - :param stream: the stream to iterate over. + .. versionchanged:: 0.8 + This function now ensures that the limit was reached. + + .. versionadded:: 0.9 + added support for iterators as input stream. + + :param stream: the stream or iterate to iterate over. :param limit: the limit in bytes for the stream. (Usually content length. Not necessary if the `stream` is a :class:`LimitedStream`. :param buffer_size: The optional buffer size. """ - if not isinstance(stream, LimitedStream): - if limit is None: - raise TypeError('stream not limited and no limit provided.') - stream = LimitedStream(stream, limit) - _read = stream.read + def _iter_basic_lines(): + _read = make_chunk_iter_func(stream, limit, buffer_size) + buffer = [] + while 1: + new_data = _read() + if not new_data: + break + new_buf = [] + for item in chain(buffer, new_data.splitlines(True)): + new_buf.append(item) + if item and item[-1:] in '\r\n': + yield ''.join(new_buf) + new_buf = [] + buffer = new_buf + if buffer: + yield ''.join(buffer) + + # This hackery is necessary to merge 'foo\r' and '\n' into one item + # of 'foo\r\n' if we were unlucky and we hit a chunk boundary. + previous = '' + for item in _iter_basic_lines(): + if item == '\n' and previous[-1:] == '\r': + previous += '\n' + item = '' + if previous: + yield previous + previous = item + if previous: + yield previous + + +def make_chunk_iter(stream, separator, limit=None, buffer_size=10 * 1024): + """Works like :func:`make_line_iter` but accepts a separator + which divides chunks. If you want newline based processing + you should use :func:`make_limited_stream` instead as it + supports arbitrary newline markers. + + .. versionadded:: 0.8 + + .. versionadded:: 0.9 + added support for iterators as input stream. + + :param stream: the stream or iterate to iterate over. + :param separator: the separator that divides chunks. + :param limit: the limit in bytes for the stream. (Usually + content length. Not necessary if the `stream` + is a :class:`LimitedStream`. + :param buffer_size: The optional buffer size. + """ + _read = make_chunk_iter_func(stream, limit, buffer_size) + _split = re.compile(r'(%s)' % re.escape(separator)).split buffer = [] while 1: - if len(buffer) > 1: - yield buffer.pop() - continue - - # we reverse the chunks because popping from the last - # position of the list is O(1) and the number of chunks - # read will be quite large for binary files. - chunks = _read(buffer_size).splitlines(True) - chunks.reverse() - - first_chunk = buffer and buffer[0] or '' - if chunks: - if first_chunk.endswith('\n') or first_chunk.endswith('\r'): - yield first_chunk - first_chunk = '' - first_chunk += chunks.pop() - if not first_chunk: - return - - buffer = chunks - yield first_chunk + new_data = _read() + if not new_data: + break + chunks = _split(new_data) + new_buf = [] + for item in chain(buffer, chunks): + if item == separator: + yield ''.join(new_buf) + new_buf = [] + else: + new_buf.append(item) + buffer = new_buf + if buffer: + yield ''.join(buffer) class LimitedStream(object): @@ -688,14 +752,21 @@ class LimitedStream(object): """This is called when the stream tries to read past the limit. The return value of this function is returned from the reading function. - - Per default this raises a :exc:`~werkzeug.exceptions.BadRequest`. """ if self.silent: return '' from werkzeug.exceptions import BadRequest raise BadRequest('input stream exhausted') + def on_disconnect(self): + """What should happen if a disconnect is detected? The return + value of this function is returned from read functions in case + the client went away. By default a + :exc:`~werkzeug.exceptions.ClientDisconnected` exception is raised. + """ + from werkzeug.exceptions import ClientDisconnected + raise ClientDisconnected() + def exhaust(self, chunk_size=1024 * 16): """Exhaust the stream. This consumes all the data left until the limit is reached. @@ -718,9 +789,15 @@ class LimitedStream(object): """ if self._pos >= self.limit: return self.on_exhausted() - if size is None: + if size is None or size == -1: # -1 is for consistence with file size = self.limit - read = self._read(min(self.limit - self._pos, size)) + to_read = min(self.limit - self._pos, size) + try: + read = self._read(to_read) + except (IOError, ValueError): + return self.on_disconnect() + if to_read and len(read) != to_read: + return self.on_disconnect() self._pos += len(read) return read @@ -732,7 +809,12 @@ class LimitedStream(object): size = self.limit - self._pos else: size = min(size, self.limit - self._pos) - line = self._readline(size) + try: + line = self._readline(size) + except (ValueError, IOError): + return self.on_disconnect() + if size and not line: + return self.on_disconnect() self._pos += len(line) return line @@ -758,6 +840,13 @@ class LimitedStream(object): last_pos = self._pos return result + def tell(self): + """Returns the position of the stream. + + .. versionadded:: 0.9 + """ + return self._pos + def next(self): line = self.readline() if line is None: