Python : compiler et faire tourner plusieurs versions sans collisions

Il faut aller chercher le code source qui vous intéresse.

Exemple, faire tourner un « vieux » Python 3.6, aller dans les versions ici et prendre celle qui nous intéresse.

Puis récupérer le code source et le compiler :

mkdir ~/source ; cd ~/source
wget https://www.python.org/ftp/python/3.6.13/Python-3.6.13.tar.xz
tar xvf Python-3.6.13.tar.xz
cd ~/source/Python-3.6.13
./configure && make
sudo make altinstall

Et voilà :

~/source/Python-3.6.13$ python3.6
Python 3.6.13 (default, May 21 2021, 17:12:12) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Serveur asynchrone TCP Python. Et le client C# Unity !

Deux exemples très courts pour vous mettre sur les rails, qui envoient et reçoivent du binaire « pur » = très peu de bande passante, avec une connexion persistante.

Je vous donne deux envois-réception qui devraient vous permettre de faire tous vos envois binaires :

  1. C# : le client envoie un octet, qui correspond à un booléen, pour dire s’il est en big ou little endian ;
  2. C# : le client envoie un message encodé en UTF-8 (oui j’ai trouve la solution qui fonctionne !) ;
  3. Python : le serveur lit ce booléen ;
  4. Python : le serveur lit le message et le dit à voix haute (sous Windows) ;
  5. Python : le serveur envoie un entier non signé, puis deux float ;
  6. C# : le client lit l’entier non signé puis deux floats.

Avec ça, vous avez de quoi comprendre et faire tous les échanges que vous voulez !

Serveur asynchrone TCP Python

import asyncio
import struct
from asyncio import StreamWriter, StreamReader
import pythoncom
import win32com.client as win32_client
HOST = '192.168.1.31'
PORT = 9696
async def handle(reader: StreamReader, writer: StreamWriter):
    is_little_endian = False
    buffer = bytearray(100)
    addr = writer.get_extra_info('peername')
    print(f"Connected with {addr!r}")
    is_little_endian, = struct.unpack_from(
        '?', await reader.read(struct.calcsize('c'))
    )
    print(f'{is_little_endian=}')
    data = await reader.read(4096)
    message = data.decode('utf8')
    pythoncom.CoInitialize()
    speak = win32_client.Dispatch('SAPI.SpVoice')
    speak.Speak(message)
    print(f"Received {message!r} from {addr!r}")
    print(f"Send: {message!r}")
    float1 = 1.1
    float2 = 2.2
    struct.pack_into(
        # =: native order, std. size & alignment
        # H: unsigned short
        # f: float
        "=Hff",
        buffer, 0, 1, float1, float2)
    writer.write(buffer)
    await writer.drain()
    print("Close the connection")
    writer.close()
async def main():
    server = await asyncio.start_server(handle, HOST, PORT)
    print(f'Serving on {server.sockets[0].getsockname()}')
    async with server:
        await server.serve_forever()
asyncio.run(main())

Client C# Unity

using System;
using System.IO;
using System.Net.Sockets;
using UnityEngine;
public class Connexion : MonoBehaviour
{
    public string server;
    public string message;
    public ushort port;
    private void Start()
    {
        // working sample to send text:
        byte[] data = System.Text.Encoding.UTF8.GetBytes(message);
        byte isLittleEndian = BitConverter.IsLittleEndian ? (byte)1 : (byte)0;
        TcpClient client = new TcpClient(server, port);
        NetworkStream stream = client.GetStream();
        // Send the message to the connected TcpServer.
        stream.WriteByte(isLittleEndian);
        stream.Write(data, 0, data.Length);
        Debug.Log($"Sent: {message}");
        // read sample
        BinaryReader reader = new BinaryReader(stream);
        uint len = reader.ReadUInt16();
        var x = reader.ReadSingle();
        var y = reader.ReadSingle();
        Debug.Log("len=" + len);
        Debug.Log($"x={x}, y={y}");
    }
}

Pour la note, ces deux exemples paraissent simples, mais ils m’ont pris un temps fou, et je n’ai eu aucune réponse au bout de 3 semaines sur stackoverflow…

Python : EAFP vs LBYL

Très souvent vous pouvez avoir deux styles de codes différents qui font la même chose en Python :


import os

if os.path.exists("fichier.txt"):
    os.unlink("fichier.txt")

import os
try:
    os.unlink("fichier.txt")
except OSError:  # levé si le fichier n'existe pas
    pass

Alors, lequel choisir ?

Personnellement, j’ai toujours préféré le premier choix, et pourtant… dans la documentation officielle, ils le déconseillent !

Pourquoi cela ? Explication : l’opposé de EAFP, c’est LBYL.

