zipapp — Gestiona archivadores zip ejecutables de Python

Nuevo en la versión 3.5.

Código fuente: Lib/zipapp.py


Este módulo provee herramientas para administrar la creación de archivos zip que contengan código Python, los que pueden ser ejecutados directamente por el intérprete de Python. El módulo provee tanto una Interfaz de línea de comando y una API de Python.

Ejemplo básico

El siguiente ejemplo muestra cómo la Interfaz de línea de comando puede utilizarse para crear un archivador ejecutable de un directorio que contenga código en Python. Al ponerse en funcionamiento, el archivador ejecutará la función main del módulo myapp en el archivador.

$ python -m zipapp myapp -m "myapp:main"
$ python myapp.pyz
<output from myapp>

Interfaz de línea de comando

En la ejecución como programa desde la línea de comandos, se utiliza el siguiente formato:

$ python -m zipapp source [options]

Si source es un directorio, se creará un archivador zip para los contenidos de source. Si source es un archivo, debería ser un archivador, y se copiará al archivador de destino (o los contenidos de su línea shebang se mostrarán si se especifica la opción –info).

Se aceptan las siguientes opciones:

-o <output>, --output=<output>

Escribe la salida a un archivo llamado output. Si esta opción no está especificada, el nombre del archivo de salida será el mismo que la entrada source, con la extensión .pyz agregada. Si se provee de un nombre de archivo explícito, se usa tal como está (por lo que una extensión .pyz debería incluirse si esto se requiere).

Un nombre de archivo de salida debe especificarse si source es un archivador (y en ese caso, output no debería ser igual que source).

-p <interpreter>, --python=<interpreter>

Agrega una línea #! al archivador especificando interpreter como comando a ejecutar. También en POSIX, convierte al archivador en ejecutable. La opción por defecto es no escribir una línea #!, y no hacer que el archivo sea ejecutable.

-m <mainfn>, --main=<mainfn>

Escribe un archivo __main__.py en el archivador que ejecuta mainfn. El argumento mainfn debería tener la forma «pkg.mod:fn», donde «pkg.mod» is un paquete/módulo en el archivador, y «fn» es un invocable (callable) en ese módulo. El archivo __main__.py ejecutará ese invocable.

--main no puede especificarse al copiar un archivador.

-c, --compress

Comprime los archivos con el método deflate, lo que reduce el tamaño del archivo de salida. Por defecto, los archivos se guardan sin comprimir en el archivador.

--compress no surte efecto al copiar un archivador.

Nuevo en la versión 3.7.

--info

Muestra el intérprete incrustado en el archivador, para diagnósticos. En este caso cualquier otra opción se ignora, y SOURCE debe ser un archivador, no un directorio.

-h, --help

Muestra un breve mensaje sobre el modo de uso, y sale.

API de Python

El módulo define dos funciones convenientes:

zipapp.create_archive(source, target=None, interpreter=None, main=None, filter=None, compressed=False)

