Importaciones de paquetes hermanos

199

Intenté leer las preguntas sobre las importaciones entre hermanos e incluso la documentación del paquete , pero aún no he encontrado una respuesta.

Con la siguiente estructura:

├── LICENSE.md
├── README.md
├── api
   ├── __init__.py
   ├── api.py
   └── api_key.py
├── examples
   ├── __init__.py
   ├── example_one.py
   └── example_two.py
└── tests
   ├── __init__.py
   └── test_one.py

¿Cómo pueden los scripts en los directorios examplesy testsimportar desde el apimódulo y ejecutarse desde la línea de comandos?

Además, me gustaría evitar el sys.path.inserttruco feo para cada archivo. Seguramente esto se puede hacer en Python, ¿verdad?

zachwill
fuente
77
Recomiendo saltar todos los sys.pathhacks y leer la única solución real que se ha publicado hasta ahora (¡después de 7 años!).
Aran-Fey
1
Por cierto, todavía hay espacio para otra buena solución: separar el código ejecutable del código de la biblioteca; la mayoría de las veces, una secuencia de comandos dentro de un paquete no debería ser ejecutable para empezar.
Aran-Fey
Esto es muy útil, tanto la pregunta como las respuestas. Solo tengo curiosidad, ¿por qué "Respuesta aceptada" no es lo mismo que la recompensa en este caso?
Indominus
@ Aran-Fey Ese es un recordatorio subestimado en estas preguntas y respuestas relativas al error de importación. He estado buscando un truco todo este tiempo, pero en el fondo sabía que había una manera simple de diseñar mi salida del problema. No quiere decir que es la solución para todos los que están leyendo aquí, pero es un buen recordatorio, ya que podría ser para muchos.
colorlace

Respuestas:

69

Siete años después

Desde que escribí la respuesta a continuación, la modificación sys.pathsigue siendo un truco rápido y sucio que funciona bien para scripts privados, pero ha habido varias mejoras

  • Instalar el paquete (en un virtualenv o no) le dará lo que desea, aunque sugeriría usar pip para hacerlo en lugar de usar setuptools directamente (y usar setup.cfgpara almacenar los metadatos)
  • Usar el -mindicador y ejecutarse como un paquete también funciona (pero resultará un poco incómodo si desea convertir su directorio de trabajo en un paquete instalable).
  • Para las pruebas, específicamente, pytest puede encontrar el paquete api en esta situación y se encarga de los sys.pathhacks por usted.

Entonces, realmente depende de lo que quieras hacer. Sin embargo, en su caso, dado que parece que su objetivo es hacer un paquete adecuado en algún momento, la instalación pip -ees probablemente su mejor opción, incluso si aún no es perfecta.

Vieja respuesta

Como ya se dijo en otra parte, la terrible verdad es que tienes que hacer hacks feos para permitir importaciones de módulos hermanos o paquetes padres desde un __main__módulo. El problema se detalla en PEP 366 . PEP 3122 intentó manejar las importaciones de una manera más racional, pero Guido lo rechazó por cuenta de

El único caso de uso parece estar ejecutando scripts que viven dentro del directorio de un módulo, que siempre he visto como un antipatrón.

( aquí )

Sin embargo, uso este patrón de forma regular con

# Ugly hack to allow absolute import from the root folder
# whatever its name is. Please forgive the heresy.
if __name__ == "__main__" and __package__ is None:
    from sys import path
    from os.path import dirname as dir

    path.append(dir(path[0]))
    __package__ = "examples"

import api

Aquí path[0]está la carpeta principal de su script en ejecución y dir(path[0])su carpeta de nivel superior.

Sin embargo, todavía no he podido usar importaciones relativas con esto, pero sí permite importaciones absolutas desde el nivel superior (en la apicarpeta principal de su ejemplo ).

