Importaciones de paquetes hermanos
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 examples
y desde el módulo y ejecutarlos desde la línea de comandos?tests
api
Además, me gustaría evitar el feo sys.path.insert
truco para cada archivo. Seguramente esto se puede hacer en Python, ¿verdad?
¿Estás cansado de los hacks de sys.path?
Hay muchos sys.path.append
trucos 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.toml
un archivo para describir su paquete (consulte el mínimopyproject.toml
a continuación) - Pip instala el paquete en estado editable con
pip install -e <myproject_folder>
- Importar usando
from 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_api
resultarí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.toml
serí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 myproject
usando pip
. El truco consiste en utilizar la -e
bandera 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 install
funcionarán todavía funcionan bien. Vea un ejemplo a continuación.
Prueba la solución
Ahora, probemos la solución usando api.py
lo definido arriba y test_one.py
lo 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.
Siete años después
Desde que escribí la respuesta a continuación, la modificación sys.path
sigue 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.cfg
para almacenar los metadatos). - Usar la
-m
bandera 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.path
hacks 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 -e
es 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 api
carpeta principal de su ejemplo).