Aplicaciones modulares con Blueprints

Changelog

Nuevo en la versión 0.7.

Flask utiliza un concepto de blueprints para crear componentes de aplicaciones y soportar patrones comunes dentro de una aplicación o entre aplicaciones. Los blueprints pueden simplificar en gran medida el funcionamiento de las grandes aplicaciones y proporcionar un medio central para que las extensiones de Flask registren operaciones en las aplicaciones. Un objeto Blueprint funciona de forma similar a un objeto de aplicación Flask, pero no es realmente una aplicación. Más bien es un blueprint de cómo construir o extender una aplicación.

¿Por qué los Blueprints?

Los Blueprints en Flask están pensados para estos casos:

  • Factorizar una aplicación en un conjunto de blueprints. Esto es ideal para aplicaciones más grandes; un proyecto podría instanciar un objeto de aplicación, inicializar varias extensiones y registrar una colección de blueprints.

  • Registrar un blueprint en una aplicación en un prefijo de URL y/o subdominio. Los parámetros en el prefijo de URL/subdominio se convierten en argumentos comunes de la vista (con valores predeterminados) en todas las funciones de la vista en el blueprint.

  • Registrar un blueprint varias veces en una aplicación con diferentes reglas de URL.

  • Proporcionar filtros de plantilla, archivos estáticos, plantillas y otras utilidades a través de blueprints. Un blueprint no tiene que implementar aplicaciones o funciones de vista.

  • Registrar un blueprint en una aplicación para cualquiera de estos casos al inicializar una extensión de Flask.

Un blueprint en Flask no es una aplicación enchufable porque no es realmente una aplicación – es un conjunto de operaciones que pueden ser registradas en una aplicación, incluso múltiples veces. ¿Por qué no tener múltiples objetos de aplicación? Puedes hacerlo (ver Despacho de aplicaciones), pero tus aplicaciones tendrán configuraciones separadas y serán gestionadas en la capa WSGI.

En cambio, los blueprints proporcionan una separación a nivel de Flask, comparten la configuración de la aplicación y pueden cambiar un objeto de la aplicación según sea necesario con estar registrado. El inconveniente es que no se puede anular el registro de un blueprint una vez que se ha creado una aplicación sin tener que destruir todo el objeto de aplicación.

El concepto de Blueprints

El concepto básico de los blueprints es que registran las operaciones a ejecutar cuando se registran en una aplicación. Flask asocia las funciones de vista con los blueprints cuando despacha peticiones y genera URLs de un endpoint a otro.

Mi primer Blueprint

Este es el aspecto de un blueprint muy básico. En este caso queremos implementar un blueprint que haga un simple renderizado de plantillas estáticas:

from flask import Blueprint, render_template, abort
from jinja2 import TemplateNotFound

simple_page = Blueprint('simple_page', __name__,
                        template_folder='templates')

@simple_page.route('/', defaults={'page': 'index'})
@simple_page.route('/<page>')
def show(page):
    try:
        return render_template(f'pages/{page}.html')
    except TemplateNotFound:
        abort(404)

Cuando se vincula una función con la ayuda del decorador @simple_page.route, el blueprint registrará la intención de registrar la función show en la aplicación cuando se registre posteriormente. Además, antepondrá al punto final de la función el nombre del blueprint que se le dio al constructor Blueprint (en este caso también simple_page). El nombre del blueprint no modifica la URL, sólo el endpoint.

Registro de Blueprints

Entonces, ¿cómo se registra ese blueprint? Así:

from flask import Flask
from yourapplication.simple_page import simple_page

app = Flask(__name__)
app.register_blueprint(simple_page)

Si comprueba las normas registradas en la aplicación, encontrará estas:

>>> app.url_map
Map([<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
 <Rule '/<page>' (HEAD, OPTIONS, GET) -> simple_page.show>,
 <Rule '/' (HEAD, OPTIONS, GET) -> simple_page.show>])