Evpok
fuente
3
no tiene que hacerlo si se ejecuta desde un directorio de proyecto utilizando el -mformulario o si instala el paquete (pip y virtualenv lo hacen fácil)
jfs
2
¿Cómo encuentra Pytest el paquete api para usted? Divertidamente, encontré este hilo porque me encuentro con este problema específicamente con la importación de paquetes pytest y hermanos.
JuniorIncanter
1
Tengo dos preguntas, por favor. 1. Tu patrón parece funcionar sin __package__ = "examples"mí. por que lo usas? 2. ¿En qué situación está __name__ == "__main__"pero __package__no está None?
actual_panda
@actual_panda La configuración __packages__ayuda si desea una ruta absoluta como examples.apitrabajar iirc (pero ha pasado mucho tiempo desde la última vez que lo hice) y verificar que el paquete no es Ninguno fue principalmente seguro para situaciones extrañas y futuras pruebas.
Evpok
165

¿Cansado de los sys.path hacks?

Hay muchos sys.path.appendtrucos disponibles, pero encontré una forma alternativa de resolver el problema.

Resumen

  • Envuelva el código en una carpeta (por ejemplo packaged_stuff)
  • Use crear setup.pyscript donde use setuptools.setup () .
  • Pip instala el paquete en estado editable con pip install -e <myproject_folder>
  • Importar usando from packaged_stuff.modulename import function_name

Preparar

El punto de partida es la estructura de archivos que ha proporcionado, envuelta en una carpeta llamada myproject.

.
└── myproject
    ├── api
       ├── api_key.py
       ├── api.py
       └── __init__.py
    ├── examples
       ├── example_one.py
       ├── example_two.py
       └── __init__.py
    ├── LICENCE.md
    ├── README.md
    └── tests
        ├── __init__.py
        └── test_one.py

Llamaré a la .carpeta raíz y, en mi caso de ejemplo, se encuentra en C:\tmp\test_imports\.

api.py

Como caso de prueba, usemos lo siguiente ./api/api.py

def function_from_api():
    return 'I am the return value from api.api!'

test_one.py

from api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

Intenta ejecutar test_one:

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\myproject\tests\test_one.py", line 1, in <module>
    from api.api import function_from_api
ModuleNotFoundError: No module named 'api'

También intentar las importaciones relativas no funcionará:

Usar from ..api.api import function_from_apiresultaría en

PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
Traceback (most recent call last):
  File ".\tests\test_one.py", line 1, in <module>
    from ..api.api import function_from_api
ValueError: attempted relative import beyond top-level package

Pasos

  1. Cree un archivo setup.py en el directorio del nivel raíz

El contenido para el setup.pysería *

from setuptools import setup, find_packages

setup(name='myproject', version='1.0', packages=find_packages())
  1. Usa un entorno virtual

Si está familiarizado con los entornos virtuales, active uno y salte al siguiente paso. El uso de entornos virtuales no es absolutamente obligatorio, pero realmente lo ayudarán a largo plazo (cuando tenga más de 1 proyecto en curso ...). Los pasos más básicos son (ejecutar en la carpeta raíz)

  • Crea env virtual
    • python -m venv venv
  • Activar env virtual
    • source ./venv/bin/activate(Linux, macOS) o ./venv/Scripts/activate(Win)

Para obtener más información sobre esto, solo busque en Google "tutorial de python virtual env" o similar. Probablemente nunca necesite más comandos que crear, activar y desactivar.

Una vez que haya creado y activado un entorno virtual, su consola debe indicar el nombre del entorno virtual entre paréntesis

PS C:\tmp\test_imports> python -m venv venv
PS C:\tmp\test_imports> .\venv\Scripts\activate
(venv) PS C:\tmp\test_imports>

y tu árbol de carpetas debería verse así **

.
├── myproject
   ├── api
      ├── api_key.py
      ├── api.py
      └── __init__.py
   ├── examples
      ├── example_one.py
      ├── example_two.py
      └── __init__.py
   ├── LICENCE.md
   ├── README.md
   └── tests
       ├── __init__.py
       └── test_one.py
├── setup.py
└── venv
    ├── Include
    ├── Lib
    ├── pyvenv.cfg
    └── Scripts [87 entries exceeds filelimit, not opening dir]
  1. pip instala tu proyecto en estado editable

Instale su paquete de nivel superior myprojectusando pip. El truco es usar la -ebandera al hacer la instalación. De esta forma, se instala en un estado editable, y todas las ediciones realizadas en los archivos .py se incluirán automáticamente en el paquete instalado.

