'Applatir' un dossier de fichiers .py

Bonjour à tous!

La question

Connaissez-vous un moyen / outil / package pour ‘aplatir’ un dossier de fichiers .py en une seule chaine de caractères ou fichier .py ? L’idée serait de prendre un fichier de type main.py et d’y ajouter dans le fichier tout le code qui est importé depuis les fichiers et sous-dossiers adjacentes, de préférence sans ajouter tout ce qui est importé depuis le standard library.

L’Exemple

donc par exemple avec un dossier qui ressemble à ça :

main.py
utils.py
junk.py
my_package
| __init.py
| main.py

Ne serait ajouté à main.py que le code qui est importé des fichiers listés et non pas tout le code merdique dans junk.py ni d’autres imports du stdlib

La complication

Ca doit tourner sous IronPython (2.7) donc pas de C compilé - que du pure python sans des trucs récents Python 3. C’est pour la création des plugins pour le logiciel Rhino3D, qui passe par une chaine de caractères pour représenter le script, et qui ne permet donc pas une organisation du code par module, plusieurs fichiers, …

Mes idées

J’ai cherché sur GitHub mais j’ai l’impression de manquer un peu de vocabulaire : flatten py files, inline code …? J’ai pense à inspect.getsource() mais il faut gérer les espaces nom à l’import. J’ai pensé à tokenize pour cela sans aller plus loin pour l’instant.

Voilà tout - merci pour vos idées !

Salut,

Déjà le problème de n’inclure que ce qui est utilisé c’est que ça peut changer d’une utilisation sur l’autre : le code Python est dynamique et donc les modules importés peuvent varier selon la configuration ou différents paramètres extérieurs.

Sinon je ne connais pas d’outils qui fassent cela, je pense qu’il serait possible d’intervenir au niveau du meta_path pour tracer les modules importés, mais il faudrait encore inspecter pour savoir ce qui y est ou sera effectivement utilisé ou non (encore une fois dans la limite de ce qui est possible).

1 « J'aime »

Ahh oui mais je pensais inclure tout ce qui pourrait être importé par inspection du code et non pas tout ce qui a vraiment été importé par exécution … meta_path me donne une liste vide … Si je comprends l’idée je pourrais ajouter ma propre fonction ici pour enregistrer tout import et ainsi connaitre le nom du fichier, son chemin et le nom importé? Effectivement je crois que cela me donne toutes les infos mais m’oblige d’exécuter 100% de mon code pour être sur de tout tracer…

Je doute que ça existe exactement comme tu l’entends, mais tu peux tenter la méthode La Rache.

Il y a plusieurs problèmes à “applatir simplement”, c’est ce qui me fait douter de l’existance de l’outil :

  • Les utilisations de __name__ typiquement dans getLogger, mais j’imagine que tu n’en as pas.
  • Les conflits de noms : en regroupant tout dans un même fichier tu regroupe tout dans un même namespace, deux fonctions ayant le même nom s’écraseront.
  • Probablement d’autres ?

