Diagnostics : approche par signatures

Une fois que nous avons tiré l’essentiel des packagers tels que pip, conda, mamba ainsi que du système d’import de Python, nous pouvons nous intéresser à une approche par signature pour diagnostiquer l’installation d’un paquetage donné. C’était mon approche initiale, que j’ai mise de coté pour produire un module pypkgs optimisé. Mais il n’en reste pas moins que c’est une c’est une voie intéressante à explorer, en particulier lorsqu’on a des outils externes ou fournissant une API Python, mais qui ne peuvent pas être testés par la première approche. Les fonctions de recherche et d’indexation dans buildez.sys permettent de redéfinir des outils de recherche par signature qui sont intégrés dans le module pypkgs_files et complètent le module pypkgs. Le code est plus léger et simplifié par rapport à la génération précédente (module libtools.py), il est destiné à compléter pypkgs pour la construction de macros qui sont regroupées dans le module pypkgs_macros.

1. Définition d’une signature

Les signatures sont implémentées sous la forme de dictionnaires incluant plusieurs champs qui définissent ce que l’on doit chercher, ou le chercher, et comment le faire, en fonction de l’OS.

  • Champ 'name' : C’est le nom de la signature, par exemple 'openbabel', 'pybel', ‘openbabel_data'.
  • Champ 'ostype': Le système d’exploitation auquel s’applique la signature, 'UX' pour un système type UNIX/Linux ou 'DOS' pour les systèmes basés sur MS-Windows. Nous pouvons ajouter d’autres types d’OS, il faut se mettre d’accord sur la chaine qui les identifie. La fonction getostype du module buildez.oslocal est utilisée en mode simple (imprécise), si elle détecte 'LINUX|DARWIN|CYGWIN' elle renvoie sur 'UX', si 'WINDOWS' elle renvoie sur 'DOS'.
  • Champ 'targets' : Une liste qui identifie la racine des arborescences à partir de lesquelles chercher (plus bas). Par exemple ["c:/local"] ou ['/usr/bin'] en utilisant systématiquement le / et non l’antislash. Noter qu’il ne s’agit pas de répertoires Python, mais si cette clé correspond à une liste vide, le code essaiera de démarrer la recherche à partir de la racine ou des répertoires Python (cf. module pydir). On peut également spécifier cette clé pour des répertoires Python, par exemple ['/usr/lib/python3/dist-packages/'] qui ne sont pas dans l’espace utilisateur.
  • Champ 'dirs' : Un dossier particulier pouvant faire partie d’une signature, il s’agit d’une liste qui va identifier des répertoires à rechercher dans l’arborescence définie par la clé 'targets'.
  • Champ 'files' : Une liste de fichiers définis en tant que signatures. Par exemple si j’ai 'dirs':['PIL'], 'files':['ImageShow.py', 'image.py'], je veux que le répertoire 'PIL' existe et que sous ce répertoire, au niveau de la racine ou en profondeur, je puisse trouver les fichiers définis dans la liste 'files'.
  • Champ 'smode' : Il s’agit d’une chaine qui définit le mode de comparaison en trois parties séparées par des caractères '-', par exemple 'smode':'or-equal-not_strict'.

Le champ smode est important, car c’est lui qui va définir la stringence de la signature.
Dans le premier article, nous pouvons avoir 'or' ou 'and', si c’est 'and' tous les noms de la listes définie par la clé files doivent être trouvés, si c’est 'or' un de ces fichiers suffira.
Le second article peut être 'equal' ou 'find'. Dans le cas de 'equal' les noms de fichiers doivent correspondre (en entier). Dans le cas de 'find' on doit trouver une correspondance de sous-chaine. Ce mode est implémenté mais non utilisé dans le cadre d’une recherche par signature.
Enfin, le troisième article peut être positionné sur 'strict' ou 'not_strict' selon que la recherche sera sensible à la casse (ex: première lettre d’un fichier en majuscules) ou pas, respectivement.

Nous constatons que cela nous permet de faire des recherches assez précise, pour qu’une recherche par signatures, idéalement il faudrait n’avoir qu’un seul résultat à examiner. Si nous avons une centaine de fichiers comme résultats potentiels, ce type d’approche n’apporte plus grand chose.

Exemples de signatures

Elles sont définies localement dans le module pypkgs_files, et regroupées dans une liste pdcheck. Au vu de l’implémentation il serait assez facile de l’externaliser, dans un écosystème CSVM ou Json.

Exploitation