EAFP : Easier to ask for forgiveness than permission

Plus facile de demander pardon que la permission. Ce style de codage très utilisé en Python suppose l’existence de clés ou d’attributs valides et intercepte les exceptions si l’hypothèse s’avère fausse. Ce style propre et rapide se caractérise par la présence de nombreuses déclarations try and except. La technique contraste avec le style LBYL commun à de nombreux autres langages tels que C.

LBYL : Look before you leap

Réfléchir avant d’agir.

Ce style de codage teste explicitement les conditions préalables avant d’effectuer des appels ou des recherches. Ce style contraste avec l’approche EAFP et se caractérise par la présence de nombreuses déclarations if.

Dans un environnement multi-thread, l’approche LBYL peut risquer d’introduire une condition de concurrence entre « la vérification » et « la validation ». Par exemple, le code : if key in mapping: return mapping [key] peut échouer si un autre thread supprime la clé du mappage après le test, mais avant la recherche. Ce problème peut être résolu avec des verrous ou en utilisant l’approche EAFP.

Django et git : bonnes pratiques / idées pour faire du CI

Conseil d’un ami :

  • gitlab n’est pas 100% opensource, ils proposent une édition communautaire limité et la totalité des fonctionnalités est dispo avec la version entreprise, leur modèle économique c’est de brider la version CEE (community) dans pas mal de coin pour te pousser à prendre une licence, perso je trouve que c’est plus un freeware qu’autre chose
  • gitlab est pas mal mais honnêtement pour l’avoir beaucoup (vraiment beaucoup) utilisé dans le passé ce n’est pas la meilleur alternative.

Si tu cherche à mettre en place un truc je te conseille fortement de jeter un œil à :

Cette stack est un peu plus compliquée à mettre qu’un gitlab, mais c’est très puissant, surtout la partie gerrit qui transcende la manière de faire des revues de code.

Si le code Django / Python n’est pas correct, il y a des méthodes pour améliorer les choses notamment :

  • https://nvie.com/posts/a-successful-git-branching-model/
  • https://semver.org/

Il est également important de mettre en place un système de « core developer » même au sein d’un projet privé en entreprise, afin de garantir la cohérence des données et l’intégrité de l’historique. En effet, si tout le monde à les droits d’écriture sur tout, ça finira toujours à un moment donné à partir dans tous les sens… L’idée, c’est de donner les droits d’écriture à seulement 2-3 personnes de confiance qui ont un certain niveau et à tous les autres imposer de faire des forks dans leur namespace gitlab et de proposer des merge request (l’équivalent des pull requests avec github). Ainsi, la revue de code est obligatoire et il n’est plus possible de merger du mauvais code… et les développeurs « standard » (sans droits donc) ont juste 2-3 commandes de base à connaître pour bosser avec les autres. Sans ce genre de système cela ne peut pas réellement fonctionner car git à été developpé pour ce mode de fonctionnement (au même titre que svn et d’autres gestionnaire de version) et donc son workflow est idéal dans ce cadre.

Fonctionner comme ça m’a permis de :

  1. coder un petit robot qui réagissait au merge requests proposé et donc qui checkait tout un tas de choses automatiquement et qui proposait des commandes si les choses n’allaient pas
  2. responsabiliser les gens de l’équipe en les rendant responsables de leur propre forks
  3. faire monter en compétence l’équipe qui était ultra frileuse à faire du versionning puis au final à pris goût au truc…

J’ai fais des petites formations au début pour leur expliquer l’intérêt et expliquer la sémantique des changement et qu’il est important de penser ses commits de manière propre. Cela permet :

  • dans certains cas de retirer (revert) des bugs complet d’un seul coup,
  • de faire du debug de manière automatique sans rien toucher et de trouver les changements responsable d’un bug (git bisect),
  • de faire un gain énorme de qualité au final
  • d’avoir des historiques clairs avec des messages de commit ayant une réelle plus value (exemple)
  • d’automatiser tout un tas d’actions qui vont augmenter la qualité globale du projet et vont réduire les taches inutiles et donc laisser plus de temps pour les trucs fun à faire.

C’est gagnant gagnant, mais au début les gens râlent… il faut juste s’y préparer et avec le temps ils changent d’avis !

Utiliser Chrome pour imprimer / générer un PDF

Google Chrome a plein d’options cachées en ligne de commande, et parmi celles-ci, une option qui permet d’imprimer en PDF.
De plus, si, comme moi, vous ne voulez pas les numéros de pages / nom de fichier en haut et en bas, il vous faut ajouter l’option --print-to-pdf-no-header

