Flask ***** .. figure:: data/flask.png Flask est un framework web beaucoup plus évolué que Bottle. Il reste compact mais comporte beaucoup plus de possibilité de bottle (a priori on ré-invente moins la roue) Il prend en charge nativement les templates **Jinja2** Il possède de plus un ensemble de plugin qui permet rapidement de lui rajouter ce qui lui manque Installation ============ Rien de plus simple avec pip .. code-block:: bash pip install flask Premier exemple =============== Commençons par un simple "hello word"... .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- from flask import Flask app = Flask(__name__) @app.route("/") def hello(): return "Hello World!" if __name__ == "__main__": app.run() Pour tester ce code, il suffit juste de lancer la commande suivante. Et d’ouvrir le navigateur sur l’URL qui sera affichée dans la console. .. code-block:: bash $ python app.py * Running on http://127.0.0.1:5000/ Pour l'instant si on compare Flask et Bottle cela ne fait pas beaucoup de différence. Et heureusement puisqu'ils respectent tous les deux le PEP de python sur les framework web. Debugger ======== Flask possède un debugger intégré très performant puisqu'il permet de lancer via un naviguateur web une console python (accessible via chaque ligne du résultat du debugger) .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- from flask import Flask app = Flask(__name__) @app.route("/") def hello(): # une erreur ici a = 1/0 return "Hello World!" if __name__ == "__main__": app.run(debug=True) .. figure:: data/flask_debug.png On peut aussi activer le debug via l'utilisation une variable globale .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- from flask import Flask # Flask configuration DEBUG = True app = Flask(__name__) app.config.from_object(__name__) @app.route("/") def hello(): # une erreur ici a = 1/0 return "Hello World!" if __name__ == "__main__": app.run() .. note:: On peut utiliser n'importe quelle variable en majuscule de l'objet utilisé dans from_object. On peut ainsi passer le path d'un fichier log ou celui d'une base de donnée On récupère les variables via le code .. code-block:: python app.config['NAME_OF_VARIABLE'] Logger ====== Flask possède de façon native un handler de logger ... on peut donc facilement rajouter des loggers .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import logging from flask import Flask # Flask configuration DEBUG = True SERVER_NAME = '0.0.0.0:5001' LEVEL = logging.INFO app = Flask(__name__) app.config.from_object(__name__) @app.route('/') def hello(): app.logger.warning('A warning occurred (%d apples)', 42) app.logger.error('An error occurred') app.logger.info('Info') return "Hello World!" if __name__ == '__main__': handler = logging.StreamHandler() handler.setLevel(app.config['LEVEL']) app.logger.addHandler(handler) app.run() .. warning:: si on passe le DEBUG a False on ne voit que les errors et les warnings plus les infos Ajout de fonction pre et post ============================= Comme pour bottle on peut décorer les appels ... mais Flask possède des décorateurs pré-défini .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import logging from flask import Flask # Flask configuration DEBUG = True SERVER_NAME = '0.0.0.0:5001' LEVEL = logging.INFO app = Flask(__name__) app.config.from_object(__name__) @app.before_request def before_request(): app.logger.warning('before call') @app.teardown_request def teardown_request(exception): app.logger.warning('after call') @app.route('/') def hello(): app.logger.warning('A warning occurred (%d apples)', 42) app.logger.error('An error occurred') app.logger.info('Info') return "Hello World!" if __name__ == '__main__': handler = logging.StreamHandler() handler.setLevel(app.config['LEVEL']) app.logger.addHandler(handler) app.run() Filtrage des formulaires et des réponses ======================================== .. code-block:: python @app.route('/contact', methods=['GET', 'POST']) def contact(): if request.method == 'GET': # afficher le formulaire else: # traiter les données reçues # afficher : "Merci de m'avoir laissé un message !" Les sessions ============ Les sessions sont donc un moyen simplifié et plus sécurisé de retenir des informations pour vos visiteurs. Les sessions utilisent deux choses distinctes : * Un cookie placé chez le client, pour identifier sa session. * Des données stockées sur le serveur, reliées à cette même session. Pour fonctionner, nous aurons besoin d'importer l'objet session : .. code-block:: python from flask import session Cet objet s'utilise ensuite de la même manière qu'un dictionnaire Python. L'avantage est qu'on peut lire et écrire dedans, on ne passe donc plus par les objets requête et réponse. Voilà l'équivalent du code avec les cookies transposé dans une version utilisant session à la place : .. code-block:: python @app.route('/') def index(): if 'pseudo' in session: return "C'est un plaisir de se revoir, {pseudo} !".format(pseudo=session['pseudo']) else: session['pseudo'] = 'Luc' return "Bonjour, c'est votre première visite ?" Le défaut des sessions est qu'on ne peut pas choisir de période d'expiration différente pour chaque variable qu'on stocke dans la session. Par défaut, la session sera effacée quand le visiteur fermera son navigateur. Si on veut qu'elle dure plus longtemps, il faut placer session.permanent = True après sa création. Elle vivra alors un mois. Pour changer cette durée, il faut modifier la configuration de l'objet app : .. code-block:: python app.config['PERMANENT_SESSION_LIFETIME'] = 3600 # la session dure une heure On peut stocker n'importe quoi dans session : entiers, chaînes, listes, objets, dictionnaires, etc. Cependant, dans le cas des listes et des dictionnaires (et de tout type mutable plus généralement), il faut notifier la session lorsqu'on les modifie, en positionnant à True l'attribut modified : .. code-block:: python session['mon_panier'].append('appareil photo') # mon_panier est une liste session.modified = True Pour finir, lorsqu'on désire supprimer une variable de la session, on utilise la méthode pop() de la session : .. code-block:: python session.pop('pseudo', None) .. note:: Le deuxième paramètre (None) est là uniquement pour éviter une erreur s'il n'y a pas de variable 'pseudo' dans la session. Les templates ============= .. code-block:: python #! /usr/bin/python # -*- coding:utf-8 -*- from flask import Flask app = Flask(__name__) @app.route('/') def accueil(): mots = ["bonjour", "à", "toi,", "visiteur."] return render_template('accueil.html', titre="Bienvenue !", mots=mots) if __name__ == '__main__': app.run(debug=True) .. note:: penser dans les template a ne pas utiliser d'adresse en dur mais d'utiliser url_for() .. code-block:: html Création d'un site statique =========================== .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import os from flask import Flask import flask # Flask configuration DEBUG = True PATH_HTML = 'C:/Users/f.aoustin/Google Drive/mydoc/build/html' PATH_INDEX = 'index.html' app = Flask(__name__) app.config.from_object(__name__) @app.route("/") def index(): return flask.send_from_directory(app.config['PATH_HTML'],app.config['PATH_INDEX']) @app.route("/") def static_web(path): return flask.send_from_directory(app.config['PATH_HTML'],path) if __name__ == "__main__": app.run() Plugins ======= flask-login +++++++++++ Généralement lors de la création d'une applicatino web une des premiere fonctionnalité est la gestion de l'accès à cette dernière. Flask possède un plugin qui gère cela pour nous: flask-login .. code-block:: python pip install flask pip install flask-login on va par la suite créer cette arborescence :: dir |__ myapp.py |__ myapp.cfg |__ templates | |__ login.html |__ statics |__ css |__ fonts |__ js | |__ myjs.js |__ less |__ scss |__ admin.html |__ protected.html On utilisera les modules font-awesomes et nos modules propres de design (cf autre billet: design web) admin.html .. code-block:: html test admin logout index protected.html .. code-block:: html test index logout admin myjs.js .. code-block:: javascript $().ready(function() { $('.tdauto').css({ width: ($(window).width() - 250)/2 + 'px' }); $("input[type=checkbox]").click(function () { $(this).toggleClass("checked"); $.ajax({ type: "POST", url: "http://192.168.1.18:5001/update", dataType: "xml", data: "id=" + $(this).attr("id"), async: true, global: false, success: function(xml) { /* todo */ }, beforeSend: function() { /* todo */ }, complete: function() { /* todo */ } }); }); // hide block expand $("div.list>input.checked").each(function() { $("div.expand>span[id='"+$(this).parent().attr("view")+"']").append(" "+$(this).next().text()+""); } ); $("input[type=radio]").click(function () { $('input:radio[name='+$(this).attr("name")+']').removeClass("checked"); $(this).toggleClass("checked"); /*$('input:radio[name='+$(this).attr("name")+']').toggleClass("checked");*/ $.ajax({ type: "POST", url: "http://192.168.1.18:5001/update", dataType: "xml", data: "id=" + $(this).attr("id")+ "&val=" + $(this).val(), async: true, global: false, success: function(xml) { /* todo */ }, beforeSend: function() { /* todo */ }, complete: function() { /* todo */ } }); /* manage list */ if ( $( this ).hasClass( "list" ) ) { $(this).parent().css("display","none"); $("div.expand>span[id='"+$(this).parent().attr("view")+"']").toggleClass("expand"); $("div.expand>span[id='"+$(this).parent().attr("view")+"']>strong").remove(); $("div.expand>span[id='"+$(this).parent().attr("view")+"']").append(" "+$(this).next().text()+""); }; }); $('input[type=text].online').on('input', function() { $.ajax({ type: "POST", url: "http://192.168.1.18:5001/update", dataType: "xml", data: "id=" + $(this).attr("id") + "&val=" + $(this).val(), async: true, global: false, success: function(xml) { /* todo */ }, beforeSend: function() { /* todo */ }, complete: function() { /* todo */ } }); }); $('input.validform').keypress(function(event){ if(event.keyCode == 13){ $(this).parents("form")[0].submit(); } }); // hide block expand $("div.expand>span").each(function() { $("[view='"+$(this).attr("id")+"']").css("display","none"); } ); $("div.expand>span").click(function () { $(this).toggleClass("expand"); if ( $( this ).hasClass( "expand" ) ) { $("[view='"+$(this).attr("id")+"']").css("display","inline-block"); } else { $("[view='"+$(this).attr("id")+"']").css("display","none"); } }); $('input[type=range]').on('input change', function() { $.ajax({ type: "POST", url: "http://192.168.1.18:5001/update", dataType: "xml", data: "id=" + $(this).attr("id") + "&val=" + $(this).val(), async: true, global: false, success: function(xml) { /* todo */ }, beforeSend: function() { /* todo */ }, complete: function() { /* todo */ } }); }); $("button").click(function () { $.ajax({ type: "POST", url: "http://192.168.1.18:5001/update", dataType: "xml", data: "id=" + $(this).attr("id"), async: true, global: false, success: function(xml) { /* todo */ }, beforeSend: function() { /* todo */ }, complete: function() { /* todo */ } }); }); }); login.html .. code-block:: html
myapp.cfg .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import os import logging DEBUG = True PATH_STATICS = './statics' SECRET_KEY = 'secret_key' PORT = 5001 HOST = '0.0.0.0' LEVEL = logging.DEBUG USERS = [ { 'id' : 1, 'username' : 'admin', 'password' : 'keyadmin', 'groups' : ['admin','user'] }, { 'id' : 2, 'username' : 'test', 'password' : 'keytest', 'groups' : ['user'] } ] myapp.py .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import os import logging from flask import Flask import flask from flask.ext.login import LoginManager from flask.ext.login import login_user , logout_user , current_user , login_required from werkzeug.security import generate_password_hash, check_password_hash app = Flask(__name__) app.config.from_pyfile('myapp.cfg') login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'login' class User(): """ class of User, for login""" def __init__(self, username): self.username = username def check_password(self, password): if self.username in [i['username'] for i in app.config['USERS']]: if [ i['password'] for i in app.config['USERS'] if i['username'] == self.username][0] == password: return True return False def in_groups(self, group): if group in [ i['groups'] for i in app.config['USERS'] if i['username'] == self.username][0]: return True return False def is_authenticated(self): return True def is_active(self): return True def is_anonymous(self): return False def get_id(self): return [ i['id'] for i in app.config['USERS'] if i['username'] == self.username][0] def __repr__(self): return '' % (self.username) class checkGroup(object): def __init__(self, group): self.group = group def __call__(self, fn): def wrapped_f(*args, **kwargs): if current_user.in_groups('admin') : return fn(*args, **kwargs) return unauthorized() return wrapped_f @app.route('/') @login_required def index(): app.logger.debug('load protected.html by %s ' % current_user.username) return flask.send_from_directory(app.config['PATH_STATICS'],'protected.html') @app.route('/admin') @login_required @checkGroup('admin') def admin(): app.logger.debug('load admin.html by %s ' % current_user.username) return flask.send_from_directory(app.config['PATH_STATICS'],'admin.html') @app.route('/login',methods=['GET','POST']) def login(): app.logger.debug('login method: %s' % flask.request.method) if flask.request.method == 'GET': return flask.render_template('login.html', warnings=[], infos=[]) username = flask.request.form['username'] app.logger.debug(username) password = flask.request.form['password'] registered_user = User(username) if registered_user.check_password(password): login_user(registered_user, remember = True) app.logger.debug('Logged in successfully') return flask.redirect(flask.request.url_root) return flask.render_template('login.html', warnings=['username and/or password are wrong',], infos=[]) #return flask.abort(401) # 401 Unauthorized @app.route('/logout') def logout(): logout_user() return flask.redirect(flask.url_for('login')) @login_manager.unauthorized_handler def unauthorized(): return "non autoriser bouh !!!" @login_manager.user_loader def load_user(id): app.logger.debug('id %s' % id) return User([i['username'] for i in app.config['USERS'] if i['id']== id][0]) @app.route("/") def static_web(path): app.logger.debug('load static %s' % path) return flask.send_from_directory(app.config['PATH_STATICS'],path) if __name__ == "__main__": handler = logging.StreamHandler() handler.setLevel(app.config['LEVEL']) app.logger.addHandler(handler) app.run(host = app.config['HOST'], port = app.config['PORT']) # TODO: https://www.openshift.com/blogs/use-flask-login-to-add-user-authentication-to-your-python-application # TODO: https://github.com/shekhargulati/flask-login-openshift-quickstart/blob/master/wsgi/todoapp.py on peut au lieu d'enregistrer le mot de passe en dure dans un fichier le **hasher** puis par la suite contrôler que le hash sauvegardé correspond au password fournit .. code-block:: python 4 >>from werkzeug.security import generate_password_hash, check_password_hash 5 >>hash = generate_password_hash('test') 6 >>print(hash) pbkdf2:sha1:1000$FaKJFp6q$eee385f5f75cf3e7da560b51580c0c9b565e0cb5 7 >>print check_password_hash(hash,'test') True 8 >>print check_password_hash(hash,'test2') False 9 >> Flask-WeasyPrint ++++++++++++++++ Il est parfois utile d'avoir un outil simple pour générer du pdf à la voler. Le plugin Flask-WeasyPrint est fait pour vous .. code-block:: python pip install flask pip install Flask-WeasyPrint on va par la suite créer cette arborescence :: dir |__ myapp.py |__ myapp.cfg |__ templates | |__ hello.html |__ statics |__ css | |__ styles.css |__ img |__ hello.png myapp.cfg .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import os DEBUG = True PATH_STATICS = './statics' SECRET_KEY = 'secret_key' PORT = 5001 HOST = '0.0.0.0' myapp.py .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- from flask import Flask, render_template, url_for, send_from_directory from flask_weasyprint import HTML, render_pdf app = Flask(__name__) app.config.from_pyfile('myapp.cfg') @app.route('/hello/', defaults={'name': 'World'}) @app.route('/hello//') def hello_html(name): return render_template('hello.html', name=name) @app.route('/hello_.pdf') def hello_pdf(name): # Make a PDF from another view return render_pdf(url_for('hello_html', name=name)) #return render_pdf(HTML(string=html)) other possibilite generate HTML @app.route("/") def static_web(path): return send_from_directory(app.config['PATH_STATICS'],path) if __name__ == "__main__": app.run(host = app.config['HOST'], port = app.config['PORT']) hello.html .. code-block:: python Hello