En el directorio raíz, ejecute

pip install -e . (tenga en cuenta el punto, significa "directorio actual")

También puede ver que se instala utilizando pip freeze

(venv) PS C:\tmp\test_imports> pip install -e .
Obtaining file:///C:/tmp/test_imports
Installing collected packages: myproject
  Running setup.py develop for myproject
Successfully installed myproject
(venv) PS C:\tmp\test_imports> pip freeze
myproject==1.0
  1. Agregar myproject.a sus importaciones

Tenga en cuenta que tendrá que agregar myproject.solo a las importaciones que de otro modo no funcionarían. Las importaciones que funcionaron sin el setup.py& pip installfuncionarán aún funcionan bien. Vea un ejemplo a continuación.


Prueba la solución

Ahora, probemos la solución usando los api.pydefinidos anteriormente y los test_one.pydefinidos a continuación.

test_one.py

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

corriendo la prueba

(venv) PS C:\tmp\test_imports> python .\myproject\tests\test_one.py
I am the return value from api.api!

* Consulte los documentos de setuptools para obtener ejemplos más detallados de setup.py.

** En realidad, podría colocar su entorno virtual en cualquier parte de su disco duro.

np8
fuente
13
Gracias por la publicación detallada. Aquí está mi problema. Si hago todo lo que dijiste y hago una congelación de pip, me sale una línea ¿ -e git+https://[email protected]/folder/myproject.git@f65466656XXXXX#egg=myprojectAlguna idea sobre cómo resolver?
Si Mon
2
¿Por qué no funciona la solución de importación relativa? Te creo, pero estoy tratando de entender el sistema intrincado de Python.
Jared Nielsen
8
¿Alguien tiene problemas con respecto a un ModuleNotFoundError? ¿He instalado 'myproject' en un virtualenv siguiendo estos pasos, y cuando entro en una sesión interpretada y ejecuto import myprojectme sale ModuleNotFoundError: No module named 'myproject'? pip list installed | grep myprojectmuestra que está allí, el directorio es correcta, y tanto el verison de pipy pythonse verifica que es correcta.
ThoseKind
2
Hola @ np8, funciona, lo instalé accidentalmente en venv y en os :) pip listmuestra paquetes, mientras que pip freezemuestra nombres extraños si está instalado con la bandera -e
Grzegorz Krug
3
Pasé aproximadamente 2 horas tratando de descubrir cómo hacer que las importaciones relativas funcionen, y esta respuesta fue la que finalmente hizo algo sensato. 👍👍
Graham Lea
43

Aquí hay otra alternativa que inserto en la parte superior de los archivos de Python en la testscarpeta:

# Path hack.
import sys, os
sys.path.insert(0, os.path.abspath('..'))
Cenk Alti
fuente
1
+1 realmente simple y funcionó perfectamente. Necesita agregar la clase padre a la importación (ex api.api, examples.example_two) pero lo prefiero de esa manera.
Evan Plaice
10
Creo que vale la pena mencionar a los novatos (como yo) que ..aquí está relacionado con el directorio desde el que está ejecutando, no el directorio que contiene ese archivo de prueba / ejemplo. Estoy ejecutando desde el directorio del proyecto, y necesitaba en su ./lugar. Espero que esto ayude a alguien más.
Joshua Detwiler
@JoshDetwiler, sí, absolutamente. No estaba al tanto de eso. Gracias.
doak
1
Esta es una respuesta pobre. Hackear el camino no es una buena práctica; Es escandaloso cuánto se usa en el mundo de Python. Uno de los puntos principales de esta pregunta era ver cómo se podían hacer las importaciones evitando este tipo de pirateo.
jtcotton63
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))@JoshuaDetwiler
vldbnc
31

No necesita y no debe piratear a sys.pathmenos que sea necesario y en este caso no lo es. Utilizar:

import api.api_key # in tests, examples

Ejecutar en el directorio del proyecto: python -m tests.test_one.

Probablemente deberías moverte tests(si son las pruebas unitarias de la API) dentro apiy correr python -m api.testpara ejecutar todas las pruebas (suponiendo que las haya __main__.py) o python -m api.test.test_onecorrer en su test_onelugar.

