modulos importar como clases archivo python python-3.x import python-importlib

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"?


  1. Asegúrese de que su raíz esté en una carpeta que se busque explícitamente en PYTHONPATH
  2. 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