google-chrome --headless --disable-gpu --run-all-compositor-stages-before-draw --print-to-pdf-no-header --print-to-pdf=Bureau/test.pdf file://Bureau/git_book.html

Et vous aurez votre fichier PDF qui sera généré ;

[0730/112224.164336:INFO:headless_shell.cc(615)] Written to file Bureau/test.pdf

JavaScript : comment surcharger console.log()

J’avais besoin de décaler mes log pour voir rapidement si j’étais, ou pas, à l’intérieur d’une fonction.

Eh oui, après plusieurs années, même si on peut faire des points d’arrêt, JavaScript est là pour répondre à des événements utilisateurs et autres… donc il ne faut souvent pas arrêter pendant l’exécution ou la visualisation d’une page Web… on n’a toujours pas d’autre choix que de faire le bon console.log() des familles qui fait que le développement est extrêmement ralenti…

Je me suis donc crée les fonctions log_in(), log() et log_out().
Ainsi, quand j’entre dans une fonction, j’appelle log_in(), et tous mes log() dans cette fonction seront décalés, ce qui fait que je vois immédiatement où sont appelés mes logs ! Ensuite, ne pas oublier de faire un log_out().

let logLevel = 0;
let log = function () {
    let args = Array.from(arguments);
    if (logLevel > 0) {
        args = [Array(logLevel).fill(' ').join('')].concat(args);
    }
    console.log.apply(null, args);
};
let log_in = function () {
    log.apply(null, arguments);
    logLevel++;
}, log_out = function (s) {
    if (logLevel>0) {
        logLevel--;
    }
    log.apply(null, arguments);
};

Django : comment vérifier si on est en mode debug dans le template

Vous trouverez l’information dans beaucoup de sites, et cela semble très simple : dans votre template, il suffit de faire :

{% if not debug %}Je suis en debug !{% endif %}

En réalité cela ne suffit pas.

Il faut dans votre fichier settings.py, configurer correctement les adresses IP’s qui précisent qu’on est / ou pas / en mode « développement » :

INTERNAL_IPS = ['127.0.0.1', ]

(Notez que ce code peut être largement optimisé et dépendant de l’environnement, par exemple j’ai fait un settings.py qui prend cela en compte)

Linux : comment monter un disque réseau automatiquement

Sous Mint, Lorsque mon PC démarre, je veux qu’il « monte » automatiquement mes disques et qu’il fasse quelques vérifications au passage (dossier existant etc).
Problème : il arrive parfois la même chose que sous Windows, à savoir que si le mount ne fonctionne pas, il faut le faire à la main.
J’ai donc essayé de suivre plusieurs tutoriels et la plupart disaient d’ajouter dans les tâches planifiées (« cronjob« ) une ligne qui commençait par @reboot.

Je ne sais pas pourquoi, mais sur ma version de Mint, il ne connaissait pas @reboot.

Après pas mal de recherches, voici ce que j’ai trouvé, et qui fonctionne parfaitement :

  • Éditer /etc/rc.local
  • Lui ajouter la ligne de commande que l’on veut faire

Donc mon fichier /etc/rc.local j’ai donc deux lignes de code :
/home/olivier/apply.config.perso.sh -f /home/olivier/boot.log
exit 0

(J’ai enlevé les commentaires très importants qu’il y a dans le fichier, à vous de les lire !)
Le code précédent signifie « lance le script /home/olivier/apply.config.perso.sh« .

Et je me suis fait mon script, soit /home/olivier/apply.config.perso.sh :

(Il prend en compte le paramètre « -f » pour préciser le fichier de sortie, comme cela je logue tout, on n’est jamais assez prudent).
# ----------------------------------------------
# command line options
# first save args to pass them later on to other scripts:
args=("$@")
while [ "$1" != "" ]; do
    case $1 in
        -f | --file ) shift
                                # test if output_filename starts with "-":
                                if [[ "$1" =~ ^-.* ]]; then
                                    usage
                                    exit
                                fi
                                # from that point on:
                                # direct all stdout and stderr to $1:
                                exec &>>$1
                                ;;
        -u ) # ignored here, for mount script (later in there)
                                ;;
        -h | --help ) usage
                                exit
                                ;;
        * ) usage
                                exit 1
    esac
    shift
done
echo "`(date '+%Y.%m.%d-%Hh%Mm%Ss')` $filename > Applying my own configuration..."
sudo -Hu olivier gsettings set org.cinnamon.settings-daemon.peripherals.keyboard delay 120
sudo -Hu olivier gsettings set org.cinnamon.settings-daemon.peripherals.keyboard repeat-interval 18
if mountpoint -q "/mnt/Olivier"; then
    echo "`(date '+%Y.%m.%d-%Hh%Mm%Ss')` $filename > Already mounted, nothing to do."