El primero es obviamente de la propia aplicación para los archivos estáticos. Los otros dos son para la función show del blueprint simple_page. Como puedes ver, también están prefijados con el nombre del blueprint y separados por un punto (.).

Sin embargo, los blueprints también se pueden montar en diferentes lugares:

app.register_blueprint(simple_page, url_prefix='/pages')

Y efectivamente, estas son las reglas generadas:

>>> app.url_map
Map([<Rule '/static/<filename>' (HEAD, OPTIONS, GET) -> static>,
 <Rule '/pages/<page>' (HEAD, OPTIONS, GET) -> simple_page.show>,
 <Rule '/pages/' (HEAD, OPTIONS, GET) -> simple_page.show>])

Además, puede registrar los blueprints varias veces, aunque no todos los blueprint pueden responder adecuadamente a ello. De hecho, depende de cómo se implemente el blueprint si se puede montar más de una vez.

Anidando blueprints

Es posible registrar un blueprints dentro de otro blueprint.

parent = Blueprint('parent', __name__, url_prefix='/parent')
child = Blueprint('child', __name__, url_prefix='/child')
parent.register_blueprint(child)
app.register_blueprint(parent)

El blueprint hijo obtendrá el nombre del padre como prefijo de su nombre, y las URLs hijas llevarán el prefijo de la URL del padre.

url_for('parent.child.create')
/parent/child/create

In addition a child blueprint’s will gain their parent’s subdomain, with their subdomain as prefix if present i.e.

parent = Blueprint('parent', __name__, subdomain='parent')
child = Blueprint('child', __name__, subdomain='child')
parent.register_blueprint(child)
app.register_blueprint(parent)

url_for('parent.child.create', _external=True)
"child.parent.domain.tld"

Las funciones específicas del blueprint antes de la solicitud, etc. registradas con el padre se activarán para el hijo. Si un hijo no tiene un manejador de errores que pueda manejar una excepción dada, se intentará el del padre.

Recursos de blueprints

Los blueprints también pueden proporcionar recursos. A veces puede querer introducir un blueprint sólo por los recursos que proporciona.

Carpeta de recursos Blueprint

Al igual que para las aplicaciones normales, se considera que los blueprints están contenidos en una carpeta. Aunque varios blueprints pueden originarse en la misma carpeta, no tiene por qué ser así y no suele ser recomendable.

La carpeta se infiere del segundo argumento de Blueprint que suele ser __name__. Este argumento especifica qué módulo o paquete lógico de Python corresponde al blueprint. Si apunta a un paquete real de Python ese paquete (que es una carpeta en el sistema de archivos) es la carpeta de recursos. Si es un módulo, el paquete que contiene el módulo será la carpeta de recursos. Puedes acceder a la propiedad Blueprint.root_path para ver cuál es la carpeta de recursos:

>>> simple_page.root_path
'/Users/username/TestProject/yourapplication'

Para abrir rápidamente las fuentes de esta carpeta puedes utilizar la función open_resource():

with simple_page.open_resource('static/style.css') as f:
    code = f.read()

Archivos estáticos

Un blueprint puede exponer una carpeta con archivos estáticos proporcionando la ruta a la carpeta en el sistema de archivos con el argumento carpeta_estática. Es una ruta absoluta o relativa a la ubicación del blueprint:

admin = Blueprint('admin', __name__, static_folder='static')

Por defecto, la parte más a la derecha de la ruta es donde se expone en la web. Esto puede cambiarse con el argumento static_url_path. Como la carpeta se llama static aquí estará disponible en el url_prefix del blueprint + /static. Si el blueprint tiene el prefijo /admin, la URL estática será /admin/static.

El endpoint se llama nombre_blueprint.static. Puedes generar URLs hacia él con url_for() como lo harías con la carpeta static de la aplicación:

url_for('admin.static', filename='style.css')

Sin embargo, si el blueprint no tiene un url_prefix, no es posible acceder a la carpeta estática del blueprint. Esto se debe a que la URL sería /static en este caso, y la ruta /static de la aplicación tiene prioridad. A diferencia de las carpetas de plantillas, las carpetas estáticas de los blueprints no se buscan si el archivo no existe en la carpeta estática de la aplicación.