También puede eliminar __init__.pyde examples(no es un paquete de Python) y ejecutar los ejemplos en un virtualenv donde apiestá instalado, por ejemplo, pip install -e .en un virtualenv instalaría el apipaquete in situ si tiene el adecuado setup.py.

jfs
fuente
@Alex la respuesta no asume que las pruebas son pruebas API, excepto el párrafo donde dice explícitamente "si son pruebas unitarias de la API" .
jfs
desafortunadamente, entonces está atascado con la ejecución desde el directorio raíz y PyCharm todavía no encuentra el archivo para sus buenas funciones
mhstnsc
@mhstnsc: no es correcto. Deberías poder correr python -m api.test.test_onedesde cualquier lugar cuando virtualenv esté activado. Si no puede configurar PyCharm para ejecutar sus pruebas, intente hacer una nueva pregunta de desbordamiento de pila (si no puede encontrar una pregunta existente sobre este tema).
jfs
@jfs Perdí la ruta de env virtual pero no quiero usar nada más que shebang line para ejecutar estas cosas desde cualquier directorio. No se trata de correr con PyCharm. Los desarrolladores con PyCharm sabrán también que tienen funciones completas y de salto que no podría hacer que funcione con ninguna solución.
mhstnsc
@mhstnsc un shebang apropiado es suficiente en muchos casos (apúntelo al binario virtualenv python. Cualquier IDE Python decente debería admitir un virtualenv.
jfs
9

Todavía no tengo la comprensión de Pythonology necesaria para ver la forma prevista de compartir código entre proyectos no relacionados sin un hack de hermanos / importación relativa. Hasta ese día, esta es mi solución. Para exampleso testspara importar cosas ..\api, se vería así:

import sys.path
import os.path
# Import from sibling directory ..\api
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
import api.api
import api.api_key
usuario1330131
fuente
Esto aún le daría el directorio padre de la API y no necesitaría la concatenación "/ .." sys.path.append (os.path.dirname (os.path.dirname (os.path.abspath ( file ))) )
Camilo Sanchez
4

Para las importaciones de paquetes de hermanos, puede usar el método insert o append del módulo [sys.path] [2] :

if __name__ == '__main__' and if __package__ is None:
    import sys
    from os import path
    sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
    import api

Esto funcionará si está iniciando sus scripts de la siguiente manera:

python examples/example_one.py
python tests/test_one.py

Por otro lado, también puede usar la importación relativa:

if __name__ == '__main__' and if __package__ is not None:
    import ..api.api

En este caso, deberá iniciar su script con el argumento '-m' (tenga en cuenta que, en este caso, no debe dar la extensión '.py' ):

python -m packageName.examples.example_one
python -m packageName.tests.test_one

Por supuesto, puede mezclar los dos enfoques, para que su script funcione sin importar cómo se llame:

if __name__ == '__main__':
    if __package__ is None:
        import sys
        from os import path
        sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
        import api
    else:
        import ..api.api
Paolo Rovelli
fuente
Estaba usando el marco Click que no tiene el __file__global, así que tuve que usar lo siguiente: sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0]))))Pero ahora funciona en cualquier directorio
GammaGames
3

TLDR

Este método no requiere herramientas de configuración, ataques de ruta, argumentos adicionales de la línea de comandos o especificar el nivel superior del paquete en cada archivo de su proyecto.

Simplemente haga un script en el directorio principal de lo que sea que esté llamando a ser suyo __main__y ejecute todo desde allí. Para más explicaciones continúe leyendo.

Explicación

Esto se puede lograr sin hackear una nueva ruta juntos, argumentos de línea de comando adicionales o agregar código a cada uno de sus programas para reconocer a sus hermanos.

La razón por la que esto falla, como creo que se mencionó antes, es porque los programas que se llaman tienen su __name__conjunto como __main__. Cuando esto ocurre, el script que se llama acepta que se encuentra en el nivel superior del paquete y se niega a reconocer los scripts en directorios hermanos.

