Módulo 2 · Lección 21 de 28
Comandos instalables
Cómo convertir tu proyecto en un comando que se escribe por su nombre, con el pyproject.toml explicado campo a campo.
Tu herramienta funciona, pero para usarla tienes que escribir esto:
python3 /Users/erwin/proyectos/misgastos/src/misgastos/cli.py anadir "Café" 1.60Y eso suponiendo que te acuerdes de la ruta, que estés en el entorno virtual correcto y que la importación de misgastos.modelo encuentre el paquete, cosa que desde ahí no va a pasar. Lo que quieres escribir es esto:
misgastos anadir "Café" 1.60Desde cualquier carpeta, sin rutas y sin pensar. Eso se consigue con un fichero de configuración de quince líneas y una orden de instalación.
Qué es el pyproject.toml#
Es el fichero que describe tu proyecto para las herramientas de empaquetado. Va en la raíz, al lado de src/, y en un proyecto moderno es el único fichero de configuración que hace falta.
Está escrito en TOML, un formato pensado para configuración que ya te resultará familiar: parejas de clave = valor agrupadas en tablas, que son esas cabeceras entre corchetes. Los textos van entre comillas, los números sin ellas y las listas entre corchetes.
Este es el fichero entero de misgastos:
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "misgastos"
version = "0.1.0"
description = "Apunta tus gastos desde la terminal y consulta el total por categoría."
readme = "README.md"
requires-python = ">=3.11"
license = "MIT"
authors = [{ name = "Erwin", email = "erwin@example.com" }]
keywords = ["gastos", "terminal", "cli"]
dependencies = []
[project.scripts]
misgastos = "misgastos.cli:main"Quince líneas útiles. Vamos una por una.
La tabla [build-system]#
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"Esta tabla responde a una sola pregunta: quién va a construir el paquete. Empaquetar no lo hace pip; pip solo descarga e instala. La construcción la hace una herramienta aparte llamada backend, y aquí le dices a pip cuál quieres y qué necesita.
requireses la lista de lo que hay que instalar para construir, no para usar tu programa.pipcrea un entorno temporal aparte, instala ahí esas herramientas, construye el paquete y tira el entorno. Nada de esto acaba en el ordenador de quien te instale.build-backendes el módulo Python que hace el trabajo.
setuptools es el backend más antiguo y el que verás en más proyectos. Hay otros, como hatchling o flit, con configuraciones algo más cortas. Para lo que vas a hacer, cualquiera vale, y estas tres líneas se copian tal cual de proyecto en proyecto.
La tabla [project]#
Aquí van los datos de tu proyecto: los que se enseñan en el índice de paquetes, los que aparecen al hacer pip show y los que determinan si se puede instalar o no.
name#
name = "misgastos"El nombre con el que se instala y con el que se desinstala. Tiene que ser único en el índice de paquetes si algún día lo publicas, así que conviene buscarlo antes en pypi.org. Solo admite letras, números, guiones, guiones bajos y puntos, y no distingue mayúsculas de minúsculas.
No tiene por qué coincidir con el nombre del paquete Python, aunque lo normal es que coincida y así se evitan confusiones. Un ejemplo conocido de lo contrario: el paquete que se instala con pip install pillow se importa con import PIL.
version#
version = "0.1.0"La versión de esta entrega. Es obligatoria y, como ves, está repetida: también está en __init__.py. Son dos sitios distintos porque los lee gente distinta (el fichero lo lee Python, la tabla la leen las herramientas de empaquetado) y hay que acordarse de tocar los dos. En la lección siguiente verás qué significa cada número.
description#
description = "Apunta tus gastos desde la terminal y consulta el total por categoría."Una sola frase. Es lo que sale al lado del nombre en las listas de resultados y en pip show. Sin punto final o con él, da igual, pero que quepa en una línea.
readme#
readme = "README.md"El fichero que se usará como descripción larga. El índice de paquetes lo renderiza tal cual en la página del proyecto, así que ese README.md acaba siendo la portada pública de tu herramienta.
requires-python#
requires-python = ">=3.11"La versión mínima de Python con la que funciona. No es decorativo: si alguien con Python 3.9 intenta instalarlo, pip se lo impedirá con un mensaje claro en lugar de instalarlo y dejar que reviente en mitad de la ejecución.
Pon aquí la versión más antigua que hayas probado de verdad. misgastos usa list[Gasto] y str | None en las anotaciones, que necesitan 3.9 y 3.10 respectivamente, así que declarar 3.11 va sobrado y es la versión razonable hoy.
license#
license = "MIT"El identificador de la licencia. La forma moderna es esta cadena corta con un identificador estándar (MIT, Apache-2.0, GPL-3.0-or-later), y el fichero LICENSE con el texto completo va en la raíz del proyecto. Las herramientas lo copian solo dentro del paquete construido.
authors#
authors = [{ name = "Erwin", email = "erwin@example.com" }]Una lista de tablas en línea, con llaves. Es la sintaxis TOML para meter un diccionario dentro de una lista, y admite varias personas separadas por comas. Tanto name como email son opcionales por separado, pero alguno tiene que haber.
keywords#
keywords = ["gastos", "terminal", "cli"]Palabras para que te encuentren en el buscador del índice de paquetes. No sirven para nada más y se pueden omitir.
dependencies#
dependencies = []La lista de bibliotecas que tu programa necesita para funcionar, no para construirse. Cuando alguien instale tu herramienta, pip instalará también todo lo que haya aquí.
misgastos no usa nada fuera de la biblioteca estándar, así que la lista está vacía. En cuanto añadas rich, en la lección 24, pasará a ser así:
dependencies = [
"rich>=13.0",
]Ese >=13.0 es una restricción de versión: pides al menos la 13.0 porque es la primera que tiene lo que usas. Las formas habituales:
| Restricción | Significado |
|---|---|
rich | Cualquier versión |
rich>=13.0 | De la 13.0 en adelante |
rich>=13.0,<16 | De la 13.0 en adelante, pero antes de la 16 |
rich==13.7.1 | Exactamente esa |
La tabla [project.scripts]: el comando#
Y llegamos a la línea que hace toda la magia:
[project.scripts]
misgastos = "misgastos.cli:main"Se lee así: crea un comando llamado misgastos que ejecute la función main del módulo misgastos.cli.
La sintaxis es siempre la misma, paquete.modulo:funcion, con un punto separando el paquete del módulo y dos puntos separando el módulo de la función. Ese detalle de los dos puntos es la fuente del error más común de todo el fichero, y lo verás al final de la lección.
Esa cadena se llama punto de entrada. La función a la que apunta tiene dos requisitos:
- No recibe argumentos. El comando la llamará sin pasarle nada, así que si necesita datos, los saca de
sys.argv(o deja queargparselo haga). Por esomain(argv=None)funciona: tiene un parámetro, pero con valor por defecto, así que se puede llamar sin nada. - Lo que devuelva se usa como código de salida. Si devuelve
None, el código es 0.
Puedes declarar varios comandos, uno por línea, apuntando a funciones distintas:
[project.scripts]
misgastos = "misgastos.cli:main"
misgastos-importar = "misgastos.importar:main"Qué crea exactamente la instalación#
No hay nada mágico. Al instalar, se escribe un pequeño fichero ejecutable dentro de la carpeta bin del entorno virtual (Scripts en Windows). Puedes abrirlo y mirarlo:
cat .venv/bin/misgastos#!/Users/erwin/proyectos/misgastos/.venv/bin/python3.14
import sys
from misgastos.cli import main
if __name__ == '__main__':
sys.argv[0] = sys.argv[0].removesuffix('.exe')
sys.exit(main())Es justo lo que harías a mano: un script que importa tu función y la ejecuta, con la ruta del intérprete del entorno en la primera línea para que se lance con el Python correcto. Y como la carpeta bin del entorno está en el PATH mientras el entorno esté activado, la terminal lo encuentra escribiendo solo su nombre.
Instalar en modo editable#
Con el pyproject.toml escrito, sitúate en la raíz del proyecto, con el entorno virtual activado, y ejecuta:
pip install -e .Ese punto final es la carpeta actual: le estás diciendo a pip que instale el proyecto que hay aquí. La -e es lo interesante, y significa editable.
Obtaining file:///Users/erwin/proyectos/misgastos
Installing build dependencies: started
Installing build dependencies: finished with status 'done'
Checking if build backend supports build_editable: started
Checking if build backend supports build_editable: finished with status 'done'
Getting requirements to build editable: started
Getting requirements to build editable: finished with status 'done'
Preparing editable metadata (pyproject.toml): started
Preparing editable metadata (pyproject.toml): finished with status 'done'
Building wheels for collected packages: misgastos
Building editable for misgastos (pyproject.toml): started
Building editable for misgastos (pyproject.toml): finished with status 'done'
Created wheel for misgastos: filename=misgastos-0.1.0-0.editable-py3-none-any.whl size=2078
Successfully built misgastos
Installing collected packages: misgastos
Successfully installed misgastos-0.1.0Y ya está. El comando existe:
which misgastos/Users/erwin/proyectos/misgastos/.venv/bin/misgastosmisgastos --versionmisgastos 0.1.0misgastos --helpusage: misgastos [-h] [--version] [-f FICHERO] {anadir,listar,resumen} ...
Apunta tus gastos y consulta cuánto llevas gastado.
positional arguments:
{anadir,listar,resumen}
anadir apunta un gasto nuevo
listar muestra los gastos apuntados
resumen muestra el total por categoría
options:
-h, --help show this help message and exit
--version show program's version number and exit
-f, --fichero FICHERO
fichero JSON donde se guardan los gastosY funciona desde cualquier carpeta:
cd ~/Documentos
misgastos anadir "Café con leche" 1.60 -c comida
misgastos anadir "Billete de metro" 2.40 -c transporte
misgastos anadir "Compra semanal" 54.20 -c comida
misgastos anadir "Entradas cine" 18.00 -c ocio
misgastos listarApuntado: Café con leche (1.60 €, comida)
Apuntado: Billete de metro (2.40 €, transporte)
Apuntado: Compra semanal (54.20 €, comida)
Apuntado: Entradas cine (18.00 €, ocio)
2026-09-19 Café con leche 1.60 € comida
2026-09-19 Billete de metro 2.40 € transporte
2026-09-19 Compra semanal 54.20 € comida
2026-09-19 Entradas cine 18.00 € ocio
TOTAL 76.20 €Qué significa "editable"#
Una instalación normal copia tu código a la carpeta de paquetes del entorno. A partir de ahí hay dos copias del programa: la que editas y la que se ejecuta. Cada vez que cambiaras una línea tendrías que reinstalar para verla.
Una instalación editable no copia nada. Deja en el entorno un apuntador a la carpeta de tu proyecto:
ls .venv/lib/python3.14/site-packages/__editable__.misgastos-0.1.0.pth
misgastos-0.1.0.dist-infoEse fichero .pth es el apuntador. Cuando Python arranca, lo lee y añade tu src/ a la lista de sitios donde buscar módulos. Solo hay una copia del código: la tuya.
Qué cambia y qué no al editar#
Esta distinción evita mucha confusión:
| Qué tocas | ¿Hay que reinstalar? |
|---|---|
El cuerpo de una función en cli.py | No |
| Un módulo nuevo dentro del paquete | No |
dependencies en el pyproject.toml | Sí |
version en el pyproject.toml | Sí |
[project.scripts] en el pyproject.toml | Sí |
La regla es sencilla: el código se lee en cada ejecución, el pyproject.toml solo se leyó al instalar.
Compruébalo. Cambia un mensaje en cli.py:
if not gastos:
print("Todavía no has apuntado nada.")
return 0Sin reinstalar nada, el comando ya lo dice:
misgastos -f vacio.json listarTodavía no has apuntado nada.Cambia ahora la versión en el pyproject.toml a 0.3.0 y pregunta:
pip show misgastosName: misgastos
Version: 0.1.0Sigue diciendo 0.1.0. Esa información se guardó en el dist-info durante la instalación y ahí se queda hasta que vuelvas a ejecutar pip install -e ..
Ejecutar sin instalar con python -m#
El __main__.py de la lección anterior sigue ahí, y con el paquete instalado también funciona:
python -m misgastos resumencomida 55.80 €
ocio 18.00 €
transporte 2.40 €Las dos formas hacen exactamente lo mismo. La diferencia práctica está en cuál de los dos Python se usa: misgastos a secas usa el intérprete que quedó escrito en la primera línea del ejecutable, mientras que python -m misgastos usa el python que tengas activo ahora mismo. Cuando algo se comporta de forma rara con varios entornos por medio, comparar las dos es una buena manera de averiguar qué está pasando.
Los errores típicos del pyproject.toml#
Cuatro fallos que vas a cometer, con su mensaje real.