La fonction pypkgs_files_getpdcheck permet d’exploiter ce dictionnaire lorsqu’on lui donne le nom d’une signature.
Elle renvoie (targets, dirs, files, expected, filter, smode) ou les variables targets, dirs, files et smode correspondent aux clés correspondantes du dictionnaire pdcheck, donc à des listes et une chaine de caractères. La variable expected correspond à la longueur de la liste définie par la clé files.
La variable filter correspond à un filtre initialisé pour être utilisé par la fonction index_simple_filter du module buildez.sys/index_filters. Ce filtre permet d’exclure certains répertoires, par exemple '__pycache__' dans une arborescence Python, ou de spécifier des limites de profondeur de recherche à ne pas dépasser. Mais on peut s’en passer avec des signatures plus précises, c’est une différence de conception avec les codes précédents ou l’idée c’était de ne pas savoir à l’avance ou chercher. Ce filtre n’est pas utilisé, mais son appel est implémenté dans la recherche d’une signature.

2. Recherche d’une signature

La recherche va être basée sur l’écosystème basé sur les index et inclus dans buildez.sys : génération d’un index (fonctions index_tree_files, index_tree_dirs), filtrage d’un index (index_simple_filter, index_regexp_filter) et recherche dans un index (index_search_strfind).

Ce travail va être effectué par la fonction pypkgs_files_score qui va prendre comme argument le nom de la signature (si elle existe, sinon le nom du paquetage) et une liste targets qui peut être vide. Cette liste va être complétée par celle qui est définie par la clé targets des dictionnaires définissant les signatures. Cette double entrée permets une certaine flexibilité, en ajoutant d’autres localisations à partir desquelles chercher une signature.

La fonction s’initialise en vérifiant via pypkgs_files_getpdcheck que le nom de la signature existe. Si  si ce n’est pas le cas, pypkgs_files_getpdcheck ne renvoie que des listes/variables vides ou a zéro. La clé smode est convertie en tableau de bits compatible avec certaines fonctions index_ et si smode correspond à une chaine vide, elle sera initialisée à OR-FIND-NOT_STRICT (c’est l’avantage d’un tableau de bits, la valeur zéro signifie toujours quelque chose).

La première phase consiste (pour chaque item de targets) à générer un index au format dirmatrix. Puis ces index vont être filtrés via une expression régulière (fonction index_regexp_filter) pour ne retenir que les éléments faisant partie de la clé dirs (éventuellement un filtrage supplémentaire via index_simple_filter pour éliminer certaines branches de l’index). Dans cet index réduit, on applique une recherche de fichiers via index_search_strfind avec les modalités injectées par smode. Puis chaque résultat est fusionné dans un index nlfiles.

La fonction pypkgs_files_score  inclue un argument only_files positionné à True (par défaut), mais si ce n’est pas le cas, en second rideau, on peut répéter la même recherche mais en ne tenant compte que du nom des répertoires.

Le score est calculé en sommant le nombre d’items obtenus dans chaque phase (files + dirs si only_files=False) dans la variable lscore. la fonction renvoie (lscore, expected, nlfiles), l’index nlfiles correspondant au filtrage issu de la première phase.

Test drive

Prenons le cas d’un paquetage 'toto' qui est inconnu et non défini dans une signature, c’est le cas le plus défavorable. Nous aurons un appel de la fonction pypkgs_files_score correspondant à :

Et qui va nous donner le résultat :

-> check toto, targets are 'site-packages'
!package 'toto' not in base
set targets to ['C:\\Python3\\envs\\prod\\Lib\\site-packages']
index_tree_files found 32183 items
index_search_strfind bad tags []
package 'toto' file_score is 0 {-1 expected}
... done in 1.775 s
score=0, expected=-1

Effectivement, le paquetage hypothétique 'toto' n’est pas inclus dans une signature. Par défaut un répertoire Python est sélectionné, un index est calculé, mais la recherche est infructueuse. Le score est de zéro, alors qu’on attendait 1 vu que nous avions demandé un nom/signature de paquetage unique. L’étape limitante est la constitution de l’index, moins de 1 seconde sur une machine récente.

Si les recherches sont nombreuses au sein d’une même arborescence (par exemple un environnement Python), une voie d’optimisation pour ce type de codes est de ne calculer qu’une seule fois l’index puis de l’injecter dans la fonction.

3. Macros en mode signature

Pour les paquetages et outils externes dont on a défini une ou plusieurs signatures, on peut regrouper les recherches en fonctions qui s’apparentent à des macros. Reprenons le cas de OpenBabel et dépendances (internes et externes), la fonction pypkgs_files_score_OPENBABEL regroupe trois recherches :

