• Auto-instalar dependencias de python al ejecutar un script

    Muchas veces, al ejecutar un script de Python, este pretende importar paquetes que puede que no estén instalados previamente en el entorno virtual o en el sistema. Con este truco, al poner en la misma carpeta el script depinstall.py que os dejo a continuación, se pueden declarar las dependencias en el script principal de manera que el propio script principal use este otro script para instalar esas dependencias.

    Este es el código del script depinstall.py:

    #!/usr/bin/env python3
    
    import importlib.util
    import subprocess
    import sys
    
    # Paquete apt necesario para instalar paquetes python
    cNombreDelPaqueteApt = "python3-pip"
    
    def fPaqueteAptEstaInstalado(pNombreDelPaqueteApt):
      """Verifica si un paquete apt está instalado."""
      vResultado = subprocess.run(
        ["dpkg", "-s", pNombreDelPaqueteApt],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL
      )
      return vResultado.returncode == 0
    
    def fInstalarPaqueteApt(pNombreDelPaqueteApt):
      """Instala un paquete mediante apt."""
      print(f"[*] Instalando paquete apt: {pNombreDelPaqueteApt}")
      try:
        subprocess.run(
          ["sudo", "apt-get", "-y", "update"],
          check=True
        )
        subprocess.run(
          ["sudo", "apt-get", "-y", "install", pNombreDelPaqueteApt],
          check=True
        )
        print(f"[✓] {pNombreDelPaqueteApt} instalado correctamente")
      except subprocess.CalledProcessError as e:
        print(f"[✗] Error instalando {pNombreDelPaqueteApt}: {e}")
        sys.exit(1)
    
    def fModuloPythonEstaInstalado(pNombreDelModulo):
      """Verifica si un módulo Python está disponible."""
      return importlib.util.find_spec(pNombreDelModulo) is not None
    
    def fInstalarPaquetePython(pNombreDelPaquete):
      """Instala un paquete Python mediante pip."""
      print(f"[*] Instalando paquete Python: {pNombreDelPaquete}")
      try:
        subprocess.run(
          [
            sys.executable,
            "-m", "pip", "install",
            pNombreDelPaquete,
            "--break-system-packages"
          ],
          check=True
        )
        print(f"[✓] {pNombreDelPaquete} instalado correctamente")
      except subprocess.CalledProcessError as e:
        print(f"[✗] Error instalando {pNombreDelPaquete}: {e}")
        return False
      return True
    
    def fComprobarEInstalarPaquetes(pdPaquetesPython):
      """Comprueba e instala los paquetes Python necesarios."""
      aErrores = []
      for vNombrePip, vNombreModulo in pdPaquetesPython.items():
        if fModuloPythonEstaInstalado(vNombreModulo):
          print(f"[✓] {vNombrePip} ya está instalado")
        else:
          if not fInstalarPaquetePython(vNombrePip):
            aErrores.append(vNombrePip)
      return aErrores
    
    def fInstalarDependencias(pdPaquetesPython):
      """
      Función principal para instalar dependencias.
      
      Args:
        pdPaquetesPython: Diccionario {nombre_pip: nombre_modulo}
      
      Returns:
        True si todo OK, False si hubo errores
      """
      print("=== Comprobando dependencias ===\n")
      
      # Verificar pip
      if not fPaqueteAptEstaInstalado(cNombreDelPaqueteApt):
        fInstalarPaqueteApt(cNombreDelPaqueteApt)
      else:
        print(f"[✓] {cNombreDelPaqueteApt} ya está instalado")
      
      print()
      
      # Instalar paquetes Python
      aErrores = fComprobarEInstalarPaquetes(pdPaquetesPython)
      
      print("\n=== Resumen ===")
      if aErrores:
        print(f"[!] Paquetes con errores: {', '.join(aErrores)}")
        return False
      else:
        print("[✓] Todas las dependencias instaladas correctamente")
        return True
    
    # Solo para pruebas directas del módulo
    if __name__ == "__main__":
      dPaquetesEjemplo = {
        "flask": "flask",
        "requests": "requests",
      }
      if not fInstalarDependencias(dPaquetesEjemplo):
        sys.exit(1)

    Poner ese script en la misma carpeta en la que tenemos el script principal nos permite hacer lo siguiente en el script principal, justo después del shebang:

    # -- Instalación de dependencias Python --
    from depinstall import fInstalarDependencias
    dDependencias = {
      "flask": "flask",
      "requests": "requests",
      "Pillow": "PIL",
      "python-nmap": "nmap",
    }
    fInstalarDependencias(dDependencias)
    # -- Fin de la instalación de dependencias python

    De esta forma, cada vez que se ejecute el script principal, comprobará que los paquetes necesarios para que el script se ejecute correctamente, estén instalados. Y, si no lo están, los instalará.

    También se puede hacer de otra forma: podemos poner los paquetes de python en el archivo requirements-python.txt y los paquetes de debian en requirements-debian.txt y que el siguiente código:

    #!/usr/bin/env python3
    
    import importlib.util
    import subprocess
    import sys
    from pathlib import Path
    
    cArchivoRequirementsPython = "requirements-python.txt"
    cArchivoRequirementsDebian = "requirements-debian.txt"
    cNombreDelPaqueteApt = "python3-pip"
    
    def fPaqueteAptEstaInstalado(pNombreDelPaqueteApt):
      vResultado = subprocess.run(
        ["dpkg", "-s", pNombreDelPaqueteApt],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL
      )
      return vResultado.returncode == 0
    
    def fInstalarPaqueteApt(pNombreDelPaqueteApt):
      print(f"[*] Instalando paquete apt: {pNombreDelPaqueteApt}")
      try:
        subprocess.run(
          ["sudo", "apt-get", "-y", "update"],
          check=True
        )
        subprocess.run(
          ["sudo", "apt-get", "-y", "install", pNombreDelPaqueteApt],
          check=True
        )
        print(f"[✓] {pNombreDelPaqueteApt} instalado correctamente")
      except subprocess.CalledProcessError as e:
        print(f"[✗] Error instalando {pNombreDelPaqueteApt}: {e}")
        sys.exit(1)
    
    def fModuloPythonEstaInstalado(pNombreDelModulo):
      return importlib.util.find_spec(pNombreDelModulo) is not None
    
    def fInstalarPaquetePython(pNombreDelPaquete):
      print(f"[*] Instalando paquete Python: {pNombreDelPaquete}")
      try:
        subprocess.run(
          [
            sys.executable,
            "-m", "pip", "install",
            pNombreDelPaquete,
            "--break-system-packages"
          ],
          check=True
        )
        print(f"[✓] {pNombreDelPaquete} instalado correctamente")
      except subprocess.CalledProcessError as e:
        print(f"[✗] Error instalando {pNombreDelPaquete}: {e}")
        return False
      return True
    
    def fLeerArchivoRequisitos(pRutaArchivo):
      vRuta = Path(pRutaArchivo)
      if not vRuta.exists():
        print(f"[!] Archivo no encontrado: {pRutaArchivo}")
        return []
    
      aLineas = []
      with vRuta.open("r", encoding="utf-8") as vFichero:
        for vLinea in vFichero:
          vLinea = vLinea.strip()
          if vLinea and not vLinea.startswith("#"):
            aLineas.append(vLinea)
      return aLineas
    
    def fInstalarPaquetesDebianDesdeArchivo(pArchivo):
      print("=== Comprobando paquetes Debian ===\n")
      aPaquetes = fLeerArchivoRequisitos(pArchivo)
      aErrores = []
    
      for vPaquete in aPaquetes:
        if fPaqueteAptEstaInstalado(vPaquete):
          print(f"[✓] {vPaquete} ya está instalado")
        else:
          try:
            fInstalarPaqueteApt(vPaquete)
          except SystemExit:
            aErrores.append(vPaquete)
    
      return aErrores
    
    def fInstalarPaquetesPythonDesdeArchivo(pArchivo):
      print("\n=== Comprobando paquetes Python ===\n")
      aPaquetes = fLeerArchivoRequisitos(pArchivo)
      aErrores = []
    
      for vPaquete in aPaquetes:
        vNombreModulo = vPaquete.split("==")[0].split(">=")[0].split("<=")[0]
        if fModuloPythonEstaInstalado(vNombreModulo):
          print(f"[✓] {vPaquete} ya está instalado")
        else:
          if not fInstalarPaquetePython(vPaquete):
            aErrores.append(vPaquete)
    
      return aErrores
    
    def fInstalarDependencias():
      print("=== Iniciando instalación de dependencias ===\n")
    
      if not fPaqueteAptEstaInstalado(cNombreDelPaqueteApt):
        fInstalarPaqueteApt(cNombreDelPaqueteApt)
      else:
        print(f"[✓] {cNombreDelPaqueteApt} ya está instalado")
    
      aErroresDebian = fInstalarPaquetesDebianDesdeArchivo(cArchivoRequirementsDebian)
      aErroresPython = fInstalarPaquetesPythonDesdeArchivo(cArchivoRequirementsPython)
    
      print("\n=== Resumen ===")
      if aErroresDebian:
        print(f"[!] Errores en paquetes Debian: {', '.join(aErroresDebian)}")
      if aErroresPython:
        print(f"[!] Errores en paquetes Python: {', '.join(aErroresPython)}")
    
      if not aErroresDebian and not aErroresPython:
        print("[✓] Todas las dependencias instaladas correctamente")
        return True
    
      return False
    
    if __name__ == "__main__":
      if not fInstalarDependencias():
        sys.exit(1)
    

    …busque e instale los paquetes que aparecen en esos archivos de forma que, ponemos los tres archivos en la misma carpeta del código de nuestra app de python, podemos llamarlo directamente desde el script de la app haciendo esto:

    import depinstall
    
    if not depinstall.fInstalarDependencias():
      raise SystemExit(1)

    El contenido de requirements-debian.txt podría ser

    python3-nmap
    python3-requests

    Y el contenido de requirements-python.txt podría ser

    flask
    x

    Los comentarios están cerrados.