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.
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'
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 .
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"?
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
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
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()
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
Utilisez une importation absolue:
à partir de root.folder import implicit_sibling_import #called from app.py