Python 3.5+: comment importer dynamiquement un module avec le chemin complet du fichier (en présence d'importations implicites entre frères et sœurs)?

Question

de La bibliothèque standard clairement documents comment importer des fichiers source directement (étant donné le chemin absolu vers le fichier le fichier source), mais cette approche ne fonctionne pas si le fichier source utilise implicite de la fratrie des importations comme décrit dans l'exemple ci-dessous.

comment cet exemple pourrait-il être adapté pour fonctionner en présence d'importations jumelles implicites?

j'ai déjà vérifié ce et cet autre Stackoverflow questions sur le sujet, mais ils ne traitent pas de l'implicite de la fratrie des importations à l'intérieur le fichier à importer à la main.

Setup/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 la if __name__ == '__main__': bloc commenté les rendements ISI says: world en Python 3.6.

mais en cours d'exécution python directory/app.py yields:

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 contournement

si j'ajoute import sys; sys.path.insert(0, os.path.dirname(isi_path)) à app.py , python app.py rendements world comme prévu, mais je voudrais éviter munging la sys.path si possible.

Répondre à des exigences de

j'aimerais python app.py pour imprimer ISI says: world et j'aimerais accomplir ceci 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 à directory sys.path , Je ne voudrais pas que import requests commence à importer directory/requests.py au lieu d'importer la requests library que j'ai installé avec pip install requests .

La solution DOIT être mis en œuvre comme une fonction python qui accepte le chemin absolu vers le fichier module désiré et renvoie le "1519890920 module" objet .

idéalement, la solution ne devrait pas introduire les effets secondaires (par exemple. si elle ne modifie sys.path , il devrait revenir 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 des effets secondaires.


PYTHONPATH

si j'ai plusieurs projets qui font cela, je ne veux pas avoir à me rappeler de mettre PYTHONPATH chaque fois que je change entre eux. L'utilisateur devrait juste pouvoir pip install mon projet et l'exécuter sans aucune configuration supplémentaire.

-m

la -m drapeau est l'approche recommandée/pythonique, mais la bibliothèque standard documente aussi clairement comment importer des fichiers source directement . J'aimerais savoir comment je peux adapter cette approche pour faire face aux importations relatives implicites. De toute évidence, les internes de Python doivent faire cela, les fichiers internes diffèrent de la documentation" importer directement les fichiers source"?

28
demandé sur Community 2017-01-25 23:50:24

5 réponses

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 causer de problèmes à moins que vous ne faites des importations dans un autre fil simultanément. Sinon, puisque sys.path est restauré à son état antérieur, il ne devrait pas y avoir d'effets secondaires indésirables.

Edit:

je me rends compte que ma réponse est quelque peu insatisfaisant, mais, creuser dans le code révèle que, la ligne spec.loader.exec_module(module) se traduit essentiellement par exec(spec.loader.get_code(module.__name__),module.__dict__) obtenir appelé. 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 la déclaration import se comportent différemment en injectant simplement une ou plusieurs variables globales par le second argument de la déclaration exec -. Cependant, " quoi que vous fassiez pour faire l'importation machinery look dans le dossier de ce fichier, il devra s'attarder au-delà de la durée de l'importation initiale, puisque les fonctions à partir 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 changer le comportement de la déclaration import semble être de changer sys.path ou dans un paquet __path__ . module.__dict__ contient déjà __path__ de sorte que cela ne semble pas fonctionner qui laisse sys.path (ou en essayant de comprendre pourquoi exec ne traite pas le code comme un paquet alors qu'il a __path__ et __package__ ... - Mais je ne sais pas par où commencer-peut-être que cela a quelque chose à voir avec le fait qu'il n'y a pas de fichier __init__.py ).

de plus, cette question ne semble pas être propre à importlib , mais plutôt un problème général avec importations entre frères et sœurs .

Edit 2: If vous ne voulez pas que le module finisse dans sys.modules ce qui suit devrait fonctionner (notez que tous les modules ajoutés à 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
11
répondu Jonathan von Schroeder 2017-05-23 12:34:41

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

augmente le chemin de recherche par défaut pour les fichiers modules. Le format est le même que le chemin du shell: un ou plusieurs noms de répertoire séparé par os.pathsep (par exemple, les colonnes sur Unix ou les points-virgule sur Windows.) Inexistante répertoires sont ignorées silencieusement.

sur le coup sa comme ceci:

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

ou exécuter directement:

PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py
6
répondu ShmulikA 2017-01-25 21:13:31
  1. assurez-vous que votre racine est dans un dossier qui est explicitement recherché dans le PYTHONPATH
  2. utiliser une importation absolue:

    de root.l'importation de dossiers implique l'import_import_appelé depuis app.py

1
répondu Amaury Larancuent 2017-01-25 21:49:24

l'idée de L'OP est grande, ce travail seulement pour cet exemple en ajoutant des modules frères avec le nom propre au sys.modules, je dirais que c'est la même chose que d'ajouter PYTHONPATH. testé et compatible 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
répondu Gang 2017-02-03 06:36:20

, Essayez:

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

ou exécuter directement:

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

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

from root.folder import implicit_sibling_import #called from app.py
1
répondu Md Jahangir Alam 2017-02-03 15:08:11