Crea un archivador de aplicación a partir de source. Este source (origen), puede ser cualquiera de los siguientes:

  • El nombre de un directorio, o un path-like object que se refiera a un directorio, en cuyo caso un nuevo archivador de aplicación se creará a partir del contenido de dicho directorio.

  • El nombre de un archivador de aplicación existente o un path-like object que refiera a dicho archivo, en cuyo caso el archivo se copiará al destino (modificándolo para reflejar el valor dado por el argumento interpreter. El nombre de archivo debería incluir la extensión .pyz, si se requiere.

  • Un objeto archivo abierto para lectura en modo bytes. El contenido del archivo debería ser un archivador de aplicación. Se infiere que el objeto archivo está posicionado al comienzo del archivador.

El argumento target determina dónde quedará escrito el archivador resultante:

  • Si es el nombre de un archivo, o un path-like object, el archivador será escrito a ese archivo.

  • Si es un objeto archivo abierto, el archivador se escribirá en ese objeto archivo, el cual debe estar abierto para escritura en modo bytes.

  • Si el destino (target) se omite (o es None), el origen (source) debe ser un directorio, y el destino será un archivo con el mismo nombre que el origen, con la extensión .pyz añadida.

El argumento interpreter especifica el nombre del intérprete Python con el que el archivador será ejecutado. Se escribe como una línea «shebang» al comienzo del archivador. En POSIX, el Sistema Operativo será quien lo interprete, y en Windows será gestionado por el lanzador Python. Omitir el interpreter tendrá como consecuencia que no se escribirá ninguna línea shebang. Si se especifica un intérprete y el destino (target) es un nombre de archivo, el bit de ejecución del archivo destino será activado.

El argumento main especifica el nombre de un invocable (callable) que se utilizará como programa principal para el archivador. Solamente se puede especificar si el origen (source) es un directorio que no contiene un archivo __main__.py. El argumento main debería tener la forma «pkg.module:callable», y el archivador será ejecutado importando «pkg.module» y ejecutando el callable sin argumentos. Es un error omitir main si el origen es un directorio que no contiene un archivo __main__.py, ya que esto resultaría en un archivador no ejecutable.

El argumento opcional filter especifica una función callback que se pasa como objeto Path, para representar el path (ruta) al archivo que se está añadiendo (ruta relativa al directorio origen, source). Debería retornar True si el archivo será añadido.

El argumento opcional compressed determina si los archivos están comprimidos. Si se define como True, los archivos en el archivador serán comprimidos con el método deflate.

Si se especifica un objeto archivo para source o * target*, es responsabilidad de quien invoca cerrarlo luego de invocar a create_archive.

Al copiar un archivador existente, los objetos archivo provistos, solamente necesitan los métodos read y readline, o bien write. Al crear un archivador a partir de un directorio, si el destino (target) es un objeto archivo, éste se pasará a la clase zipfile.ZipFile, y debe proveer los métodos que esa clase necesita.

Nuevo en la versión 3.7: Añadidos los argumentos filter y compressed.

zipapp.get_interpreter(archive)

Retorna el intérprete especificado en la línea #! al comienzo del archivador. Si no hay línea #!, retorna None. El argumento archive (archivador), puede ser un nombre de archivo o un objeto tipo archivo abierto para lectura en modo bytes. Se supone que está al principio del archivador.

Ejemplos

Empaqueta un directorio en un archivador, y lo ejecuta.

$ python -m zipapp myapp
$ python myapp.pyz
<output from myapp>

Lo mismo puede lograrse utilizando la función create_archive():

>>> import zipapp
>>> zipapp.create_archive('myapp', 'myapp.pyz')

Para que la aplicación sea ejecutable directamente en POSIX, especifica un intérprete.

$ python -m zipapp myapp -p "/usr/bin/env python"
$ ./myapp.pyz
<output from myapp>

Para reemplazar la línea shebang en un archivador existente, cree un archivador modificado, utilizando la función create_archive():

>>> import zipapp
>>> zipapp.create_archive('old_archive.pyz', 'new_archive.pyz', '/usr/bin/python3')

Para actualizar el archivo en el lugar, reemplaza en memoria utilizando un objeto BytesIO, y luego sobreescribe el origen (source). Nótese que hay un riesgo al sobreescribir un archivo en el lugar, ya que un error resultará en la pérdida del archivo original. Este código no ofrece protección contra este tipo de errores, sino que el código de producción debería hacerlo. Además, este método solamente funcionará si el archivador cabe en la memoria:

>>> import zipapp
>>> import io
>>> temp = io.BytesIO()
>>> zipapp.create_archive('myapp.pyz', temp, '/usr/bin/python2')
>>> with open('myapp.pyz', 'wb') as f:
>>>     f.write(temp.getvalue())

Especificar el intérprete

Nótese que si se especifica el intérprete y luego se distribuye el archivador de aplicación, es necesario asegurarse de que el intérprete utilizado es portable. El lanzador Python para Windows soporta las formas más comunes de líneas #! POSIX, pero hay otras cuestiones a considerar:

  • Si se utiliza «/usr/bin/env python» (u otras formas del comando «python», como «/usr/bin/python»), es necesario considerar que los usuarios quizá tengan tanto Python2 como Python3 como versión por defecto, y el código debe escribirse bajo ambas versiones.

  • Si se utiliza una versión específica, por ejemplo «/usr/bin/env python3», la aplicación no funcionará para los usuarios que no tengan esa versión. (Esta puede ser la opción deseada si no se hecho el código compatible con Python 2).

  • No hay manera de decir «python X.Y o posterior», así que se debe ser cuidadoso al utilizar una versión exacta, tal como «/usr/bin/env python3.4», ya que será necesario cambiar la línea shebang para usuarios de Python 3.5, por ejemplo.

Normalmente, se debería utilizar «/usr/bin/env python2» o «/usr/bin/env python3», según si el código está escrito para Python 2 ó 3.

Creando aplicaciones independientes con zipapp

Utilizando el módulo zipapp, es posible crear programas Python auto-contenidos, que pueden ser distribuidos a usuarios finales que solo necesitarán una versión adecuada de Python instalada en sus sistemas. La clave es empaquetar todas las dependencias de la aplicación dentro del archivador, junto al código de la misma.

Los pasos para crear un archivador independiente son los siguientes:

  1. Crea tu aplicación en un directorio normalmente, tal que tengas una directorio myapp que contenga un archivo __main__.py, y cualquier código extra de la aplicación.

  2. Instala todas las dependencias de la aplicación en el directorio myapp, usando pip:

    $ python -m pip install -r requirements.txt --target myapp
    

    (se supone que tienes los requisitos de tu proyecto en un archivo requirements.txt, de lo contrario, puedes listar manualmente las dependencias en la línea de comandos de pip).

  3. Opcionalmente, borra los directorios .dist-info creados por pip en el directorio myapp. Éstos tienen metadatos para que pip gestione los paquetes, y como ya no se utilizará pip, no hace falta que permanezcan (aunque no causará ningún problema si los dejas).

  4. Empaqueta la aplicación utilizando:

    $ python -m zipapp -p "interpreter" myapp
    

Esto producirá un ejecutable independiente, que puede ser ejecutado en cualquier máquina que disponga del intérprete apropiado. Véase Especificar el intérprete para más detalles. Puede enviarse a los usuarios como un solo archivo.

En Unix, el archivo myapp.pyz será ejecutable tal como está. Puede ser renombrado, para quitar la extensión .pyz si se prefiere un nombre de comando «simple». En Windows, el archivo myapp.pyz[w] es ejecutable, ya que el intérprete Python registra las extensiones .pyz y pyzw al ser instalado.

Hacer un ejecutable para Windows

En Windows, registrar la extensión .pyz es opcional, y además hay ciertos sitios que no reconocen las extensiones registradas de manera «transparente» (el ejemplo más simple es que subprocess.run(['myapp']) no va a encontrar la aplicación, es necesario especificar explícitamente la extensión).

Por lo tanto, en Windows, suele ser preferible crear un ejecutable a partir del zipapp. Esto es relativamente fácil, aunque requiere un compilador de C. La estrategia básica se basa en que los archivos zip pueden tener datos arbitrarios antepuestos, y los archivos exe de Windows pueden tener datos arbitrarios agregados. Entonces, si se crea un lanzador adecuado mudando el archivo .pyz al final del mismo, se obtiene un solo archivo ejecutable que corre la aplicación.

Un lanzador adecuado puede ser así de simple:

#define Py_LIMITED_API 1
#include "Python.h"

#define WIN32_LEAN_AND_MEAN
#include <windows.h>

#ifdef WINDOWS
int WINAPI wWinMain(
    HINSTANCE hInstance,      /* handle to current instance */
    HINSTANCE hPrevInstance,  /* handle to previous instance */
    LPWSTR lpCmdLine,         /* pointer to command line */
    int nCmdShow              /* show state of window */
)
#else
int wmain()
#endif
{
    wchar_t **myargv = _alloca((__argc + 1) * sizeof(wchar_t*));
    myargv[0] = __wargv[0];
    memcpy(myargv + 1, __wargv, __argc * sizeof(wchar_t *));
    return Py_Main(__argc+1, myargv);
}

Si se define el símbolo de preprocesador WINDOWS, se generará una GUI (interfaz gráfica de usuario) ejecutable, y sin este símbolo, un ejecutable de consola.

Para compilar el ejecutable, se puede usar únicamente la línea de comando estándar MSVC, o se puede aprovechar la ventaja de que distutils sabe cómo compilar código fuente Python:

>>> from distutils.ccompiler import new_compiler
>>> import distutils.sysconfig
>>> import sys
>>> import os
>>> from pathlib import Path

>>> def compile(src):
>>>     src = Path(src)
>>>     cc = new_compiler()
>>>     exe = src.stem
>>>     cc.add_include_dir(distutils.sysconfig.get_python_inc())
>>>     cc.add_library_dir(os.path.join(sys.base_exec_prefix, 'libs'))
>>>     # First the CLI executable
>>>     objs = cc.compile([str(src)])
>>>     cc.link_executable(objs, exe)
>>>     # Now the GUI executable
>>>     cc.define_macro('WINDOWS')
>>>     objs = cc.compile([str(src)])
>>>     cc.link_executable(objs, exe + 'w')

>>> if __name__ == "__main__":
>>>     compile("zastub.c")

El lanzador resultante utiliza «Limited ABI», así que correrá sin cambios con cualquier versión de Python 3.x. Todo lo que necesita es que Python (python3.dll) esté en el PATH del usuario.

Para una distribución completamente independiente, se puede distribuir el lanzador con la aplicación incluida, empaquetada con la distribución «embedded» de Python. Va a funcionar en cualquier PC con la arquitectura adecuada (32 o 64 bits).

Advertencias

Hay algunas limitaciones para empaquetar la aplicación en un solo archivo. In la mayoría, si no en todos los casos, se pueden abordar sin que haga falta ningún cambio importante en la aplicación.

  1. Si la aplicación depende de un paquete que incluye una extensión C, ese paquete no puede ser ejecutado desde un archivo zip (esta es una limitación del sistema operativo, dado que el código ejecutable debe estar presente en el sistema de archivos para que el loader del SO lo pueda cargar). En este caso, se puede excluir la dependencia del archivo zip, y requerir que los usuarios la tengan instalada, o bien distribuirla conjuntamente con el archivo zip, y agregar código al __main__.py para que incluya el directorio que contiene el módulo descomprimido en sys.path. En este caso, será necesario asegurarse de distribuir los binarios adecuados para la/s arquitectura/s a las que esté destinada la aplicación (y potencialmente elegir la versión correcta para agregar a sys.path en tiempo de ejecución, basándose en la máquina del usuario).

  2. Al distribuir ejecutables Windows tal como se describe más arriba, hay que asegurarse de que los usuarios tienen python3.dll en su PATH (lo cual no es una opción por defecto en el instalador), o bien empaquetar la aplicación con la distribución embedded.

  3. El lanzador que se sugiere más arriba, utiliza la API de incrustación de Python (Python embedding API). Esto significa que sys.executable será la aplicación, y no el intérprete Python convencional. El código y sus dependencias deben estar preparados para esta posibilidad. Por ejemplo, si la aplicación utiliza el módulo multiprocessing, necesitará invocar a multiprocessing.set_executable() para permitir que el módulo sepa dónde encontrar el intérprete Python estándar.

El formato de archivado Zip de aplicaciones Python

Python puede ejecutar archivadores zip que contengan un archivo __main__.py desde la versión 2.6. Para que sea ejecutada por Python, basta con que una aplicación de archivador sea un archivo zip estándar que contenga un archivo __main__.py, el cual será ejecutado como punto de entrada para la aplicación. Como es usual para cualquier script Python, el elemento padre del script (en este caso, el archivo zip), será ubicado en sys.path, por lo que pueden importarse otros módulos desde el archivo zip.

El formato de archivo zip, permite que se antepongan al archivo datos arbitrarios. El formato de aplicación zip utiliza esta capacidad para anteponer una línea «shebang» POSIX estándar al archivo (#!/ruta/al/interprete).

Formalmente, el Formato de archivado Zip de aplicaciones Python es:

  1. Una línea shebang opcional, conteniendo los caracteres``b”#!”`` seguidos por un nombre de intérprete, y luego un carácter de nueva línea (b'\n'). El nombre del intérprete puede ser cualquiera que sea aceptable para el procesamiento de shebang del Sistema Operativo, o el lanzador Python en Windows. El intérprete debería estar codificado en UTF-8 en Windows, y en sys.getfilesystemencoding() en POSIX.

  2. Los datos estándares de archivo zip, tal como se generan en el módulo zipfile. El contenido del archivo zip debe incluir un archivo llamado __main__.py (que debe estar en la «raíz» del archivo zip, es decir, no puede estar en un subdirectorio). El los datos del archivo zip pueden estar comprimidos o no.

Si un archivador de aplicación tiene una línea de shebang, puede tener el bit de ejecución activado en los sistemas POSIX, para permitir que sea ejecutado directamente.

No se requiere que las herramientas de este módulo sean las que se utilicen para crear archivadores de aplicación. El módulo es útil, pero cualquier archivo que esté en el formato descripto anteriormente es aceptable para Python, no importa cómo haya sido creado.