La diferencia entre un script y un programa, es que es script es un único archivo python que se ejecuta desde su ubicación. Puede ser muy útil, especialmente para pequeñas tareas, pero un programa software es más profesional y tiene muchas más ventajas: ejecutarlo desde cualquier ubicación, manejar información estructurada (JSON, Bases de datos), usar módulos (diferentes archivos de biblioteca).
No es que no pueda hacerse todo esto con un script, pero es más complejo y es preferible estructurarlo.
La mejor forma de construir un programa python es estructutarlo como un paquete Python, con soporte para control de versiones, en nuestro caso GitHub. De este modo podrá instalarse a través del comando pip.
Para que nuestro proyecto pueda beneficiarse de ser un paquete Python, debe tener la siguiente estructura:
nombre_proyecto/ │ ├── nombre_programa/ # Paquete principal │ ├── __init__.py │ ├── programa.py # Tus funciones │ └── main.py # Punto de entrada │ ├── setup.py # Script de instalación ├── README.md # (Opcional) ├── LICENSE # Licencia └── .gitignore # Archivos a ignorar en la sincronización con el repositorio
A continuación comento cada uno de estos archivos.
Este archivo indica a Python que esta carpeta es un paquete (podría estar vacía).
Funciona como un “expositor” de las funciones principales del paquete, de modo que puede incluir código que define qué funciones o clases están disponibles al hacer import.
Ejemplo:
from .gendocs import generar_documentos_simples_xlsx, generar_documentos_compuestos_xlsx, cruce_documentos
Es el punto de entrada al paquete cuando lo ejecutas desde consola.
Es donde se define la interfaz de línea de comandos (CLI, Command Line Interface): qué comandos, opciones y argumentos puede introducir el usuario, incluidos los –help. En ocasiones el archivo donde se encuentra la función main() también suele llamarse cli.py
Utiliza la biblioteca argparse para leer argumentos desde línea de comandos.
En main.py se puede:
En definitiva, realiza el flujo del programa.
Cuando añadimos parámetros con add_argument, tenemos las siguientes opciones:
| Parámetro | Descripción | Ejemplo |
|---|---|---|
| `name or flags` | Nombre del argumento (posicional o con guiones). | `'–verbose'`, `'-v'`, `'archivo'` |
| `action` | Qué hacer con el argumento al encontrarlo (ver tabla de abajo). | `'store'`, `'store_true'`, `'append'`, etc. |
| `nargs` | Número de valores esperados (`?`, `*`, `+`, número entero, etc). | `nargs='?'`, `nargs=2` |
| `const` | Valor constante usado cuando se emplea con ciertas acciones como `store_const`. | `const=42` |
| `default` | Valor por defecto si no se proporciona el argumento. | `default='salida.txt'` |
| `type` | Tipo de dato al que convertir el argumento. | `type=int`, `type=float`, `type=Path` |
| `choices` | Lista de valores permitidos. | `choices=['bajo', 'medio', 'alto']` |
| `required` | Si el argumento es obligatorio (solo para argumentos opcionales). | `required=True` |
| `help` | Texto de ayuda mostrado al ejecutar `–help`. | `help='Archivo de entrada.'` |
| `metavar` | Nombre que aparece en la ayuda en lugar del nombre real del argumento. | `metavar='FICHERO'` |
| `dest` | Nombre del atributo en `args` donde se guarda el valor. | `dest='nivel'` |
Para el parámetro “action” disponemos e estas posibilidades:
| Acción | Descripción | Ejemplo de uso |
|---|---|---|
| `'store'` | Guarda el valor dado (por defecto). | `–salida salida.txt` |
| `'store_const'` | Guarda un valor constante (requiere `const=`). | `–modo`, `action='store_const', const='debug'` |
| `'store_true'` | Guarda `True` si se especifica, `False` si no. | `–verbose`, `action='store_true'` |
| `'store_false'` | Guarda `False` si se especifica, `True` si no. | `–no-cache`, `action='store_false'` |
| `'append'` | Añade cada valor a una lista. | `–tag urgente –tag personal` → `['urgente',…]` |
| `'append_const'` | Añade un valor constante a una lista (requiere `const=`). | (menos común) |
| `'count'` | Cuenta cuántas veces aparece el argumento. | `-v -v -v` → `3` |
| `'help'` | Muestra el mensaje de ayuda y termina. (automática con `–help`) | `–help` |
| `'version'` | Muestra el número de versión y termina. (requiere `version=`) | `–version` |
A continuación un ejemplo, en el que se define el comando “gendocs” y 3 subcomandos “simples”, “compuestos” y “cruce”, cada uno con sus parámetros de entrada:
# Este archivo es el punto de entrada al paquete cuando lo ejecutas desde consola: Define la función main() que se ejecuta cuando alguien escribe "docgen ..." # Usa argparse para leer argumentos desde línea de comandos. import argparse import os from gendocs.gendocs import ( generar_documentos_simples_xlsx, generar_documentos_compuestos_xlsx, cruce_documentos ) def main(): parser = argparse.ArgumentParser(description="Generador de documentos Word/PDF, a partir de los datos de un documento Excel y plantillas Word") subparsers = parser.add_subparsers(dest="comando", required=True, help="Elige el tipo de generación") # Los argumentos obligatorios deben ir en orden, los opcionales deben llamarse igual que en la función. # Subcomando: Documentos simples parser_simple = subparsers.add_parser( "simple", help="Genera documentos simples a partir de un Excel" ) parser_simple.add_argument("excel", help="Ruta al archivo Excel") parser_simple.add_argument("hoja", help="Nombre de la hoja del Excel") parser_simple.add_argument("plantilla", help="Ruta a la plantilla Word") parser_simple.add_argument("salida", help="Directorio donde se guardarán los documentos") parser_simple.add_argument("--guardar_ruta_pdf", action="store_true", help="Guarda la ruta del PDF en el Excel") parser_simple.add_argument("--normalizar", action="store_true", help="Normaliza nombres de archivo") parser_simple.add_argument("--encabezado_ruta_pdf", default="RutaPDF", help="Nombre del encabezado para ruta PDF") parser_simple.add_argument("--separador", default="_", help="Separador para el nombre del archivo") parser_simple.add_argument("--nombre_archivo", nargs="*", help="Lista de campos para construir el nombre del archivo") # Subcomando: Documentos compuestos parser_compuesto = subparsers.add_parser( "compuesto", help="Genera documentos agrupados a partir de un Excel" ) parser_compuesto.add_argument("excel", help="Ruta al archivo Excel") parser_compuesto.add_argument("hoja", help="Nombre de la hoja del Excel") parser_compuesto.add_argument("plantilla", help="Ruta a la plantilla Word") parser_compuesto.add_argument("salida", help="Directorio donde se guardarán los documentos") parser_compuesto.add_argument("--campos_agrupar", nargs="+", required=True, help="Campos del Excel para agrupar") parser_compuesto.add_argument("--guardar_ruta_pdf", action="store_true", help="Guarda la ruta del PDF en el Excel") parser_compuesto.add_argument("--normalizar", action="store_true", help="Normaliza nombres de archivo") parser_compuesto.add_argument("--encabezado_ruta_pdf", default="RutaPDF", help="Nombre del encabezado para ruta PDF") parser_compuesto.add_argument("--separador", default="_", help="Separador para el nombre del archivo") parser_compuesto.add_argument("--nombre_archivo", nargs="*", help="Lista de campos para construir el nombre del archivo") parser_compuesto.add_argument("--hoja_docs", default="Documentos", help="Nombre de la hoja donde guardar las rutas") # Subcomando: Cruce automático parser_cruce = subparsers.add_parser( "cruce", help="Elige automáticamente entre documento simple o compuesto según agrupación" ) parser_cruce.add_argument("excel", help="Ruta al archivo Excel") parser_cruce.add_argument("hoja", help="Nombre de la hoja del Excel") parser_cruce.add_argument("plantilla_simple", help="Ruta a la plantilla Word para documentos simples") parser_cruce.add_argument("plantilla_resumen", help="Ruta a la plantilla Word para documentos compuestos") parser_cruce.add_argument("salida", help="Directorio donde se guardarán los documentos") parser_cruce.add_argument("--campos_agrupar", nargs="+", required=True, help="Campos del Excel para agrupar") parser_cruce.add_argument("--guardar_ruta_pdf", action="store_true", help="Guarda la ruta del PDF en el Excel") parser_cruce.add_argument("--normalizar", action="store_true", help="Normaliza nombres de archivo") parser_cruce.add_argument("--encabezado_ruta_pdf", default="RutaPDF", help="Nombre del encabezado para ruta PDF") parser_cruce.add_argument("--separador", default="_", help="Separador para el nombre del archivo") parser_cruce.add_argument("--nombre_archivo", nargs="*", help="Lista de campos para construir el nombre del archivo") parser_cruce.add_argument("--hoja_docs_simples", default="Documentos simples", help="Nombre de la hoja para documentos simples") parser_cruce.add_argument("--hoja_docs_compuestos", default="Documentos compuestos", help="Nombre de la hoja para documentos compuestos") args = parser.parse_args() os.makedirs(args.salida, exist_ok=True) # Asegura que la carpeta de salida existe if args.comando == "simple": generar_documentos_simples_xlsx( doc_excel=args.excel, hoja_excel=args.hoja, doc_word=args.plantilla, directorio=args.salida, nombre_archivo=args.nombre_archivo or [], separador=args.separador, normalizar=args.normalizar, guardar_ruta_pdf=args.guardar_ruta_pdf, encabezado_ruta_pdf=args.encabezado_ruta_pdf ) elif args.comando == "compuesto": generar_documentos_compuestos_xlsx( doc_excel=args.excel, hoja_excel=args.hoja, doc_word=args.plantilla, directorio=args.salida, campos_agrupar=args.campos_agrupar, nombre_archivo=args.nombre_archivo or [], separador=args.separador, normalizar=args.normalizar, guardar_ruta_pdf=args.guardar_ruta_pdf, encabezado_ruta_pdf=args.encabezado_ruta_pdf, hoja_docs=args.hoja_docs ) elif args.comando == "cruce": cruce_documentos( doc_excel=args.excel, hoja_excel=args.hoja, plantilla_word_simple=args.plantilla_simple, plantilla_word_resumen=args.plantilla_resumen, directorio=args.salida, campos_agrupar=args.campos_agrupar, nombre_archivo=args.nombre_archivo or [], separador=args.separador, normalizar=args.normalizar, guardar_ruta_pdf=args.guardar_ruta_pdf, encabezado_ruta_pdf=args.encabezado_ruta_pdf, hoja_docs_simples=args.hoja_docs_simples, hoja_docs_compuestos=args.hoja_docs_compuestos ) else: parser.print_help() if __name__ == "__main__": main()
Un ejemplo de comando sería:
gendocs cruce ` "E:\OneDrive CAD\OneDrive - Universidad de Alcala\CURSO 2024-25\Taller 3º Anamnesis e historia clínica\Taller de Anamnesis e Historia Clínica 24_25(1-8).xlsx" ` "Hoja1" ` "E:\OneDrive CAD\OneDrive - Universidad de Alcala\CURSO 2024-25\Taller 3º Anamnesis e historia clínica\Plantilla certificado simple.docx" ` "E:\OneDrive CAD\OneDrive - Universidad de Alcala\CURSO 2024-25\Taller 3º Anamnesis e historia clínica\Plantilla certificado compuesto.docx" ` "E:\OneDrive CAD\OneDrive - Universidad de Alcala\CURSO 2024-25\Taller 3º Anamnesis e historia clínica\Certificados" ` --campos_agrupar "Nombre" "Apellidos" "Correo electrónico" ` --guardar_ruta_pdf ` --normalizar ` --separador _ ` --nombre_archivo Apellidos Nombre "-" Taller "24-25" ` --hoja_docs_simples "Documentos simples" ` --hoja_docs_compuestos "Documentos compuestos"
Es el script de instalación. Usa la librería setuptools para convertir este proyecto en un paquete instalable con pip.
Describe los metadatos (nombre, versión, dependencias, etc.), permite que el proyecto se instale localmente o se distribuya y configura comandos que puedas ejecutar desde cualquier terminal.
Crea el comando: “gendocs –help”, que ejecuta la función main() que está en nombre_programa/main.py
Ejemplo:
from setuptools import setup, find_packages setup( name="gendocs", # Nombre del paquete version="1.0.0", # Versión packagez=find_packages(), # Encuentra automáticamente los subpaquetes entry_points={ # Punto de entrada para crear un comando en consola "console_scripts": [ "gendocs=gendocs.main:main", # Conecta el comando docgen a la función main() ], }, install_requires=[ # Dependencias "python-docx", "docx2pdf", "openpyxl" ], python_requires = ">=3.8", # Versión mínima de Python )
El bloque entry_points[“console_scripts”] de setup.py sirve para registrar comandos de terminal que apuntan a funciones Python:
"console_scripts": [
"mi_comando = paquete.modulo:funcion_que_se_ejecuta"
]
mi_comando → nombre que escribirás en la terminal.
paquete.modulo → ruta al módulo dentro del paquete.
funcion_que_se_ejecuta → debe ser una función sin argumentos (o que gestione sys.argv) que actuarán como el “main()” del comando.
Si se desea cambiar el comando para ejecutar un programa, es suficiente con indicarlo en “console_scripts” y cambiar el campo “mi_comando”. A continuación es necesario reinstalar con “pip install -e .”, por ejemplo.
Es un archivo de texto (usualmente en formato Markdown, de ahí la extensión .md) que explica de qué trata el proyecto.
Sirve como presentación o guía rápida del proyecto. Suele incluir:
Ejemplo:
# Mi Proyecto Genial Este programa convierte texto a voz. ## Instalación ```bash pip install mi-proyecto-genial
Archivo que especifica los términos legales de uso del software. Indica:
Algunos ejemplos de licencias:
No es obligatorio incluirlo, pero sin licencia, legalmente nadie puede usar tu código, aunque esté en GitHub.
Pueden encontrarse en sus respectivas páginas web.
Puede que no quieras que el usuario al que está destinado el programa tenga acceso a todas las funciones, aunque sean necesarias internamente. Para eso existen 2 opciones:
__all__ = [ "generar_documento_simple", "generar_documento_compuesto", ] # De este modo, si alguien importa con from certificados_lib import *, solo se cargan esas funciones visibles.
Para instalar el programa en tu máquina local, desde la consola de comandos debes navegar hasta onde se encuentre el archivo setup.py y ejecutar:
pip install -e . # Sustituye '.' por la ruta, si no estás en el mismo directorio que setup.py
El instalador añade el comando (el nombre que hayas puesto en entry_points) al PATH de tu entorno Python.
Además, la carpeta desde donde haces la instalación en modo editable (pip install -e .) es, literalmente, la carpeta donde está el código que se ejecuta: Python crea un enlace simbólico (acceso directo interno) desde el entorno Python hacia la carpeta donde está tu código (donde está setup.py).
Por eso cualquier cambio en esa carpeta afecta directamente al paquete instalado y ejecutable, sin necesidad de reinstalar.
Lógicamente si se lleva el paquete a otro equipo, podría instalarse de la forma anterior.
Sin embargo, también podría instalar se esta manera:
pip install . # Sustituye '.' por la ruta, si no estás en el mismo directorio que setup.py
Al prescindir del parámetro '-e' no ya no estamos en modo editable: se copiar el código fuente dentro de la carpeta interna de paquetes de Python (site-packages).
En este caso, la ejecución del programa ya no depende de la carpeta de instalación original y es posible mover, borrar o renombrar la carpeta del proyecto, y el comando seguirá funcionando igual, porque ya tiene su copia interna.
Puedes instalar tu paquete desde cualquier máquina con:
pip install git+https://github.com/usuario/repositorio.git
O para una versión específica:
# Esto instalará la versión exactamente como estaba en el tag v1.0.0. pip install git+https://github.com/usuario/repositorio.git@v1.0.0
Desde GitHub, el programa se instala en modo no editable.
Cada vez que se realice una mejora, corrección o cambio relevante, se hace un commit con un mensaje descriptivo.
Al completar un conjunto de cambios estables (por ejemplo, una versión funcional completa), se hace un release/tag con un número de versión:
git tag v1.0.0 git push origin v1.0.0 # Ahora en GitHub aparece ese tag como versión 1.0.0. Puedes descargar ese estado exacto o instalar desde ahí.
Y para actualizar a la nueva versión (por ejemplo la v1.1.0):
pip install --upgrade git+https://github.com/usuario/repositorio.git@v1.1.0