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"?
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
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
- assurez-vous que votre racine est dans un dossier qui est explicitement recherché dans le PYTHONPATH
-
utiliser une importation absolue:
de root.l'importation de dossiers implique l'import_import_appelé depuis app.py
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()
, 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