web-dev-qa-db-fra.com

Python 3.5+: comment importer dynamiquement un module étant donné le chemin d'accès complet au fichier (en présence d'importations implicites de frères et sœurs)?

Question

La bibliothèque standard documente clairement comment importer directement les fichiers source (étant donné le chemin absolu du fichier vers le fichier source), mais cette approche ne fonctionne pas si ce fichier source utilise des importations de frères implicites 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é cet et cet autre questions Stackoverflow sur le sujet, mais elles ne traitent pas des importations implicites de frères et sœurs dedans le fichier importé par 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())

L'exécution de python folder/implicit_sibling_import.py Avec le bloc if __name__ == '__main__': A commenté les rendements ISI says: world Dans Python 3.6.

Mais exécuter python directory/app.py Donne:

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'

Workaround

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 je voudrais éviter de figer le sys.path Si possible .

Exigences de réponse

J'aimerais que python app.py Imprime ISI says: world Et j'aimerais accomplir cela en modifiant la fonction path_import.

Je ne suis pas sûr des implications de la mutilation sys.path. Par exemple. s'il y avait directory/requests.py et j'ai ajouté le chemin de directory au sys.path, je ne voudrais pas que import requests commence à importer directory/requests.py au lieu d'importer le bibliothèque de requêtes que j'ai installé avec pip install requests.

La solution [~ # ~] doit [~ # ~] être implémentée comme une fonction python qui accepte l'absolu chemin du fichier vers le module souhaité et renvoie objet module .

Idéalement, la solution ne devrait pas introduire d'effets secondaires (par exemple, si elle modifie sys.path, Elle devrait ramener sys.path À son état d'origine). Si la solution présente des effets secondaires, elle doit expliquer pourquoi une solution ne peut être obtenue sans introduire d'effets secondaires.


PYTHONPATH

Si j'ai plusieurs projets en train de faire cela, je ne veux pas avoir à me rappeler de définir PYTHONPATH chaque fois que je bascule entre eux. L'utilisateur doit simplement pouvoir pip install Mon projet et l'exécuter sans aucune configuration supplémentaire.

-m

-m Flag est l'approche recommandée/Pythonic, mais la bibliothèque standard documente aussi clairement Comment importer directement les fichiers source . J'aimerais savoir comment adapter cette approche pour faire face aux importations relatives implicites. De toute évidence, les composants internes de Python doivent le faire, alors en quoi les composants internes diffèrent-ils de la documentation "importer directement les fichiers source"?

29
Pedro Cattori

La solution la plus simple que j'ai pu trouver est de modifier temporairement sys.path Dans la fonction faisant 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ème sauf si vous importez simultanément dans un autre thread. Sinon, puisque sys.path Est restauré à son état précédent, il ne devrait pas y avoir d'effets secondaires indésirables.

Modifier:

Je me rends compte que ma réponse est quelque peu insatisfaisante mais, en fouillant dans le code, cela révèle que la ligne spec.loader.exec_module(module) a pour résultat que exec(spec.loader.get_code(module.__name__),module.__dict__) est appelée. Ici spec.loader.get_code(module.__name__) est simplement le code contenu dans lib.py.

Ainsi, une meilleure réponse à la question devrait trouver un moyen de faire en sorte que l'instruction import se comporte différemment en injectant simplement une ou plusieurs variables globales via le deuxième argument de l'instruction exec. Cependant, "quoi que vous fassiez pour que la machinerie d'importation apparaisse dans le dossier de ce fichier, elle devra rester au-delà de la durée de l'importation initiale, car les fonctions de ce fichier peuvent effectuer d'autres importations lorsque vous les appelez", comme indiqué par @ user2357112 dans les commentaires de la question.

Malheureusement, la seule façon de modifier le comportement de l'instruction import semble être de modifier sys.path Ou dans un package __path__. module.__dict__ Contient déjà __path__, Ce qui ne semble pas fonctionner, ce qui laisse sys.path (Ou essayer de comprendre pourquoi exec ne traite pas le code comme un package même s'il a __path__ Et __package__ ... - Mais je ne sais pas par où commencer - Peut-être que cela a quelque chose à voir avec l'absence de fichier __init__.py).

De plus, ce problème ne semble pas être spécifique à importlib mais plutôt un problème général avec importation frère .

Edit2: Si vous ne voulez pas que le module se retrouve dans sys.modules, Ce qui suit devrait fonctionner (notez que tous les modules ajoutés à sys.modules Lors de l'importation sont supprimés):

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
13

ajouter à la variable d'environnement PYTHONPATH le chemin d'accès de votre application

Augmentez le chemin de recherche par défaut pour les fichiers de module. Le format est le même que le PATH du shell: un ou plusieurs noms de répertoire séparés par os.pathsep (par exemple, deux-points sur Unix ou des points-virgules sur Windows). Les répertoires inexistants sont ignorés en silence.

sur bash c'est comme ça:

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

ou exécutez directement:

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

L'idée de l'OP est excellente, cela ne fonctionne que pour cet exemple en ajoutant des modules frères et sœurs avec le nom propre aux modules sys.modules, je dirais que c'est le même que l'ajout de PYTHONPATH. testé et fonctionne 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()
1
Gang

Essayer:

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

ou exécutez directement:

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

Assurez-vous que votre racine se trouve dans un dossier qui est explicitement recherché dans le PYTHONPATH. Utilisez une importation absolue:

from root.folder import implicit_sibling_import #called from app.py
1
Md Jahangir Alam
  1. Assurez-vous que votre racine se trouve dans un dossier qui est explicitement recherché dans le PYTHONPATH
  2. Utilisez une importation absolue:

    à partir de root.folder import implicit_sibling_import #called from app.py

1
Amaury Larancuent