Sin embargo, todo lo que esté debajo del nivel superior del directorio seguirá reconociendo CUALQUIER OTRA COSA debajo del nivel superior. Esto significa que lo ÚNICO que tiene que hacer para que los archivos en directorios hermanos se reconozcan / utilicen entre sí es llamarlos desde un script en su directorio principal.

Prueba de concepto En un directorio con la siguiente estructura:

.
|__Main.py
|
|__Siblings
   |
   |___sib1
   |   |
   |   |__call.py
   |
   |___sib2
       |
       |__callsib.py

Main.py contiene el siguiente código:

import sib1.call as call


def main():
    call.Call()


if __name__ == '__main__':
    main()

sib1 / call.py contiene:

import sib2.callsib as callsib


def Call():
    callsib.CallSib()


if __name__ == '__main__':
    Call()

y sib2 / callsib.py contiene:

def CallSib():
    print("Got Called")

if __name__ == '__main__':
    CallSib()

Si reproduce este ejemplo, notará que la llamada Main.pydará como resultado que se imprima "Got Called" tal como se define en sib2/callsib.py aunque sib2/callsib.pyse le haya llamado sib1/call.py. Sin embargo, si se llamara directamente sib1/call.py(después de hacer los cambios apropiados a las importaciones), arroja una excepción. Aunque funcionó cuando fue llamado por el script en su directorio padre, no funcionará si cree que se encuentra en el nivel superior del paquete.

Thunderwood
fuente
2

Hice un proyecto de muestra para demostrar cómo manejé esto, que de hecho es otro hack de sys.path como se indicó anteriormente. Ejemplo de importación de Python Sibling , que se basa en:

if __name__ == '__main__': import os import sys sys.path.append(os.getcwd())

Esto parece ser bastante efectivo siempre que su directorio de trabajo permanezca en la raíz del proyecto Python. Si alguien implementa esto en un entorno de producción real, sería bueno saber si funciona allí también.

ADataGMan
fuente
Esto solo funciona si está ejecutando desde el directorio padre del script
Evpok
1

Debe mirar para ver cómo se escriben las declaraciones de importación en el código relacionado. Si examples/example_one.pyusa la siguiente declaración de importación:

import api.api

... luego espera que el directorio raíz del proyecto esté en la ruta del sistema.

La forma más fácil de soportar esto sin ningún tipo de pirateo (como lo dice) sería ejecutar los ejemplos desde el directorio de nivel superior, como este:

PYTHONPATH=$PYTHONPATH:. python examples/example_one.py 
AJ
fuente
Con Python 2.7.1 consigo el siguiente: $ python examples/example.py Traceback (most recent call last): File "examples/example.py", line 3, in <module> from api.api import API ImportError: No module named api.api. También me pasa lo mismo import api.api.
zachwill
Actualizado mi respuesta ... usted no tiene que agregar el directorio actual a la ruta de importación, no hay manera de evitar eso.
AJ.
1

En caso de que alguien que usa Pydev en Eclipse termine aquí: puede agregar la ruta principal del hermano (y, por lo tanto, el padre del módulo de llamada) como una carpeta de biblioteca externa usando Proyecto-> Propiedades y configurando Bibliotecas externas en el menú izquierdo Pydev-PYTHONPATH . Luego puede importar desde su hermano, por ejemplo from sibling import some_class.

Lord Henry Wotton
fuente
-3

Primero, debe evitar tener archivos con el mismo nombre que el módulo en sí. Puede romper otras importaciones.

Cuando importa un archivo, primero el intérprete verifica el directorio actual y luego busca directorios globales.

Dentro exampleso testspuedes llamar:

from ..api import api

fuente
Me sale lo siguiente con Python 2.7.1:Traceback (most recent call last): File "example_one.py", line 3, in <module> from ..api import api ValueError: Attempted relative import in non-package
zachwill
2
Ah, entonces deberías agregar un __init__.pyarchivo al directorio de nivel superior. De lo contrario, Python no puede tratarlo como un módulo
8
No va a funcionar El problema no es que la carpeta principal no sea un paquete, es que dado que el módulo __name__es en __main__lugar de package.module, Python no puede ver su paquete principal, por lo .que no señala nada.
Evpok