Hello, {{ name }}!

style.css .. code-block:: css body { font: 2em Fontin, serif } nav { font-size: .7em } @page { size: A5; margin: 1cm } @media print { nav { display: none } } on peut tester sur http://127.0.0.1:5001/hello Flask-Mail ++++++++++ On peut avoir besoin dans une application d'envoyer des mails .. code-block:: python pip install flask pip install Flask-Mail on va par la suite créer cette arborescence :: dir |__ myapp.py |__ myapp.cfg |__ templates | |__ mail.html |__ statics |__ img |__ hello.png myapp.cfg .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import os import logging DEBUG = True PATH_STATICS = './statics' SECRET_KEY = 'secret_key' PORT = 5001 HOST = '0.0.0.0' MAIL_SERVER = 'srvxxxx1' myapp.py .. code-block:: python from flask import Flask, send_from_directory from flask_mail import Mail, Message mail = Mail() app = Flask(__name__) app.config.from_pyfile('myapp.cfg') mail.init_app(app) @app.route("/mail") def send_mail(): msg = Message("Hello", sender="informatiquepz@myprop-group.com", recipients=["f.aoustin@myprop-group.com"]) msg.body = "testing" with app.open_resource("%s/img/hello.png" % app.config['PATH_STATICS']) as fp: msg.attach("hello.png", "image/png", fp.read()) #msg.html = "testing" mail.send(msg) return send_from_directory(app.config['PATH_STATICS'],'mail.html') if __name__ == "__main__": app.run(host = app.config['HOST'], port = app.config['PORT']) mail.html .. code-block:: html send mail on peut tester sur http://127.0.0.1:5001/mail Flask-WTF +++++++++ Cette extension permet de gérer simplement des formulaires. .. code-block:: python pip install flask pip install Flask-WTF Nous allons contruire une application possédant un formulaire de contact avec 4 champs: - nom - mail - sujet - message Tout les champs sont obligatoires et nous validerons le format du mail Notre application aura l'arborescence suivante :: dir |__ myapp.py |__ myapp.cfg |__ templates | |__ contact.html | |__ home.html | |__ layout.html |__ statics |__ css |__ main.css contact.html .. code-block:: html {% extends "layout.html" %} {% block content %}