else
    delay=1
    while ! mountpoint -q "/mnt/Olivier"; do
        echo "`(date '+%Y.%m.%d-%Hh%Mm%Ss')` $filename > Trying to mount..."
        # re-pass the same arguments via "${args[@]}"
        /home/olivier/mount.diskstation.sh "${args[@]}"
        sleep $delay
        if [ "$delay" -gt 60 ]; then
            echo "`(date '+%Y.%m.%d-%Hh%Mm%Ss')` $filename > Couldn't mount! Too long! Exiting."
            exit 1
        fi
        delay=$((delay+5))
    done
    echo "`(date '+%Y.%m.%d-%Hh%Mm%Ss')` $filename > Successful mount."
fi
echo "`(date '+%Y.%m.%d-%Hh%Mm%Ss')` $filename > Done."

Explications :

Les deux ordres gsettings set org.cinnamon.settings-daemon.peripherals.keyboard modifient la vitesse de mon clavier (qui n’est, de base, pas assez rapide pour moi).

L’ordre /home/olivier/mount.diskstation.sh monte mon disque.

Ensuite je fais une boucle qui vérifie si mon « mount » est réussi ou pas : tant que ce n’est pas le cas, il attend un « compteur » qu’il augmente de 5s à chaque boucle, et tout ça au maximum 60 fois. Si vraiment il n’y arrive pas, il logue le problème et quitte.

Tout fonctionne parfaitement depuis maintenant 2 ans !

Django : faire des pages d’erreur sur mesure (404, 500, etc.)

Lorsque vous voulez faire des pages d’erreur sur mesure, c’est très simple… une fois que vous savez !
En effet, la documentation n’est pas très claire…

Voici un résumé de mon expérience, pour faire des pages d’erreur sur mesure (404, 500, etc.).
Il faut d’abord savoir que les erreurs sont des fonctions appelées (= vous ne pouvez pas le faire via les vues génériques).
Ensuite, la documentation donne un exemple, mais ce n’est pas assez.
Grâce aux raccourcis de Django, vous avez la fonction render() à laquelle vous pouvez passer un template et un contexte (= donc des variables).
Je me suis servi de cela pour créer un dictionnaire qui contient les erreurs, et les passer dans dans le contexte avec la clé title et content.


Voici le code des vues qui affiche une erreur « proprement » :

from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
VIEW_ERRORS = {
    404: {'title': _("404 - Page not found"),
          'content': _("A 404 Not found error indicates..."), },
    500: {'title': _("Internal error"),
          'content': _("A 500 Internal error means..."), },
    403: {'title': _("Permission denied"),
          'content': _("A 403 Forbidden error means ..."), },
    400: {'title': _("Bad request"),
          'content': _("A 400 Bad request error means ..."), }, }
def error_view_handler(request, exception, status):
    return render(request, template_name='errors.html', status=status,
                  context={'error': exception, 'status': status,
                           'title': VIEW_ERRORS[status]['title'],
                           'content': VIEW_ERRORS[status]['content']})
def error_404_view_handler(request, exception=None):
    return error_view_handler(request, exception, 404)
def error_500_view_handler(request, exception=None):
    return error_view_handler(request, exception, 500)
def error_403_view_handler(request, exception=None):
    return error_view_handler(request, exception, 403)
def error_400_view_handler(request, exception=None):
    return error_view_handler(request, exception, 400)


Une fois les vues faites, il faut aller dans la déclaration principale de vos vues. Donc le fichier urls.py qui est la racine de votre projet. Si vous mettez le code ailleurs, il sera ignoré.

Dedans, déclarez vos fonctions qui gèrent les erreurs :

handler404 = 'app.views.errors.error_404_view_handler'
handler500 = 'app.views.errors.error_500_view_handler'
handler403 = 'app.views.errors.error_403_view_handler'
handler400 = 'app.views.errors.error_400_view_handler'

Et enfin, dans vos templates, créez un fichier errors.html dans lequel vous pouvez construire votre page HTML qui gère l’erreur « proprement », avec, en plus dans le contexte, les variables {{ title }} et {{ content }} qui affichent respectivement le titre et le détail de l’erreur.

Maintenant, comment, en mode « développement », tester ces pages ? Dans la pratique vous ne pouvez pas, car en mode développement, une erreur affiche les informations de déboguage, et pas vos pages !
La solution : faire une URL qui « simule » l’erreur. Exemple avec 404 : vous ajouter dans vos urls : path('404/', error_404_view_handler), et puis d’afficher votre URL adéquate, soit http://localhost:8000/404/ et vous verrez l’erreur !