Sans verser dans les fioritures, un petit plus ergonomique dans un programme orienté console, c’est de faire savoir que la tâche lancée prendra un certain temps et de montrer que le système continue à fonctionner normalement: c’est le principe d’une barre de progression. Il y a beaucoup d’outils sous Python qui permettent d’enrichir un terminal texte, notamment basés sur des séquences d’échappement ANSI, en direct ou via des implémentations de curses (par exemple ncurses, PDCurses). Pour un dispositif dynamique (qui évolue en fonction du degré de progression d’un programme) il me semble que c’est la bibliothèque Rich qui se dégage ces dernières années. Cette bibliothèque est multi OS (Linux, Windows, macOS), elle fournit des fonctions pour le formatage du texte et des outils comme des barres de progression, prompts, live displays …
Sauf que … rien de tout cela ne marche (ou très peu) lorsqu’on utilise la console de SciTE … qui n’est pas un vrai terminal sous Windows, et qui est aussi incompatible avec les séquences d’échappement. D’accord, oublions le formatage de texte, nous pouvons nous en passer, mais un signal style barre de progression, c’est quand même très utile. Et donc il va falloir le programmer, en mode minimaliste, ce qui fait l’objet de cette note.
Nous allons nous intéresser à une fonction d’intérêt qui n’imprime rien dans la console, qui ne communique pas, mais dont nous voulons avoir une idée de l’état d’avancement, sans modification du code ou des paramètres de cette fonction.
1. SciTE or not SciTE ?
Sous Windows, la première chose est donc de savoir si notre code s’exécute dans un shell ou dans la console de SciTE. Pour le détecter, nous allons utiliser une combinaison liée à sys.stdin (qui définit l’entrée standard).
- La méthode
sys.stdin.isattyrenverraTruesi on est connecté via un terminal interactif. - La fonction de recherche d’attribut
hasattr(sys.stdin, 'isatty')renverraTruesi un terminal existe dans stdin.
Si les deux renvoient False, le flux passe probablement par un fichier, si nous avons True-True le programme s’exécute dans un ‘vrai’ terminal, si nous avons True-False nous passons probablement par la console SciTE. Donc nous pouvons définir une fonction, puis l’enrichir plus tard en fonction de l’OS :
|
1 2 3 4 5 6 7 |
def ideutils_running_in_IDE(ide='scite'): if (str_uc(ide) == 'SCITE'): test1 = hasattr(sys.stdin, 'isatty') test2 = sys.stdin.isatty() if ((test1 == True) and (test2 == False)): return True return False return None |
Il existe d’autres recettes basées sur les variables d’environnement, par exemple une commande set nous montre que les variables SciTE_HOME et SciTE_USERHOME existent, mais cela ne nous dit pas si le code s’exécute dans la console SciTE. Une approche basée sur l’analyse des tâches serait bien compliquée en réponse à ce problème, au final nous en resterons la.
Autres IDEs
Dans le cas de Geany sous Windows, la question ne se pose pas, ne disposant pas d’une console VTE intégrée (Virtual TErminal, un widget de terminal virtuel pour Gnome/GTK) contrairement à la version Linux, c’est Windows Terminal qui s’affiche de manière externe (et qui est donc considéré comme un vrai terminal).
2. Utilisation d’un thread
Le principe de base est de découpler l’animation (style barre de progression) et la fonction d’intérêt. Nous allons utiliser un sous-processus (thread) qui se détache du programme principal et qui lance une animation, en parallèle de la fonction d’intérêt (func dans l’exemple suivant). Une fois que func est terminée, un signal stop est envoyé au thread, l’animation s’arrête et le sous-processus se termine. Pour finir avec les spécifications, nous allons utiliser une fonction d’interface ideutils_print_waiting qui masque ce mécanisme au programme qui appelle func par son intermédiaire.
|
1 2 3 4 5 6 7 8 9 10 11 12 |
def ideutils_print_waiting(msg, func, *args_func, **kwargs_func): stop_event = threading.Event() if (ideutils_running_in_IDE() == True): t = threading.Thread(target=ideutils_print_anim1, args=(stop_event, msg)) else: t = threading.Thread(target=ideutils_print_anim2, args=(stop_event, msg)) t.start() results = func(*args_func, **kwargs_func) stop_event.set() t.join() print(" ", end='') return results |
Le code est simple, mais il faut être précis dans la syntaxe et bien comprendre ce que l’on fait. En particulier sur le fait que ideutils_print_waiting étant chargée de lancer func, il faudra lui fournir à la fois les paramètres pour son propre fonctionnement mais aussi pour le fonctionnement de la fonction d’intérêt. Sauf que ideutils_print_waiting est une sous-routine générique qui doit fonctionner sans connaître func et ses arguments, donc nous allons utiliser une syntaxe particulière pour le passage des paramètres
Commençons d’abord par les arguments qui ne concernent que ideutils_print_waiting : nous n’en avons qu’un, c’est la chaine msg qui correspond au message qui sera affiché dans le terminal.
Arguments pour la fonction d’intérêt
Le premier est celui du nom (argument func) de la fonction que ideutils_print_waiting est chargé de lancer. En Python c’est très simple, le nom suffit à définir l’adresse de la fonction (la modernité à du bon). Quand aux paramètres, nous allons utiliser le mécanisme *args et ** kwargs pour définir les arguments de func. Le paramètre *args_func correspond aux arguments classiques, et le paramètre **kwargs_func correspond à des arguments nommés. Puis on lancera la fonction d’intérêt de cette manière: results = func(*args_func, **kwargs_func) sans s’occuper du détail de arguments de func. Ni du résultat qu’elle nous retourne, juste une variable results, c’est tout, et ideutils_print_waiting la renverra à son tour sans se poser de questions.
Quand il faut renvoyer plusieurs valeurs, les programmeurs ont souvent l’habitude de les regrouper dans une seule variable, donc si c’est déjà prévu dans la fonction d’intérêt, cela ne devrait pas poser de problèmes. Par exemple, return(x,y,z) pour trois valeurs de coordonnées renvoyées dans un tuple, qu’on lira (x, y, z) = ma_fonction(...) dans le code qui exploite les résultats de ma_fonction.
Exploitation éventuelle des *arguments
Si intermédiairement, avant de lancer func, nous voulons exploiter les arguments passés à travers ideutils_print_waiting (par exemple pour les vérifier) il faut connaître quelques petites choses.
Les arguments de type *args se présentent sous la forme d’un tuple (n-uplets correspondant à des listes non modifiables) et on accède à la valeur d’un argument par sa position dans le tuple. Par exemple pour la fonction ma_fonction(a, b) appelée avec ma_fonction(*args), nous récupèrerons la variable a (premier argument) par a = args[0].
Pour les arguments de type **kwargs qui sont nommés par un mot clé, le fonctionnement est analogue à celui d’un dictionnaire. Prenons l’exemple ma_fonction(x, y, z, units='meter') ou units est un argument nommé et affecté d’une valeur 'meter' par défaut. Dans ce cas, nous pourrions accéder à chaque argument nommé en utilisant un système (clé, valeur) : on utilisera kwargs['units'] pour obtenir la valeur de la variable units.
Démarrage-arrêt du thread
On initialise stop_event en tant qu’évènement, puis on instancie un thread t en fonction de l’environnement, tel qu’il a été détecté par ideutils_running_in_IDE.
- Si nous sommes dans la console de SciTE, le thread utilise une fonction d’animation compatible (fonction
ideutils_print_anim1) qui correspond à des points qui s’ajoutent au fil du temps. - Si nous sommes dans un terminal, le thread est associé à une autre fonction d’animation, par exemple un caractère qui tourne (fonction
ideutils_print_anim2).
Enfin nous lançons le thread avec t.start(), puis la fonction d’intérêt (func) associée à ses arguments. Lorsque celle ci renvoie le résultat, nous envoyons le signal stop, puis on termine le thread avec t.join() sans attendre la fin du processus parent. Il ne reste plus qu’à renvoyer le résultat de func.
A la ligne suivante, la syntaxe print(" ", end='') permet d’ajouter un espace sans faire un retour à la ligne, ce qui permettra d’ajouter (depuis programme principal) un texte à la suite de l’animation, qui mentionne que func est terminée. Donc nous aurons la séquence msg + animation + espace + texte final sur la même ligne de texte dans la console.
3. Fonctions d’animation
Nous en définissons deux, une qui est compatible avec SciTE et l’autre qui ne peut s’exécuter que dans un terminal.
La première fonction ideutils_print_anim1 permet d’ajouter chaque 2 secondes un point après la chaine msg. Nous utilisons la fonctionnalité end de print qui permet de remplacer le passage à la ligne suivante par un autre caractère ou rien.
|
1 2 3 4 5 6 |
def ideutils_print_anim1(stop_event, msg): t = threading.current_thread() print(msg, end='') while not stop_event.is_set(): print(".", end='') time.sleep(2) |
Au final, cette fonction imite une sorte de barre de progression basée sur des points et est fonctionnelle dans la console de SciTE.
La seconde fonction ideutils_print_anim2 affiche un caractère qui donne l’illusion de tourner (rotating character). On utilise le message msg associé à un caractère (vertical, oblique, horizontal) dans une chaine. Cette chaîne est écrite dans le tampon de stdout puis purgée (flush) périodiquement (chaque 0.2 secondes). Seul le caractère |/-\ change à chaque itération, ce qui donne l’illusion de la rotation de celui ci. Enfin, une dernière chaine de caractère reste après le while (à la réception du signal stop) ou deux points remplacent définitivement le caractère tournant.
|
1 2 3 4 5 6 7 8 9 |
def ideutils_print_anim2(stop_event, msg): t = threading.current_thread() while not stop_event.is_set(): for char in '|/-\\': sys.stdout.write(f'\r{msg} {char}') sys.stdout.flush() time.sleep(0.2) sys.stdout.write(f'\r{msg} :') sys.stdout.flush() |
Cette fonction ne marche pas sous SciTE (Windows) car on ne peut pas purger le flux stdout.
4. Un exemple d’utilisation
La fonction ideutils_print_waiting s’utilise très simplement, je prends l’exemple de l’appel à une fonction index_simple_counter qui va compter le nombre de fichiers, répertoires, liens, et autres items dans un arbre, ce qui peut prendre du temps s’il y a de la volumétrie et de la profondeur.
|
1 |
def index_simple_counter(index, format, verb): |
L’argument index est un arbre sérialisé sous la forme d’une matrice à deux colonnes (format='simple') ou 5 colonnes (format='dirmatrix') avec indication du type d’item ('d', 'f' … pour directory, file …) dans la première colonne. Enfin, verb (entier ≤ 0) indique le niveau de verbosité dans les messages qu’envoie la fonction au fur et à mesure de sa progression. Avec verb=0, la fonction est silencieuse. Il n’y a pas d’arguments nommés,(on aurait pu le faire avec verb) et tous les arguments sont imposés (pas de valeur par défaut).
En séquentiel
Pour appeler la fonction de comptage sans utiliser ideutils_print_waiting il faudra écrire quelque chose dans le genre :
|
1 2 3 |
print("+++ Counting => ", end='') counts = index_simple_counter(l, format=myformats[k], verb=0) print("[files, dirs, links, unkn, total]", counts) |
Nous imprimons un message "+++ Counting => ", sans retour à la ligne, puis la console reste silencieuse pendant l’exécution séquentielle de index_simple_counter. Quand le compte est terminé, on imprime sur la même ligne (après la flèche et son espace) un message qui rends compte du résultat, et le contenu de la variable count : une liste incluant le nombre de fichiers, répertoires, liens, éléments non reconnus et total des éléments. Puis on passe à la ligne suivante.
En Parallèle
Si nous utilisons ideutils_print_waiting nous avons le même message, puis l’appel à la fonction de comptage et à ses arguments (l pour l’index, myformats[k] pour le format et 0 pour le niveau de verbosité), chaque élément à la suite de l’autre, sans parenthèses.
|
1 2 |
counts = ideutils_print_waiting('+++ Counting ', index_simple_counter, l, myformats[k], 0) print("[files, dirs, links, unkn, total]", counts) |
En concurrence avec la fonction de comptage, la fonction d’animation ajoutera chaque 2 secondes un point après le message, puis une fois le thread terminé, nous aurons la même instruction pour afficher le contenu de la variable count, sur la même ligne, après les points, puis passage à la ligne suivante.
Test d’exécution
Avec le même code, c’est la fonction ideutils_print_waiting qui se charge de déterminer l’animation en fonction du contexte, nous aurons un affichage à points dans la console de SciTE :
![]() |
Nb: les couleurs sont liées à des fonctionnalités de SciTE sous Windows (notamment coloration pour les erreurs) que l’on détourne … Par exemple, une série
+++en début de ligne, donnera un rendu rouge sombre sur le texte imprimé après ce tag.
Si Python est lancé dans un shell (par exemple Windows Terminal), nous aurons la chance de voir le caractère tournant (image de gauche) pendant l’exécution de index_simple_counter puis le message final (image de droite).
![]() |
![]() |
Utiliser un sous-processus, cela parait un peu excessif (bien que cela soit très simple: synchronisation minimale, pas de section critique …) mais c’est la manière la plus lisible pour faire ce type d’opération. Mais c’est un travail à ne faire qu’une fois, si nous disposons de la fonction d’interface et des fonctions d’animation (module ideutils.py dans buildez ) c’est pratique à utiliser. Il pourrait y avoir quelques améliorations, par exemple passer une valeur qui modifie la fréquence d’affichage. Mais je reste partisan de ne pas alourdir ce type de fonctions, s’il y a trop de paramètres l’expérience montre qu’on tends à ne plus les utiliser.
Liens et lectures
- Ergonomie des interfaces informatiques [ https://fr.wikipedia.org/wiki/Ergonomie_des_interfaces_informatiques ].
- Séquence d’échappement ANSI [ https://fr.wikipedia.org/wiki/S%C3%A9quence_d%27%C3%A9chappement_ANSI ].
- Bibliothèque Curses [ https://fr.wikipedia.org/wiki/Curses ].
- Bibliothèque ncurses [ https://fr.wikipedia.org/wiki/Ncurses ].
- Windows curses [ https://pypi.org/project/windows-curses/ ].
- Welcome to Rich’s documentation! [ https://rich.readthedocs.io/en/stable/ ].
- Progress bar [ https://en.wikipedia.org/wiki/Progress_bar ].
- Thread (informatique) [ https://fr.wikipedia.org/wiki/Thread_(informatique) ].