Dans un premier temps (pass1) on vérifie la présence du logiciel Openbabel, sous Windows, en cherchant les binaires 'files': ['obabel.exe', 'obgui.exe'], sans définir un ciblage de répertoires (clé dirs, dans pdcheck), ce qui est peut être une erreur.
En seconde passe, nous recherchons  l’API Python pour Openbabel, donc la présence de pybel.py quelque part dans l’arborescence Python, pas forcément dans un répertoire faisant référence à Openbabel. Ppour le faire, on ajoute un point d’entrée à ce qui était déjà défini dans pdcheck. Enfin en troisième passe, nous cherchons la signature openbabel_data, ce qui se traduit par un fichier atomtyp.txt qui serait dans un répertoire data quelque part dans l’arborescence c:\local (nous pourrions être plus précis) ou nous installons généralement Openbabel (cf. le dictionnaire partagé).

Nous pouvons définir d’autres fonctions du même type pour d’autres paquetages, …. mais il ne s’agit que de fonctions de démonstration, pas de fonction opérationnelles, l’exemple suivant va nous le démontrer.

4. Un cas très concret

Reprenons la fonction pypkgs_files_score_OPENBABEL et regardons ce que nos signatures ont donné comme résultat :

+++ Check openbabel by files

-> check openbabel, targets defined in local dictionnary
score=1, expected=2
['f', 'c:\\local\\Gabedit248', 'obabel.exe', 'obabel', 'exe']
-> pybel, targets defined externally
score=1, expected=1
['f', 'c:\\python3\\envs\\prod\\Lib\\openbabel', 'pybel.py', 'pybel', 'py']
-> check openbabel_data, targets defined in local dictionnary
score=2, expected=1
['f', 'c:\\local\\OpenBabel-2.4.0\\data', 'atomtyp.txt', 'atomtyp', 'txt']
['f', 'c:\\local\\OpenBabel-2.4.1\\data', 'atomtyp.txt', 'atomtyp', 'txt']
... done in 3.295 s

Openbabel a donc été trouvé dans un répertoire ou est installé Gabedit qui est un outil d’interface pour Gaussian (modélisation moléculaire) et qui inclue Openbabel pour des conversions de fichiers de coordonnées moléculaires. Mais il n’est pas au bon endroit, il devrait être dans un répertoire bien à lui. D’ailleurs nous ne trouvons pas obgui.exe qui est une signature possible pour une installation spécifique. En phase 2, pybel (API d’Openbabel) est bien installée dans l’arborescence Python et dans un répertoire openbabel. Enfin, coté openbabel_data (qui doit correspondre à une variable d’environnement) , nous trouvons deux localisations possibles la ou ne devrions en trouver qu’une. Probablement deux versions d’Openbabel, sauf que si nous regardons dedans, il n’y a pas les exécutables, d’ailleurs ceux ci n’ont pas été détectés en première phase. Et si nous allons visiter le dépôt GitHub, OpenBabel est en version 3.1.1, est ce qu’un installateur venu du futur serait passé sur cette machine ?

Nous constatons que c’est un peu le désordre, rien n’est correctement installé, et si nous regardons les variables d’environnement, nous avons :

BABEL_BIN=C:\Local\OpenBabel-3.1.1
BABEL_DATADIR=C:\Python3\envs\prod\share\openbabel
BABEL_VER=3.1.1

Encore une autre version ? D’accord, c’est la bonne, mais le répertoire correspondant à BABEL_BIN n’existe pas (sinon obgui.exe et obabel.exe auraient été détectés en phase 1). Il va falloir refaire l’installation et vérifier que quelqu’un d’autre n’a pas installé Openbabel ailleurs.

Mais ce n’est pas fini …

Si nous faisons exécuter des codes du paquetage buildez.chem.pybel_interface, par exemple l’interface de test du module pybel_obj.py qui fait appel à Pybel et qui est sensé avoir besoin des bibliothèques Openbabel, et bien ça fonctionne, alors qu’Openbabel n’est pas installé.

Sauf que conda et  mamba qui installent aussi des binaires, mais dans l’arborescence Python. Nous trouvons en effet dans C:\Python3\envs\prod\Library\pkgs un dossier openbabel-3.1.1-py312hebe574a_9 qui correspond à la version et au build :

(prod) C:\Dev>mamba list | grep openbabel
openbabel      3.1.1      py312hebe574a_9      conda-forge

Et mamba a posé dans C:\Python3\envs\prod\Library\bin plusieurs binaires correspondant à Openbabel :

(prod) C:\Python3\envs\prod\Library\bin>ls ob*.exe
obabel.exe         obfitall.exe        obprobe.exe      obspectrophore.exe
obconformer.exe    obgen.exe           obprop.exe       obsym.exe
obdistgen.exe      obgrep.exe          obrms.exe        obtautomer.exe
obenergy.exe       obminimize.exe      obrotamer.exe    obthermo.exe
obfit.exe          obmm.exe            obrotate.exe

