42 votes

Python 3.5+ : Comment importer dynamiquement un module en donnant le chemin d'accès complet au fichier (en présence d'imports implicites de modules frères) ?

Question

La bibliothèque standard documente clairement comment importer directement les fichiers sources (étant donné le chemin d'accès absolu au fichier source), mais cette approche ne fonctionne pas si ce fichier source utilise des importations implicites de type sibling comme décrit dans l'exemple ci-dessous.

Comment cet exemple pourrait-il être adapté pour fonctionner en présence d'importations implicites de frères et sœurs ?

J'ai déjà vérifié ce et cet autre Questions Stackoverflow sur le sujet, mais elles ne traitent pas des importations implicites de frères et sœurs. sur le fichier étant importé à la main.

Configuration/Exemple

Voici un exemple illustratif

Structure du répertoire :

root/
  - directory/
    - app.py
  - folder/
    - implicit_sibling_import.py
    - lib.py

app.py :

import os
import importlib.util

# construct absolute paths
root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
isi_path = os.path.join(root, 'folder', 'implicit_sibling_import.py')

def path_import(absolute_path):
   '''implementation taken from https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly'''
   spec = importlib.util.spec_from_file_location(absolute_path, absolute_path)
   module = importlib.util.module_from_spec(spec)
   spec.loader.exec_module(module)
   return module

isi = path_import(isi_path)
print(isi.hello_wrapper())

lib.py :

def hello():
    return 'world'

implicit_sibling_import.py :

import lib # this is the implicit sibling import. grabs root/folder/lib.py

def hello_wrapper():
    return "ISI says: " + lib.hello()

#if __name__ == '__main__':
#    print(hello_wrapper())

Running python folder/implicit_sibling_import.py avec le if __name__ == '__main__': bloc de rendement commenté ISI says: world dans Python 3.6.

Mais courir python directory/app.py rendements :

Traceback (most recent call last):
  File "directory/app.py", line 10, in <module>
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 678, in exec_module
  File "<frozen importlib._bootstrap>", line 205, in _call_with_frames_removed
  File "/Users/pedro/test/folder/implicit_sibling_import.py", line 1, in <module>
    import lib
ModuleNotFoundError: No module named 'lib'

Solution de rechange

Si j'ajoute import sys; sys.path.insert(0, os.path.dirname(isi_path)) à app.py , python app.py donne world comme prévu, mais j'aimerais éviter de broyer le sys.path si possible.

Exigences de réponse

J'aimerais python app.py pour imprimer ISI says: world et j'aimerais y parvenir en modifiant le fichier path_import fonction.

Je ne suis pas sûr des implications d'une mauvaise interprétation sys.path . Par exemple, s'il y avait directory/requests.py et j'ai ajouté le chemin vers directory à la sys.path je ne voudrais pas import requests pour commencer à importer directory/requests.py au lieu d'importer le bibliothèque des demandes que j'ai installé avec pip install requests .

La solution MUST peut être implémenté sous la forme d'une fonction python qui accepte le chemin d'accès absolu au module souhaité et renvoie l'adresse du module. objet du module .

Idéalement, la solution ne devrait pas introduire d'effets secondaires (par exemple, si elle modifie le IIddeeaallllyy,, la ssoolluuttiioonn sshhoouulldd n'a pas I idinentatrlrolodydu,uc cete h sesi idsdeoe-l-eueftffifeoecnct tsss h (o(eueglg. d. inifof t i itit n dtdoroeoesds u mcmoeod disififydy e -effecItdse a(lelgy , itfh ei ts odlouetsi omno dsihfoyu ld n'introduit pas d'effets secondaires (par exemple, si elle modifie sys.path il devrait retourner sys.path à son état initial). Si la solution introduit des effets secondaires, il faut expliquer pourquoi il n'est pas possible de trouver une solution sans introduire d'effets secondaires.


PYTHONPATH

Si j'ai plusieurs projets qui font cela, je ne veux pas avoir à me rappeler de définir PYTHONPATH à chaque fois que je passe de l'un à l'autre. L'utilisateur devrait juste être capable de pip install mon projet et l'exécuter sans aucune configuration supplémentaire.

-m

Le site -m drapeau est l'approche recommandée/pythonique, mais la bibliothèque standard documente aussi clairement les éléments suivants Comment importer directement les fichiers sources . J'aimerais savoir comment je peux adapter cette approche pour faire face aux importations relatives implicites. Il est clair que les internes de Python doivent le faire, alors comment les internes diffèrent-ils de la documentation "import direct des fichiers sources" ?

0 votes

En ce qui concerne Python, cet "import implicite de frère et soeur" est un import absolu ordinaire, et certainement pas un import relatif implicite. Les importations relatives implicites ne sont plus prises en charge dans Python 3.

1 votes

Modification du site sys.path est probablement votre meilleure option. Quoi que vous fassiez pour que la machinerie d'importation regarde dans le dossier de ce fichier, elle devra s'attarder au-delà de la durée de l'importation initiale, puisque les fonctions de ce fichier peuvent effectuer d'autres importations lorsque vous les appelez.

0 votes

@user2357112 En effet PEP 8 indique que les importations implicites relatives sont désactivées dans Python 3. Mais je me demande : si l'exemple ci-dessus n'est pas une importation implicite relative, alors qu'est-ce qui l'est ? Avez-vous un exemple ?

25voto

La solution la plus simple que j'ai pu trouver est de modifier temporairement sys.path dans la fonction qui effectue l'importation :

from contextlib import contextmanager

@contextmanager
def add_to_path(p):
    import sys
    old_path = sys.path
    sys.path = sys.path[:]
    sys.path.insert(0, p)
    try:
        yield
    finally:
        sys.path = old_path

def path_import(absolute_path):
   '''implementation taken from https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly'''
   with add_to_path(os.path.dirname(absolute_path)):
       spec = importlib.util.spec_from_file_location(absolute_path, absolute_path)
       module = importlib.util.module_from_spec(spec)
       spec.loader.exec_module(module)
       return module

Cela ne devrait pas poser de problèmes, sauf si vous effectuez des importations dans un autre fil de discussion en même temps. Sinon, puisque sys.path est restauré à son état précédent, il ne devrait pas y avoir d'effets secondaires indésirables.

Edit :

Je me rends compte que ma réponse est quelque peu insatisfaisante mais, en creusant dans le code, on constate que la ligne spec.loader.exec_module(module) se traduit essentiellement par exec(spec.loader.get_code(module.__name__),module.__dict__) être appelé. Voici spec.loader.get_code(module.__name__) est simplement le code contenu dans lib.py.

Ainsi, une meilleure réponse à la question serait de trouver un moyen de rendre la import se comportent différemment en injectant simplement une ou plusieurs variables globales via le second argument de l'instruction exec. Cependant, "quoi que vous fassiez pour que la machinerie d'importation cherche dans le dossier de ce fichier, elle devra s'attarder au-delà de la durée de l'importation initiale, puisque les fonctions de ce fichier peuvent effectuer d'autres importations lorsque vous les appelez", comme l'a indiqué @user2357112 dans les commentaires de la question.

Malheureusement, le seul moyen de modifier le comportement de la fonction import semble être de changer sys.path ou dans un paquet __path__ . module.__dict__ contient déjà __path__ donc cela ne semble pas fonctionner ce qui laisse sys.path (Ou essayer de comprendre pourquoi exec ne traite pas le code comme un paquet même s'il a __path__ et __package__ ... - Mais je ne sais pas par où commencer - Peut-être que ça a quelque chose à voir avec le fait de n'avoir aucun... __init__.py ).

En outre, cette question ne semble pas être spécifique à l'UE. importlib mais plutôt un problème général avec importations de produits frères et sœurs .

Edit2 : Si vous ne voulez pas que le module se retrouve dans sys.modules ce qui suit devrait fonctionner (Notez que tout module ajouté à sys.modules pendant l'importation sont supprimé ) :

from contextlib import contextmanager

@contextmanager
def add_to_path(p):
    import sys
    old_path = sys.path
    old_modules = sys.modules
    sys.modules = old_modules.copy()
    sys.path = sys.path[:]
    sys.path.insert(0, p)
    try:
        yield
    finally:
        sys.path = old_path
        sys.modules = old_modules

0 votes

Je pense que sys.modules est également affecté lors de l'exécution de cette opération... mais je ne sais pas comment éviter cet effet secondaire...

0 votes

Cela semble être vrai - Je ne vois cependant pas pourquoi cela serait indésirable/problématique ? - Le module est chargé après tout.

0 votes

Ils pourraient peut-être remplacer un module dans l'espace de noms actuel en fournissant un module avec le même nom

6voto

ShmulikA Points 1537

ajouter à la PYTHONPATH variable d'environnement le chemin sur lequel se trouve votre application

Augmente le chemin de recherche par défaut pour les fichiers de modules. Le format est le même que le PATH de l'interpréteur de commandes : un ou plusieurs chemins de répertoires séparés par os.pathsep (par exemple, des deux points sous Unix ou des points-virgules sous Windows). Windows). Les répertoires inexistants sont silencieusement ignorés.

Sur Bash, c'est comme ça :

export PYTHONPATH="./folder/:${PYTHONPATH}"

ou exécuter directement :

PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py

0 votes

J'ai mis à jour ma réponse avec des exigences plus précises pour une solution. Malheureusement, comme je veux une solution qui soit purement python, cela ne fonctionnera pas.

0 votes

Puis-je vous demander quelle est l'exigence initiale ? Et en particulier, quel aspect fait que vous ne pouvez le résoudre qu'avec une solution purement python.

1voto

Amaury Larancuent Points 303
  1. Assurez-vous que votre Racine se trouve dans un dossier qui est explicitement recherché dans le PYTHONPATH
  2. Utilisez une importation absolue :

    from Root.folder import implicit_sibling_import #called from app.py

0 votes

Comme indiqué dans la question, je cherche une solution reproductible (sans avoir à reconfigurer à chaque fois) et purement python, donc cela ne fonctionnera pas.

0 votes

@Pedro La répétition de cette configuration n'est-elle pas préférable à la solution que vous proposez (qui semble clairement très peu pythique) ?

1voto

Gang Points 311

L'idée de l'OP est géniale, cela ne fonctionne que pour cet exemple en ajoutant des modules frères avec le nom approprié au sys.modules, je dirais que c'est la MÊME chose que d'ajouter PYTHONPATH. testé et fonctionnant avec la version 3.5.1.

import os
import sys
import importlib.util

class PathImport(object):

    def get_module_name(self, absolute_path):
        module_name = os.path.basename(absolute_path)
        module_name = module_name.replace('.py', '')
        return module_name

    def add_sibling_modules(self, sibling_dirname):
        for current, subdir, files in os.walk(sibling_dirname):
            for file_py in files:
                if not file_py.endswith('.py'):
                    continue
                if file_py == '__init__.py':
                    continue
                python_file = os.path.join(current, file_py)
                (module, spec) = self.path_import(python_file)
                sys.modules[spec.name] = module

    def path_import(self, absolute_path):
        module_name = self.get_module_name(absolute_path)
        spec = importlib.util.spec_from_file_location(module_name, absolute_path)
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        return (module, spec)

def main():
    pathImport = PathImport()
    root = os.path.abspath(os.path.dirname(os.path.dirname(os.path.realpath(__file__))))
    isi_path = os.path.join(root, 'folder', 'implicit_sibling_import.py')
    sibling_dirname = os.path.dirname(isi_path)
    pathImport.add_sibling_modules(sibling_dirname)
    (lib, spec) = pathImport.path_import(isi_path)
    print (lib.hello())

if __name__ == '__main__':
    main()

1voto

Essayez :

export PYTHONPATH="./folder/:${PYTHONPATH}"

ou exécuter directement :

PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py

Assurez-vous que votre racine se trouve dans un dossier qui fait l'objet d'une recherche explicite dans la base de données de l'UE. PYTHONPATH . Utilisez une importation absolue :

from root.folder import implicit_sibling_import #called from app.py

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X