Contact

{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %}
{{ form.hidden_tag() }} {{ form.name.label }} {{ form.name }} {% for message in form.name.errors %}
{{ message }}
{% endfor %} {{ form.email.label }} {{ form.email }} {% for message in form.email.errors %}
{{ message }}
{% endfor %} {{ form.subject.label }} {{ form.subject }} {% for message in form.subject.errors %}
{{ message }}
{% endfor %} {{ form.message.label }} {{ form.message }} {% for message in form.message.errors %}
{{ message }}
{% endfor %} {{ form.submit }}
{% endblock %} layout.html .. code-block:: html {% extends "layout.html" %} {% block content %} home {% endblock %} home.html .. code-block:: html Flask

Flask App

{% block content %} {% endblock %}
.. note:: on utilise içi les templates avec des notions d'extends myapp.cfg .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import os import logging DEBUG = True PATH_STATICS = './statics' SECRET_KEY = 'secret_key' PORT = 5001 HOST = '0.0.0.0' MAIL_SERVER = 'srvxxxx1' myapp.py .. code-block:: python #!/usr/bin/env python # -*- coding: utf-8 -*- import os import logging from flask import Flask import flask from flask_wtf import Form from wtforms import TextField, TextAreaField, SubmitField, validators app = Flask(__name__) app.config.from_pyfile('myapp.cfg') class RegistrationForm(Form): username = TextField('Username', [validators.Length(min=4, max=25)]) email = TextField('Email Address', [validators.Length(min=6, max=35)]) name = TextField("Name", [validators.Required("Please enter your name.")]) subject = TextField("Subject", [validators.Required("Please enter a subject.")]) message = TextAreaField("Message", [validators.Required("Please enter a message.")]) datefield = DateField('datefield') datetimefield = DateTimeField('datetimefield') decimalfield = DecimalField('decimalfield') filefield = FileField('filefield') floatfield = FloatField('floatfield') integerfield = IntegerField('integerfield') radiofield = RadioField('radiofield', choices=[('cpp', 'C++'), ('py', 'Python'), ('text', 'Plain Text')]) selectfield = SelectField('selectfield', choices=[('cpp', 'C++'), ('py', 'Python'), ('text', 'Plain Text')]) password = PasswordField('New Password', [ validators.Required(), validators.EqualTo('confirm', message='Passwords must match') ]) confirm = PasswordField('Repeat Password') accept_tos = BooleanField('I accept the TOS', [validators.Required()]) submit = SubmitField("Send") @app.route('/home') def home(): app.logger.debug(flask.render_template('home.html')) return flask.render_template('home.html') @app.route('/contact', methods=['GET', 'POST']) def contact(): form = ContactForm() if flask.request.method == 'POST': if form.validate() == False: app.logger.error('All fields are required.') flask.flash('All fields are required.') return flask.render_template('contact.html', form=form) else: app.logger.debug("name : %s" % form.name.data) app.logger.debug("email : %s" % form.email.data) app.logger.debug("subject : %s" % form.subject.data) app.logger.debug("message : %s" % form.message.data) return 'Form posted.' elif flask.request.method == 'GET': return flask.render_template('contact.html', form=form) @app.route("/") def static_web(path): app.logger.debug('load static %s' % path) return flask.send_from_directory(app.config['PATH_STATICS'],path) if __name__ == "__main__": handler = logging.StreamHandler() handler.setLevel(app.config['LEVEL']) app.logger.addHandler(handler) app.run(host = app.config['HOST'], port = app.config['PORT']) On peut tester sur http://127.0.0.1:5001/contact Il est important de noter que nous gérons les erreurs champs par champs au niveau du contact.html mais aussi au niveau gloabl grâce à la fonction flash. On peut aussi facilement rajouter un form de gestion du login .. code-block:: python from wtforms import TextField, TextAreaField, SubmitField, validators, PasswordField, BooleanField class RegistrationForm(Form): username = TextField('Username', [validators.Length(min=4, max=25)]) email = TextField('Email Address', [validators.Length(min=6, max=35)]) password = PasswordField('New Password', [ validators.Required(), validators.EqualTo('confirm', message='Passwords must match') ]) confirm = PasswordField('Repeat Password') accept_tos = BooleanField('I accept the TOS', [validators.Required()]) @app.route('/register', methods=['GET', 'POST']) def register(): form = RegistrationForm(flask.request.form) if flask.request.method == 'POST' and form.validate(): flash('Thanks for registering') return redirect(url_for('login')) return flask.render_template('register.html', form=form) Et pour register.html afin de simplifier son écriture on peut créer une fonction de template register.html .. code-block:: html {% from "_formhelpers.html" import render_field %} {% extends "layout.html" %} {% block content %}
{% for field in form %} {% if field.widget.__class__.__name__ in ('TextInput','PasswordInput','CheckboxInput','TextArea', 'ListWidget', 'Select') %} {{ render_field(field, class='wtf') }} {% endif %} {% endfor %} {{ form.submit }}
{% endblock %} _formhelpers.html .. code-block:: html {% macro render_field(field) %}
{{ field.label }}
{{ field(**kwargs)|safe }} {% if field.errors %}
    {% for error in field.errors %}
  • {{ error }}
  • {% endfor %}
{% endif %}
{% endmacro %} Concernant les éléments qu'on peut uiliser via le module wtform il y a une excellente doc https://wtforms.readthedocs.org/en/latest Il existe plusieurs validateurs: - validators.DataRequired(message=None) - validators.Email(message=None) - validators.EqualTo(fieldname, message=None) - validators.InputRequired(message=None) - validators.IPAddress(ipv4=True, ipv6=False, message=None) - validators.Length(min=-1, max=-1, message=None) - validators.MacAddress(message=None) - validators.NumberRange(min=None, max=None, message=None) - validators.Optional(strip_whitespace=True) - validators.Regexp(regex, flags=0, message=None) - validators.URL(require_tld=True, message=None) - validators.UUID(message=None) - validators.AnyOf(values, message=None, values_formatter=None) - validators.NoneOf(values, message=None, values_formatter=None) Il est possible de créer facilement des nouveaux validateurs .. code-block:: python def my_length_check(form, field): if len(field.data) > 50: raise ValidationError('Field must be less than 50 characters') class MyForm(Form): name = StringField('Name', [InputRequired(), my_length_check])