Une approche bête mais qui peut fonctionner, je te laisse tester :

  • Fais tous tes imports à coup de “from mon_module import *”, ça te permet de “simuler” que tu n’as qu’un seul espace de nom, donc si ça marche avec plusieurs fichiers, ça marchera avec un seul.
  • passe un gros sed -i 's/^from \(.*\) import \*$/#include "\1.py"/g' **/*.py, si t’as pas fait d’import * sur la stdlib, ça ne remplacera pas les imports de la stdlib, c’est un moyen détourner de ne remplacer que tes imports…
  • “compile” en utilisant le pré-compilo C qui va s’occuper des #include pour toi : cpp main.py
1 « J'aime »

dancergraham,
Pourquoi ne pas mettre un commentaire, ou dans la doc du fichier, quelque chose qui dis ce que tu souhaites garder?
Exemple # plane
Pour le coup c’est pas trop de boulot et à parser c’est très rapide.
Ce n’est pas de la progr, mais une idée.

1 « J'aime »

En mécanisme standard, il y a l’import direct depuis un .zip: zipimport — Importer des modules à partir d'archives Zip — Documentation Python 3.9.2 Comme python3 est capable de lire les .zip nativement, il suffit ensuite d’ajouter #!/usr/bin/env python3 devant le .zip et on a un exécutable self-contained. Youtube-dl utilise ce mécanisme par exemple: http://yt-dl.org/

1 « J'aime »

Ha :smiley: je ne connaissais pas :stuck_out_tongue:

C’est !False- je pense qu’il faut que je corrige cela avant de me lancer trop loin dans ce nouveau projet…

Je suis sous Windows alors pas de sed ni cpp mais pas de souci pour traduire ça en python - et c’est sans doute plus simple que de jouer avec tokenize merci!

Oui c’est possible. J’ai plusieurs fonctions que je réutilise dans différents outils - faudra donc multiplier les tags pour identifier tous les outils qui ont besoin de la fonction mais oui c’est tout à fait envisageable pour une douzaine de scripts. A mettre peut-être dans le docstring. :thinking:

Oui bien vu et je peux mettre des images à coté de mon script dans le plugin - il faut que je regarde si je peux avoir des zips également - çela pourrait être le moyen de contourner la limitation!

Y’a peut être aussi une communauté pour ton logiciel, tu n’es peut être pas le premier à te poser la question… tu as cherché dans leurs forums / stackoverflow / … ?

Oui j’ai échangé avec d’autres utilisateurs ainsi que le dev principal Python de la boite - il n’a pas de solution à proposer aujourd’hui et c’est dans son pile de travail depuis quelques années… Je fais partie des “gurus” python de la communauté :cry: :smiley:

Pour l’instant je distribue l’ensemble de mes scripts manuellement, ce qui me permet d’organiser mon code comme je veux, et le toolbar de boutons séparément mais ca serait plus propre de tout distribuer dans un ensemble cohérent. Je penche vers une solution:

Question №271 : Que se passe-il quand tu fais un “import truc” ? Quelle est l’erreur exactement / pourquoi ça ne marche pas ?

Je me demande si tu peux t’en sortir a coup de sys.meta_path, avec dans le cas extrême un Loader qui va chercher le module over HTTP ?

Je pense que c’est un ImportError : voici le message

image

On voit que le script est transformé en simple <string> pour exécution dans IronPython.

J’ai redemandé s’il est possible de connaitre le chemin de mon plugin (qui a une partie de nom aléatoire) pour l’ajouter à sys.path et retrouver mes fichiers. Au pire je pourrais garder une trace de la version de mon plugin et copier mes fichiers à jour à partir du réseau vers l’équivalent de site packages si besoin une fois par session…

Oui un téléchargement via internet pourrait marcher pour d’autres utilisateurs. Exécuter du code arbitraire chargé sur internet : quoi de plus sur :slight_smile:

“le chemin de ton plugin” n’a peut être pas trop de sens, puisque c’est une string. Par contre tu peux tenter d’append un chemin en dur à sys.path, chemin dans lequel tu mettrai tes modules, ça fonctionne ça ?

Si ça fonctionne, tu peux même jouer à garder tes modules Rhino3D extrêmement minimalistes, juste ce qu’il faut pour importer ton code, qui lui se trouve dans un dossier, versionné, propre.

Exécuter du code arbitraire chargé sur internet : quoi de plus sur :slight_smile:

Parle-en a la communauté Go ? Et puis bon, c’est pas moins sûr qu’un pip install.

1 « J'aime »

Bonjour,

Vocabulaire:
Un linter vérifie les erreurs de code, ça concerne le style par exemple le respect de la pep8 .
Un minifier va faire péter le job du linter en réduisant le code à un minimum de caractères.
Un bundler réduit un projet à un minimum de fichiers.
Un transpiler permet de rendre du code compatible d’une version à l’autre du langage, exemple, python2 vers python3.
Un inliner va transformer tous les appels à des fonctions ou méthodes en code inline (redondant), il va donc inclure le code dans le “fichier appelant”.

Il existe aussi d’autres méthodes, plus spécifiques comme les nombreuses options de refactoring de code proposées par pycharm ou d’autres ide qui permettent d’extraire un code redondant vers des méthodes, d’extraire des méthodes vers des modules, d’importer automatiquement les modules manquants, d’optimiser les imports, de supprimer les parties de code qui ne sont jamais exécutées…

Je crois que ce que tu recherche s’apel simplement:
bundler / inliner.
exemple: GitHub - Akrog/pinliner: Python Inliner merges in a single file all files from a Python package.

1 « J'aime »

Je découvre le sujet. Ne serait-il pas possible d’utiliser la string pour faire un appel externe ? Et ainsi conserver ta structure de fichier par plugin sans devoir tout applatir ?

1 « J'aime »

Bonjour,

L’interpréteur ironpython est intégré dans le logiciel et inclut les namespaces nécessaires à travailler avec la géométrie, etc etc. A priori il n’est pas possible de le faire fonctionner par appel externe. Par contre ils sont en train de créer de plus en plus d’interopérabilité avec CPython avec un tout nouveau système de communication via flask alors il sera peut-être possible de garder une instance CPython ouverte en parallèle avec Rhino et ainsi communiquer entre les deux :smiley:

Tu as essayé le coup du sys.meta_path loader ?

Pas encore mais mon :rhinoceros: refonctionne après 2 semaines sans licenses donc il sera temps d’essayer… :muscle: