From 7a08331ac062ee1e2ae3c7547f77c2d342ddb3df Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Sun, 29 May 2011 15:54:58 +0200 Subject: [PATCH] Latest iteration of the blueprint code. Far from being done --- flask/app.py | 83 ++++++++-------- flask/blueprints.py | 172 +++++++++++++++++++++++++++++++++ flask/ctx.py | 11 +++ flask/helpers.py | 62 ++++++++---- flask/module.py | 221 ++++--------------------------------------- flask/templating.py | 5 +- flask/wrappers.py | 23 +++-- tests/flask_tests.py | 1 + 8 files changed, 304 insertions(+), 274 deletions(-) create mode 100644 flask/blueprints.py diff --git a/flask/app.py b/flask/app.py index 2ed8a65f..2a83a020 100644 --- a/flask/app.py +++ b/flask/app.py @@ -28,7 +28,7 @@ from .config import ConfigAttribute, Config from .ctx import RequestContext from .globals import _request_ctx_stack, request from .session import Session, _NullSession -from .module import _ModuleSetupState +from .module import blueprint_is_module from .templating import DispatchingJinjaLoader, Environment, \ _default_template_ctx_processor from .signals import request_started, request_finished, got_request_exception, \ @@ -103,14 +103,6 @@ class Flask(_PackageBoundObject): #: :class:`~flask.Response` for more information. response_class = Response - #: Path for the static files. If you don't want to use static files - #: you can set this value to `None` in which case no URL rule is added - #: and the development server will no longer serve any static files. - #: - #: This is the default used for application and modules unless a - #: different value is passed to the constructor. - static_path = '/static' - #: The debug flag. Set this to `True` to enable debugging of the #: application. In debug mode the debugger will kick in when an unhandled #: exception ocurrs and the integrated server will automatically reload @@ -213,10 +205,19 @@ class Flask(_PackageBoundObject): #: .. versionadded:: 0.7 test_client_class = None - def __init__(self, import_name, static_path=None): + def __init__(self, import_name, static_path=None, static_url_path=None, + static_folder='static'): _PackageBoundObject.__init__(self, import_name) if static_path is not None: - self.static_path = static_path + from warnings import warn + warn(DeprecationWarning('static_path is now called ' + 'static_url_path'), stacklevel=2) + static_url_path = static_path + + if static_url_path is not None: + self.static_url_path = static_url_path + if static_folder is not None: + self.static_folder = static_folder #: The configuration dictionary as :class:`Config`. This behaves #: exactly like a regular dictionary but supports additional methods @@ -242,14 +243,14 @@ class Flask(_PackageBoundObject): #: A dictionary with lists of functions that should be called at the #: beginning of the request. The key of the dictionary is the name of - #: the module this function is active for, `None` for all requests. + #: the blueprint this function is active for, `None` for all requests. #: This can for example be used to open database connections or #: getting hold of the currently logged in user. To register a #: function here, use the :meth:`before_request` decorator. self.before_request_funcs = {} #: A dictionary with lists of functions that should be called after - #: each request. The key of the dictionary is the name of the module + #: each request. The key of the dictionary is the name of the blueprint #: this function is active for, `None` for all requests. This can for #: example be used to open database connections or getting hold of the #: currently logged in user. To register a function here, use the @@ -258,7 +259,7 @@ class Flask(_PackageBoundObject): #: A dictionary with lists of functions that are called after #: each request, even if an exception has occurred. The key of the - #: dictionary is the name of the module this function is active for, + #: dictionary is the name of the blueprint this function is active for, #: `None` for all requests. These functions are not allowed to modify #: the request, and their return values are ignored. If an exception #: occurred while processing the request, it gets passed to each @@ -270,7 +271,7 @@ class Flask(_PackageBoundObject): #: A dictionary with list of functions that are called without argument #: to populate the template context. The key of the dictionary is the - #: name of the module this function is active for, `None` for all + #: name of the blueprint this function is active for, `None` for all #: requests. Each returns a dictionary that the template context is #: updated with. To register a function here, use the #: :meth:`context_processor` decorator. @@ -278,11 +279,6 @@ class Flask(_PackageBoundObject): None: [_default_template_ctx_processor] } - #: all the loaded modules in a dictionary by name. - #: - #: .. versionadded:: 0.5 - self.modules = {} - #: all the attached blueprints in a directory by name. Blueprints #: can be attached multiple times so this dictionary does not tell #: you how often they got attached. @@ -328,9 +324,10 @@ class Flask(_PackageBoundObject): # while the server is running (usually happens during development) # but also because google appengine stores static files somewhere # else when mapped with the .yml file. - self.add_url_rule(self.static_path + '/', - endpoint='static', - view_func=self.send_static_file) + if self.has_static_folder: + self.add_url_rule(self.static_url_path + '/', + endpoint='static', + view_func=self.send_static_file) @property def propagate_exceptions(self): @@ -456,9 +453,9 @@ class Flask(_PackageBoundObject): to add extra variables. """ funcs = self.template_context_processors[None] - mod = _request_ctx_stack.top.request.module - if mod is not None and mod in self.template_context_processors: - funcs = chain(funcs, self.template_context_processors[mod]) + bp = _request_ctx_stack.top.request.blueprint + if bp is not None and bp in self.template_context_processors: + funcs = chain(funcs, self.template_context_processors[bp]) orig_ctx = context.copy() for func in funcs: context.update(func()) @@ -565,6 +562,8 @@ class Flask(_PackageBoundObject): The module system was deprecated in favor for the blueprint system. """ + assert blueprint_is_module(module), 'register_module requires ' \ + 'actual module objects. Please upgrade to blueprints though.' if not self.enable_modules: raise RuntimeError('Module support was disabled but code ' 'attempted to register a module named %r' % module) @@ -577,12 +576,7 @@ class Flask(_PackageBoundObject): 'of that extension instead. (Registered %r)' % module), stacklevel=2) - options.setdefault('url_prefix', module.url_prefix) - options.setdefault('subdomain', module.subdomain) - self.view_functions.update(module.view_functions) - state = _ModuleSetupState(self, **options) - for func in module._register_events: - func(state) + self.register_blueprint(module, **options) def register_blueprint(self, blueprint, **options): """Registers a blueprint on the application. @@ -987,9 +981,9 @@ class Flask(_PackageBoundObject): request handling is stopped. """ funcs = self.before_request_funcs.get(None, ()) - mod = request.module - if mod and mod in self.before_request_funcs: - funcs = chain(funcs, self.before_request_funcs[mod]) + bp = request.blueprint + if bp is not None and bp in self.before_request_funcs: + funcs = chain(funcs, self.before_request_funcs[bp]) for func in funcs: rv = func() if rv is not None: @@ -1009,12 +1003,12 @@ class Flask(_PackageBoundObject): instance of :attr:`response_class`. """ ctx = _request_ctx_stack.top - mod = ctx.request.module + bp = ctx.request.blueprint if not isinstance(ctx.session, _NullSession): self.save_session(ctx.session, response) funcs = () - if mod and mod in self.after_request_funcs: - funcs = reversed(self.after_request_funcs[mod]) + if bp is not None and bp in self.after_request_funcs: + funcs = reversed(self.after_request_funcs[bp]) if None in self.after_request_funcs: funcs = chain(funcs, reversed(self.after_request_funcs[None])) for handler in funcs: @@ -1029,9 +1023,9 @@ class Flask(_PackageBoundObject): tighter control over certain resources under testing environments. """ funcs = reversed(self.teardown_request_funcs.get(None, ())) - mod = request.module - if mod and mod in self.teardown_request_funcs: - funcs = chain(funcs, reversed(self.teardown_request_funcs[mod])) + bp = request.blueprint + if bp is not None and bp in self.teardown_request_funcs: + funcs = chain(funcs, reversed(self.teardown_request_funcs[bp])) exc = sys.exc_info()[1] for func in funcs: rv = func(exc) @@ -1120,6 +1114,13 @@ class Flask(_PackageBoundObject): response = self.make_response(self.handle_exception(e)) return response(environ, start_response) + @property + def modules(self): + from warnings import warn + warn(DeprecationWarning('Flask.modules is deprecated, use ' + 'Flask.blueprints instead'), stacklevel=2) + return self.blueprints + def __call__(self, environ, start_response): """Shortcut for :attr:`wsgi_app`.""" return self.wsgi_app(environ, start_response) diff --git a/flask/blueprints.py b/flask/blueprints.py new file mode 100644 index 00000000..96a97d30 --- /dev/null +++ b/flask/blueprints.py @@ -0,0 +1,172 @@ +# -*- coding: utf-8 -*- +""" + flask.blueprints + ~~~~~~~~~~~~~~~~ + + Blueprints are the recommended way to implement larger or more + pluggable applications in Flask 0.7 and later. + + :copyright: (c) 2011 by Armin Ronacher. + :license: BSD, see LICENSE for more details. +""" +import os + +from .helpers import _PackageBoundObject, _endpoint_from_view_func + + +class BlueprintSetupState(object): + """Temporary holder object for registering a blueprint with the + application. + """ + + def __init__(self, blueprint, app, options): + self.app = app + self.blueprint = blueprint + self.options = options + + subdomain = self.options.get('subdomain') + if subdomain is None: + subdomain = self.blueprint.subdomain + self.subdomain = subdomain + + url_prefix = self.options.get('url_prefix') + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + self.url_prefix = url_prefix + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + if self.url_prefix: + rule = self.url_prefix + rule + options.setdefault('subdomain', self.subdomain) + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) + self.app.add_url_rule(rule, '%s.%s' % (self.blueprint.name, endpoint), + view_func, **options) + + +class Blueprint(_PackageBoundObject): + """Represents a blueprint. + + .. versionadded:: 0.7 + """ + + def __init__(self, name, import_name, static_folder=None, + static_url_path=None, url_prefix=None, + subdomain=None): + _PackageBoundObject.__init__(self, import_name) + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.static_folder = static_folder + self.static_url_path = static_url_path + self.deferred_functions = [] + + def _record(self, func): + self.deferred_functions.append(func) + + def make_setup_state(self, app, options): + return BlueprintSetupState(self, app, options) + + def register(self, app, options): + """Called by :meth:`Flask.register_blueprint` to register a blueprint + on the application. This can be overridden to customize the register + behavior. Keyword arguments from + :func:`~flask.Flask.register_blueprint` are directly forwarded to this + method in the `options` dictionary. + """ + state = self.make_setup_state(app, options) + if self.has_static_folder: + state.add_url_rule(self.static_url_path + '/', + view_func=self.send_static_file, + endpoint='static') + + for deferred in self.deferred_functions: + deferred(state) + + def route(self, rule, **options): + """Like :meth:`Flask.route` but for a module. The endpoint for the + :func:`url_for` function is prefixed with the name of the module. + """ + def decorator(f): + self.add_url_rule(rule, f.__name__, f, **options) + return f + return decorator + + def add_url_rule(self, rule, endpoint=None, view_func=None, **options): + """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for + the :func:`url_for` function is prefixed with the name of the module. + """ + def register_rule(state): + state.add_url_rule(rule, endpoint, view_func, **options) + self._record(register_rule) + + def endpoint(self, endpoint): + """Like :meth:`Flask.endpoint` but for a module. This does not + prefix the endpoint with the module name, this has to be done + explicitly by the user of this method. + """ + def decorator(f): + def register_endpoint(state): + state.app.view_functions[endpoint] = f + self._record(register_endpoint) + return f + return decorator + + def before_request(self, f): + """Like :meth:`Flask.before_request` but for a module. This function + is only executed before each request that is handled by a function of + that module. + """ + self._record(lambda s: s.app.before_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def before_app_request(self, f): + """Like :meth:`Flask.before_request`. Such a function is executed + before each request, even if outside of a module. + """ + self._record(lambda s: s.app.before_request_funcs + .setdefault(None, []).append(f)) + return f + + def after_request(self, f): + """Like :meth:`Flask.after_request` but for a module. This function + is only executed after each request that is handled by a function of + that module. + """ + self._record(lambda s: s.app.after_request_funcs + .setdefault(self.name, []).append(f)) + return f + + def after_app_request(self, f): + """Like :meth:`Flask.after_request` but for a module. Such a function + is executed after each request, even if outside of the module. + """ + self._record(lambda s: s.app.after_request_funcs + .setdefault(None, []).append(f)) + return f + + def context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a module. This + function is only executed for requests handled by a module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(self.name, []).append(f)) + return f + + def app_context_processor(self, f): + """Like :meth:`Flask.context_processor` but for a module. Such a + function is executed each request, even if outside of the module. + """ + self._record(lambda s: s.app.template_context_processors + .setdefault(None, []).append(f)) + return f + + def app_errorhandler(self, code): + """Like :meth:`Flask.errorhandler` but for a module. This + handler is used for all requests, even if outside of the module. + """ + def decorator(f): + self._record(lambda s: s.app.errorhandler(code)(f)) + return f + return decorator diff --git a/flask/ctx.py b/flask/ctx.py index bc4877cd..1e700a84 100644 --- a/flask/ctx.py +++ b/flask/ctx.py @@ -13,6 +13,7 @@ from werkzeug.exceptions import HTTPException from .globals import _request_ctx_stack from .session import _NullSession +from .module import blueprint_is_module class _RequestGlobals(object): @@ -96,6 +97,16 @@ class RequestContext(object): except HTTPException, e: self.request.routing_exception = e + # Support for deprecated functionality. This is doing away with + # Flask 1.0 + blueprint = self.request.blueprint + if blueprint is not None: + # better safe than sorry, we don't want to break code that + # already worked + bp = app.blueprints.get(blueprint) + if bp is not None and blueprint_is_module(bp): + self.request._is_old_module = True + def push(self): """Binds the request context to the current context.""" _request_ctx_stack.push(self) diff --git a/flask/helpers.py b/flask/helpers.py index 45ecf349..a2afa0f6 100644 --- a/flask/helpers.py +++ b/flask/helpers.py @@ -156,19 +156,6 @@ def make_response(*args): def url_for(endpoint, **values): """Generates a URL to the given endpoint with the method provided. - The endpoint is relative to the active module if modules are in use. - - Here are some examples: - - ==================== ======================= ============================= - Active Module Target Endpoint Target Function - ==================== ======================= ============================= - `None` ``'index'`` `index` of the application - `None` ``'.index'`` `index` of the application - ``'admin'`` ``'index'`` `index` of the `admin` module - any ``'.index'`` `index` of the application - any ``'admin.index'`` `index` of the `admin` module - ==================== ======================= ============================= Variable arguments that are unknown to the target endpoint are appended to the generated URL as query arguments. If the value of a query argument @@ -181,12 +168,17 @@ def url_for(endpoint, **values): :param _external: if set to `True`, an absolute URL is generated. """ ctx = _request_ctx_stack.top - if '.' not in endpoint: - mod = ctx.request.module - if mod is not None: - endpoint = mod + '.' + endpoint - elif endpoint.startswith('.'): - endpoint = endpoint[1:] + if not ctx.request._is_old_module: + if endpoint[:1] == '.': + endpoint = request.blueprint + endpoint + else: + # TODO: get rid of this deprecated functionality in 1.0 + if '.' not in endpoint: + mod = ctx.request.blueprint + if mod is not None: + endpoint = mod + '.' + endpoint + elif endpoint.startswith('.'): + endpoint = endpoint[1:] external = values.pop('_external', False) return ctx.url_adapter.build(endpoint, values, force_external=external) @@ -489,6 +481,8 @@ class locked_cached_property(object): class _PackageBoundObject(object): + template_folder = 'templates' + def __init__(self, import_name): #: The name of the package or module. Do not change this once #: it was set by the constructor. @@ -497,6 +491,28 @@ class _PackageBoundObject(object): #: Where is the app root located? self.root_path = _get_package_path(self.import_name) + self._static_folder = None + self._static_url_path = None + + def _get_static_folder(self): + if self._static_folder is not None: + return os.path.join(self.root_path, self._static_folder) + def _set_static_folder(self, value): + self._static_folder = value + static_folder = property(_get_static_folder, _set_static_folder) + del _get_static_folder, _set_static_folder + + def _get_static_url_path(self): + if self._static_url_path is None: + if self.static_folder is None: + return None + return '/' + os.path.basename(self.static_folder) + return self._static_url_path + def _set_static_url_path(self, value): + self._static_url_path = value + static_url_path = property(_get_static_url_path, _set_static_url_path) + del _get_static_url_path, _set_static_url_path + @property def has_static_folder(self): """This is `True` if the package bound object's container has a @@ -504,7 +520,7 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ - return os.path.isdir(os.path.join(self.root_path, 'static')) + return self.static_folder is not None @locked_cached_property def jinja_loader(self): @@ -512,7 +528,9 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ - return FileSystemLoader(os.path.join(self.root_path, 'templates')) + if self.template_folder is not None: + return FileSystemLoader(os.path.join(self.root_path, + self.template_folder)) def send_static_file(self, filename): """Function used internally to send static files from the static @@ -520,6 +538,8 @@ class _PackageBoundObject(object): .. versionadded:: 0.5 """ + if not self.has_static_folder: + raise RuntimeError('No static folder for this object') return send_from_directory(os.path.join(self.root_path, 'static'), filename) diff --git a/flask/module.py b/flask/module.py index 5c914991..2e9b9db4 100644 --- a/flask/module.py +++ b/flask/module.py @@ -9,109 +9,25 @@ :license: BSD, see LICENSE for more details. """ -from .helpers import _PackageBoundObject, _endpoint_from_view_func - - -def _register_module(module, static_path): - """Internal helper function that returns a function for recording - that registers the `send_static_file` function for the module on - the application if necessary. It also registers the module on - the application. - """ - def _register(state): - state.app.modules[module.name] = module - # do not register the rule if the static folder of the - # module is the same as the one from the application. - if state.app.root_path == module.root_path: - return - path = static_path - if path is None: - path = state.app.static_path - if state.url_prefix: - path = state.url_prefix + path - state.app.add_url_rule(path + '/', - endpoint='%s.static' % module.name, - view_func=module.send_static_file, - subdomain=state.subdomain) - return _register - - -class _ModuleSetupState(object): - - def __init__(self, app, url_prefix=None, subdomain=None): - self.app = app - self.url_prefix = url_prefix - self.subdomain = subdomain - - -class Module(_PackageBoundObject): - """Container object that enables pluggable applications. A module can - be used to organize larger applications. They represent blueprints that, - in combination with a :class:`Flask` object are used to create a large - application. - - A module is like an application bound to an `import_name`. Multiple - modules can share the same import names, but in that case a `name` has - to be provided to keep them apart. If different import names are used, - the rightmost part of the import name is used as name. - - Here's an example structure for a larger application:: - - /myapplication - /__init__.py - /views - /__init__.py - /admin.py - /frontend.py - - The `myapplication/__init__.py` can look like this:: - - from flask import Flask - from myapplication.views.admin import admin - from myapplication.views.frontend import frontend - - app = Flask(__name__) - app.register_module(admin, url_prefix='/admin') - app.register_module(frontend) - - And here's an example view module (`myapplication/views/admin.py`):: +import os - from flask import Module - - admin = Module(__name__) - - @admin.route('/') - def index(): - pass +from .helpers import _PackageBoundObject, _endpoint_from_view_func +from .blueprints import Blueprint - @admin.route('/login') - def login(): - pass - For a gentle introduction into modules, checkout the - :ref:`working-with-modules` section. +def blueprint_is_module(bp): + """Used to figure out if something is actually a module""" + return isinstance(bp, Module) - .. versionadded:: 0.5 - The `static_path` parameter was added and it's now possible for - modules to refer to their own templates and static files. See - :ref:`modules-and-resources` for more information. - .. versionadded:: 0.6 - The `subdomain` parameter was added. +class Module(Blueprint): + """Deprecated module support. Until Flask 0.6 modules were a different + name of the concept now available as blueprints in Flask. They are + essentially doing the same but have some bad semantics for templates and + static files that were fixed with blueprints. - :param import_name: the name of the Python package or module - implementing this :class:`Module`. - :param name: the internal short name for the module. Unless specified - the rightmost part of the import name - :param url_prefix: an optional string that is used to prefix all the - URL rules of this module. This can also be specified - when registering the module with the application. - :param subdomain: used to set the subdomain setting for URL rules that - do not have a subdomain setting set. - :param static_path: can be used to specify a different path for the - static files on the web. Defaults to ``/static``. - This does not affect the folder the files are served - *from*. + .. versionchanged:: 0.7 + Modules were deprecated in favor for blueprints. """ def __init__(self, import_name, name=None, url_prefix=None, @@ -120,111 +36,8 @@ class Module(_PackageBoundObject): assert '.' in import_name, 'name required if package name ' \ 'does not point to a submodule' name = import_name.rsplit('.', 1)[1] - _PackageBoundObject.__init__(self, import_name) - self.name = name - self.url_prefix = url_prefix - self.subdomain = subdomain - self.view_functions = {} - self._register_events = [_register_module(self, static_path)] - - def route(self, rule, **options): - """Like :meth:`Flask.route` but for a module. The endpoint for the - :func:`url_for` function is prefixed with the name of the module. - """ - def decorator(f): - self.add_url_rule(rule, f.__name__, f, **options) - return f - return decorator - - def add_url_rule(self, rule, endpoint=None, view_func=None, **options): - """Like :meth:`Flask.add_url_rule` but for a module. The endpoint for - the :func:`url_for` function is prefixed with the name of the module. - - .. versionchanged:: 0.6 - The `endpoint` argument is now optional and will default to the - function name to consistent with the function of the same name - on the application object. - """ - def register_rule(state): - the_rule = rule - if state.url_prefix: - the_rule = state.url_prefix + rule - options.setdefault('subdomain', state.subdomain) - the_endpoint = endpoint - if the_endpoint is None: - the_endpoint = _endpoint_from_view_func(view_func) - state.app.add_url_rule(the_rule, '%s.%s' % (self.name, - the_endpoint), - view_func, **options) - self._record(register_rule) - - def endpoint(self, endpoint): - """Like :meth:`Flask.endpoint` but for a module.""" - def decorator(f): - self.view_functions[endpoint] = f - return f - return decorator - - def before_request(self, f): - """Like :meth:`Flask.before_request` but for a module. This function - is only executed before each request that is handled by a function of - that module. - """ - self._record(lambda s: s.app.before_request_funcs - .setdefault(self.name, []).append(f)) - return f - - def before_app_request(self, f): - """Like :meth:`Flask.before_request`. Such a function is executed - before each request, even if outside of a module. - """ - self._record(lambda s: s.app.before_request_funcs - .setdefault(None, []).append(f)) - return f - - def after_request(self, f): - """Like :meth:`Flask.after_request` but for a module. This function - is only executed after each request that is handled by a function of - that module. - """ - self._record(lambda s: s.app.after_request_funcs - .setdefault(self.name, []).append(f)) - return f - - def after_app_request(self, f): - """Like :meth:`Flask.after_request` but for a module. Such a function - is executed after each request, even if outside of the module. - """ - self._record(lambda s: s.app.after_request_funcs - .setdefault(None, []).append(f)) - return f - - def context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. This - function is only executed for requests handled by a module. - """ - self._record(lambda s: s.app.template_context_processors - .setdefault(self.name, []).append(f)) - return f - - def app_context_processor(self, f): - """Like :meth:`Flask.context_processor` but for a module. Such a - function is executed each request, even if outside of the module. - """ - self._record(lambda s: s.app.template_context_processors - .setdefault(None, []).append(f)) - return f - - def app_errorhandler(self, code): - """Like :meth:`Flask.errorhandler` but for a module. This - handler is used for all requests, even if outside of the module. - - .. versionadded:: 0.4 - """ - def decorator(f): - self._record(lambda s: s.app.errorhandler(code)(f)) - return f - return decorator + Blueprint.__init__(self, name, import_name, url_prefix=url_prefix, + subdomain=subdomain) - def _record(self, func): - self._register_events.append(func) + if os.path.isdir(os.path.join(self.root_path, 'static')): + self._static_folder = 'static' diff --git a/flask/templating.py b/flask/templating.py index 8e785169..5800bf83 100644 --- a/flask/templating.py +++ b/flask/templating.py @@ -14,6 +14,7 @@ from jinja2 import BaseLoader, Environment as BaseEnvironment, \ from .globals import _request_ctx_stack from .signals import template_rendered +from .module import blueprint_is_module def _default_template_ctx_processor(): @@ -74,7 +75,9 @@ class DispatchingJinjaLoader(BaseLoader): loader = None try: module, name = posixpath.normpath(template).split('/', 1) - loader = self.app.modules[module].jinja_loader + blueprint = self.app.blueprints[module] + if blueprint_is_module(blueprint): + loader = blueprint.jinja_loader except (ValueError, KeyError, TemplateNotFound): pass try: diff --git a/flask/wrappers.py b/flask/wrappers.py index cc35e086..8db1ca9a 100644 --- a/flask/wrappers.py +++ b/flask/wrappers.py @@ -42,6 +42,10 @@ class Request(RequestBase): #: something similar. routing_exception = None + # switched by the request context until 1.0 to opt in deprecated + # module functionality + _is_old_module = False + @property def max_content_length(self): """Read-only view of the `MAX_CONTENT_LENGTH` config key.""" @@ -61,17 +65,22 @@ class Request(RequestBase): @property def module(self): - """The name of the current module""" - if self.url_rule and \ - ':' not in self.url_rule.endpoint and \ - '.' in self.url_rule.endpoint: - return self.url_rule.endpoint.rsplit('.', 1)[0] + """The name of the current module if the request was dispatched + to an actual module. This is deprecated functionality, use blueprints + instead. + """ + from warnings import warn + warn(DeprecationWarning('modules were deprecated in favor of ' + 'blueprints. Use request.blueprint ' + 'instead.'), stacklevel=2) + if self._is_old_module: + return self.blueprint @property def blueprint(self): """The name of the current blueprint""" - if self.url_rule and ':' in self.url_rule.endpoint: - return self.url_rule.endpoint.split(':', 1)[0] + if self.url_rule and '.' in self.url_rule.endpoint: + return self.url_rule.endpoint.split('.', 1)[0] @cached_property def json(self): diff --git a/tests/flask_tests.py b/tests/flask_tests.py index 8530df8a..2abf1336 100644 --- a/tests/flask_tests.py +++ b/tests/flask_tests.py @@ -1192,6 +1192,7 @@ class ModuleTestCase(unittest.TestCase): from flask import Module app = flask.Flask(__name__) + app.testing = True app.url_map.add(Submount('/foo', [ Rule('/bar', endpoint='bar'), Rule('/', endpoint='index')