como - Python 3.5+: ¿Cómo importar dinámicamente un módulo dada la ruta completa del archivo(en presencia de importaciones de hermanos implícitas)?
importar clases en python (5)
Pregunta
La biblioteca estándar documenta claramente cómo importar archivos de origen directamente (dada la ruta de archivo absoluta al archivo de origen), pero este enfoque no funciona si ese archivo de origen utiliza importaciones implícitas de hermanos como se describe en el siguiente ejemplo.
¿Cómo podría adaptarse ese ejemplo para trabajar en presencia de importaciones implícitas de hermanos?
Ya revisé this y estas otras preguntas de Stackoverflow sobre el tema, pero no tratan las importaciones implícitas de hermanos dentro del archivo que se importa manualmente.
Configuración / Ejemplo
Aquí hay un ejemplo ilustrativo
Estructura de directorios:
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())
Ejecutar la python folder/implicit_sibling_import.py
if __name__ == ''__main__'':
bloque if __name__ == ''__main__'':
comentario comentado ISI says: world
en Python 3.6.
Pero ejecutando python directory/app.py
produce:
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''
Solución
Si agrego import sys; sys.path.insert(0, os.path.dirname(isi_path))
import sys; sys.path.insert(0, os.path.dirname(isi_path))
a app.py
, python app.py
produce el world
como se python app.py
, pero me gustaría evitar interrumpir el sys.path
si es posible.
Requisitos de respuesta
Me gustaría que python app.py
imprima ISI says: world
y me gustaría lograr esto modificando la función path_import
.
No estoy seguro de las implicaciones de sys.path
. P.ej. si hubiera un directory/requests.py
y agregué la ruta al directory
sys.path
, no quisiera que las import requests
de importación comenzaran a importar el directory/requests.py
lugar de importar la biblioteca de solicitudes que instalé con pip install requests
.
La solución DEBE implementarse como una función de python que acepta la ruta de archivo absoluta al módulo deseado y devuelve el objeto del módulo .
Idealmente, la solución no debe introducir efectos secundarios (por ejemplo, si modifica sys.path
, debería devolver sys.path
a su estado original). Si la solución introduce efectos secundarios, debe explicar por qué no se puede lograr una solución sin introducir efectos secundarios.
PYTHONPATH
Si tengo varios proyectos haciendo esto, no quiero tener que acordarme de establecer PYTHONPATH
cada vez que cambio entre ellos. El usuario debería poder pip install
mi proyecto y ejecutarlo sin ninguna configuración adicional.
-m
El indicador -m
es el enfoque recomendado / pythonic, pero la biblioteca estándar también documenta claramente cómo importar archivos de origen directamente . Me gustaría saber cómo puedo adaptar ese enfoque para hacer frente a las importaciones relativas implícitas. Claramente, las partes internas de Python deben hacer esto, entonces, ¿en qué se diferencian las partes internas de la documentación de "importar archivos de origen directamente"?
- Asegúrese de que su raíz esté en una carpeta que se busque explícitamente en PYTHONPATH
Utilice una importación absoluta:
desde root.folder import implit_sibling_import #called desde app.py
La idea del OP es excelente, este trabajo solo para este ejemplo al agregar módulos hermanos con nombre propio a los módulos sys., diría que es el MISMO que se agrega a PYTHONPATH. Probado y trabajando con la versión 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()
La solución más fácil que se me ocurre es modificar temporalmente sys.path
en la función que realiza la importación:
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
Esto no debería causar ningún problema a menos que se importen en otro subproceso al mismo tiempo. De lo contrario, dado que sys.path
se restauró a su estado anterior, no debería haber efectos secundarios no deseados.
Editar:
Me doy cuenta de que mi respuesta es un tanto insatisfactoria, pero al spec.loader.exec_module(module)
en el código se revela que la línea spec.loader.exec_module(module)
básicamente resulta en exec(spec.loader.get_code(module.__name__),module.__dict__)
que se llama. Aquí spec.loader.get_code(module.__name__)
es simplemente el código contenido en lib.py.
Por lo tanto, una mejor respuesta a la pregunta tendría que encontrar una manera de hacer que la declaración de import
comporte de manera diferente simplemente inyectando una o más variables globales a través del segundo argumento de la declaración exec. Sin embargo, "haga lo que haga para que la maquinaria de importación se vea en la carpeta de ese archivo, tendrá que demorarse más allá de la duración de la importación inicial, ya que las funciones de ese archivo pueden realizar más importaciones cuando las llame", como lo indica @ User2357112 en la pregunta comentarios.
Desafortunadamente, la única forma de cambiar el comportamiento de la declaración de import
parece ser cambiar sys.path
o en un paquete __path__
. module.__dict__
ya contiene __path__
por lo que parece que no funciona, lo que deja a sys.path
(o intentar averiguar por qué exec no trata el código como un paquete a pesar de que tiene __path__
y __package__
... - Pero no lo hago '' No sé por dónde empezar. Tal vez tenga algo que ver con no tener un archivo __init__.py
).
Además, este problema no parece ser específico de importlib
sino más bien un problema general con las importaciones de hermanos .
Edit2: si no desea que el módulo termine en sys.modules
lo siguiente debería funcionar (tenga en cuenta que cualquier módulo agregado a sys.modules
durante la importación se eliminará ):
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
Tratar:
export PYTHONPATH="./folder/:${PYTHONPATH}"
o correr directamente:
PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py
Asegúrese de que su raíz esté en una carpeta que se busque explícitamente en PYTHONPATH
. Utilice una importación absoluta:
from root.folder import implicit_sibling_import #called from app.py
agregue a la variable de entorno PYTHONPATH
la ruta en la que se encuentra su aplicación
Aumente la ruta de búsqueda predeterminada para los archivos de módulo. El formato es el mismo que el PATH de la shell: una o más rutas de directorio separadas por os.pathsep (por ejemplo, dos puntos en Unix o puntos y coma en Windows). Los directorios no existentes son ignorados silenciosamente.
en bash es como esto:
export PYTHONPATH="./folder/:${PYTHONPATH}"
o correr directamente:
PYTHONPATH="./folder/:${PYTHONPATH}" python directory/app.py