Plantillas

Si quieres que el blueprint exponga plantillas puedes hacerlo proporcionando el parámetro template_folder al constructor Blueprint:

admin = Blueprint('admin', __name__, template_folder='templates')

Para los archivos estáticos, la ruta puede ser absoluta o relativa a la carpeta de recursos del blueprint.

La carpeta de plantillas se añade a la ruta de búsqueda de plantillas, pero con una prioridad menor que la carpeta de plantillas de la aplicación real. De este modo, puede anular fácilmente las plantillas que un blueprint proporciona en la aplicación real. Esto también significa que si no quieres que una plantilla de blueprint sea anulada accidentalmente, asegúrate de que ningún otro blueprint o plantilla de la aplicación real tenga la misma ruta relativa. Cuando varios blueprints proporcionan la misma ruta relativa de la plantilla, el primer blueprint registrado tiene prioridad sobre los demás.

Así que si tienes un blueprint en la carpeta yourapplication/admin y quieres renderizar la plantilla 'admin/index.html y has proporcionado templates como carpeta_de_plantillas tendrás que crear un archivo como este: yourapplication/admin/templates/admin/index.html. La razón de la carpeta extra admin es para evitar que nuestra plantilla sea anulada por una plantilla llamada index.html en la carpeta de plantillas de la aplicación.

Para reiterar esto: Si tienes un blueprint llamado admin y quieres renderizar una plantilla llamada index.html que es específica para este blueprint, la mejor idea es diseñar tus plantillas así:

yourpackage/
    blueprints/
        admin/
            templates/
                admin/
                    index.html
            __init__.py

Y luego, cuando quieras renderizar la plantilla, utiliza admin/index.html como nombre para buscar la plantilla. Si tienes problemas para cargar las plantillas correctas, activa la variable de configuración EXPLAIN_TEMPLATE_LOADING, que indicará a Flask que imprima los pasos que sigue para localizar las plantillas en cada llamada a render_template.

Construcción de URLs

Si quieres enlazar de una página a otra puedes utilizar la función url_for() como lo harías normalmente sólo que antepones al punto final de la URL el nombre del blueprint y un punto (.):

url_for('admin.index')

Además, si estás en una función de vista de un blueprint o una plantilla renderizada y quieres enlazar con otro punto final del mismo blueprint, puedes utilizar redirecciones relativas anteponiendo al punto final sólo un punto:

url_for('.index')

Esto enlazará con admin.index, por ejemplo, en caso de que la solicitud actual haya sido enviada a cualquier otro punto final del blueprint de administración.

Manejadores de errores de blueprints

Los blueprints soportan el decorador errorhandler al igual que el objeto de aplicación Flask, por lo que es fácil hacer páginas de error personalizadas específicas para Blueprint.

Este es un ejemplo para una excepción «404 Página no encontrada»:

@simple_page.errorhandler(404)
def page_not_found(e):
    return render_template('pages/404.html')

La mayoría de los manejadores de error funcionarán simplemente como se espera; sin embargo, hay una advertencia sobre los manejadores para las excepciones 404 y 405. Estos manejadores de error sólo se invocan desde una sentencia raise apropiada o una llamada a abort en otra de las funciones de vista del blueprint; no son invocados por, por ejemplo, un acceso a una URL no válida. Esto se debe a que el blueprint no es «dueño» de un determinado espacio URL, por lo que la instancia de la aplicación no tiene forma de saber qué manejador de errores del blueprint debe ejecutar si se le da una URL inválida. Si quieres ejecutar diferentes estrategias de manejo para estos errores basados en prefijos de URL, pueden ser definidos a nivel de aplicación usando el objeto proxy request:

@app.errorhandler(404)
@app.errorhandler(405)
def _handle_api_error(ex):
    if request.path.startswith('/api/'):
        return jsonify(error=str(ex)), ex.code
    else:
        return ex

Véase Manejo de los errores de la aplicación.