Importaciones de paquetes hermanos

Resuelto zachwill asked hace 13 años • 12 respuestas

Intenté leer preguntas sobre importaciones entre hermanos e incluso la documentación del paquete , pero todavía tengo que encontrar 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 se pueden importar los scripts en los directorios examplesy desde el módulo y ejecutarlos desde la línea de comandos?testsapi

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

zachwill avatar Jun 13 '11 01:06 zachwill
Aceptado

¿Estás cansado de los hacks de sys.path?

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

Resumen

  • Envuelva el código en una carpeta (por ejemplo packaged_stuff)
  • Cree pyproject.tomlun archivo para describir su paquete (consulte el mínimo pyproject.tomla continuación)
  • Pip instala el paquete en estado editable conpip install -e <myproject_folder>
  • Importar usandofrom packaged_stuff.modulename import function_name

Configuración

El punto de partida es la estructura de archivos que proporcionó, 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 el siguiente ./api/api.py

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

prueba_uno.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'

Además, intentar importaciones relativas no funcionará:

Usarlo 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 pyproject.toml en el directorio de nivel raíz

(anteriormente la gente usaba un archivo setup.py)

El contenido para un mínimo pyproject.tomlsería*

[project]
name = "myproject"
version = "0.1.0"
description = "My small project"

[build-system]
build-backend = "flit_core.buildapi"
requires = ["flit_core >=3.2,<4"]

2) Utilice un entorno virtual

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

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

Para obtener más información sobre esto, simplemente busque en Google "tutorial de entorno virtual de Python" o similar. Probablemente nunca necesites más comandos que crear, activar y desactivar.

Una vez que haya creado y activado un entorno virtual, su consola debería 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 su á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
├── pyproject.toml
└── venv
    ├── Include
    ├── Lib
    ├── pyvenv.cfg
    └── Scripts [87 entries exceeds filelimit, not opening dir]

3) pip instala tu proyecto en estado editable

Instale su paquete de nivel superior myprojectusando pip. El truco consiste en utilizar la -ebandera al realizar la instalación. De esta manera, se instala en un estado editable y todas las ediciones realizadas en los archivos .py se incluirán automáticamente en el paquete instalado. El uso de pyproject.toml y -e flag requiere pip>= 21.3

En el directorio raíz, ejecute

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

También puedes ver que se instala usandopip freeze

Obtaining file:///home/user/projects/myproject
  Installing build dependencies ... done
  Checking if build backend supports build_editable ... done
  Getting requirements to build editable ... done
  Preparing editable metadata (pyproject.toml) ... done
Building wheels for collected packages: myproj
  Building editable for myproj (pyproject.toml) ... done
  Created wheel for myproj: filename=myproj-0.1.0-py2.py3-none-any.whl size=903 sha256=f19858b080d4e770c2a172b9a73afcad5f33f4c43c86e8eb9bdacbe50a627064
  Stored in directory: /tmp/pip-ephem-wheel-cache-qohzx1u0/wheels/55/5f/e4/507fdeb40cdef333e3e0a8c50c740a430b8ce84cbe17ae5875
Successfully built myproject
Installing collected packages: myproject
Successfully installed myproject-0.1.0
(venv) PS C:\tmp\test_imports> pip freeze
myproject==0.1.0

4) Agregue myproject.a sus importaciones

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


Prueba la solución

Ahora, probemos la solución usando api.pylo definido arriba y test_one.pylo definido a continuación.

prueba_uno.py

from myproject.api.api import function_from_api

def test_function():
    print(function_from_api())

if __name__ == '__main__':
    test_function()

ejecutando la prueba

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

* aquí usando flit como backend de compilación. Existen otras alternativas.

** En realidad, puedes colocar tu entorno virtual en cualquier lugar de tu disco duro.

Niko Fohr avatar May 05 '2018 20:05 Niko Fohr

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 entorno virtual o no) le dará lo que desea, aunque sugeriría usar pip para hacerlo en lugar de usar herramientas de configuración directamente (y usarlas setup.cfgpara almacenar los metadatos).
  • Usar la -mbandera y ejecutarlo 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 crear un paquete adecuado en algún momento, instalarlo pip -ees probablemente su mejor opción, incluso si aún no es perfecto.

Antigua respuesta

Como ya se indicó en otra parte, la terrible verdad es que hay que hacer trucos feos para permitir las importaciones desde módulos hermanos o paquetes principales desde un __main__módulo. La cuestión se detalla en PEP 366 . PEP 3122 intentó manejar las importaciones de una manera más racional pero Guido lo rechazó según el relato de

El único caso de uso parece ser ejecutar scripts que se encuentran dentro del directorio de un módulo, lo que siempre he visto como un antipatrón.

( aquí )

Sin embargo, uso este patrón regularmente 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 secuencia de comandos 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 avatar Jun 24 '2011 09:06 Evpok