C’est en vrac et mélangé à beaucoup d’autres binaires pour d’autres paquetages. Nous avons aussi un répertoire C:\Python3\envs\prod\share\openbabel qui inclue les paramètres openbabel_data, notamment le fichier atomtyp.txt. Ce qui était finalement pris en compte dans une des variables d’environnement : BABEL_DATADIR=C:\Python3\envs\prod\share\openbabel.

Donc Openbabel est installé, mais il est dédié à Python. Nous pourrions peut être faire pointer les variables d’environnement sur cette installation, si nous avons envie de nous passer d’obgui. C’est peut être une mauvaise idée, les versions d’Openbabel (incluant obgui) et de openbabel_python pouvant être différentes.

Révision des signatures

Donc il faudrait définir de nouvelles signatures, dédiées à openbabel_python, plutôt que de tout mélanger, par exemple :

Ce qui nous donnerait comme résultat :

+++ Check openbabel_python by files

-> check obpython_openbabel, targets defined in local dictionnary
score=2, expected=2
['f', 'c:\\Python3\\envs\\prod\\Library\\bin', 'obabel.exe', 'obabel', 'exe']
['f', 'c:\\Python3\\envs\\prod\\Library\\bin', 'obrotate.exe', 'obrotate', 'exe']
-> check obpython_pybel, targets defined in local dictionnary
score=1, expected=1
['f', 'c:\\Python3\\envs\\prod\\Lib\\openbabel', 'pybel.py', 'pybel', 'py']
-> check obpython_data, targets defined in local dictionnary
score=1, expected=1
['f', 'c:\\Python3\\envs\\prod\\share\\openbabel', 'atomtyp.txt', 'atomtyp', 'txt']
... done in 1.967 s

Nous avons quelque chose qui est fonctionnel et même plus rapide que la version précédente. Il faut prendre soin à éviter les fichiers du dossier C:\Python3\envs\prod\Library\pkgs\openbabel-3.1.1-py312hebe574a_9 qui sont les mêmes que ceux que l’on recherche et qui doubleraient les résultats, il faut donc éviter (dommage) de démarrer la recherche à partir de c:/Python3/envs/prod/.

Cet exemple illustre bien les subtilités liées à une installation mixte et à une vérification plus ou moins automatisée. Il y a un fort impact du contexte et de l’érosion liée à l’utilisation d’une machine. D’où l’intérêt de procédures d’installations rigoureuses et standardisées, qui seront forcément OS dépendantes, et qu’il faudra valider pour qu’un code présent sur des systèmes différents fonctionne dans le même contexte.

5. Référentiel

Fonctions du module pypkgs_files.py en mode abrégé (octobre 2025).

Fonction Utilisation
pypkgs_files_getpdcheck Initialisation d’un dictionnaire en fonction de l’identifiant d’une signature ou d’un paquetage. Utilisé par la fonction pypkgs_files_score.
pypkgs_files_score A partir d’un identifiant de signature/paquetage, renvoie un score et une liste de fichiers/répertoires correspondant à l’analyse. Nécessite que la signature soit présente dans un dictionnaire partagé.
pypkgs_files_score_OPENBABEL Démo mixte pour Openbabel (binaires), les paquetages openbabel/pybel, le dossier openbabel_data.
pypkgs_files_score_OPENBABEL_PYTHON Equivalent à la précédente mais optimisé pour une une installation d’Openbabel via conda/mamba.
pypkgs_files_score_PIL Démo pour les paquetages PIL/pillow.
pypkgs_files_score_CAIRO Démo mixte pour les librairies Cairo et pycairo.
pypkgs_files_score_GHOSTSCRIPT Démo (outil externe) pour ghostscript.
pypkgs_files_score_GRAPHVIZ Démo mixte pour Graphviz (binaires) et graphviz_python.
pypkgs_files_score_PYDOT Démo pour le paquetage pydot.
pypkgs_files_score_XSCORE
Démo (outil externe) pour XSCORE.

6. Conclusion

Au final la recherche de signatures est concentrée dans une seule fonction et une banque de signatures, au lieu de plusieurs fonctions comme dans les versions précédentes. Le processus est simplifié, plus rapide, mais peut être moins robuste, ce qui n’est pas critique car on peut passer aussi via les fonctions du module pypkgs et réserver ce module pour des éléments externes à Python ou il sera efficace. Pour les installations mixtes, il n’y a pas beaucoup de solutions, il faudra utiliser les deux approches, au sein de macros fonctionnelles.

Retour en haut