pkgutil — Utilidades de paquetes

Propósito:Agrega a la ruta de búsqueda del módulo un paquete específico y trabaja con los recursos incluidos en un paquete.

El módulo pkgutil incluye funciones para cambiar las reglas de importación de paquetes Python y para cargar recursos que no son de código desde archivos distribuidos dentro de un paquete.

Rutas de importación de paquetes

La función extend_path() se usa para modificar la ruta de búsqueda y cambiar la forma en que se importan los submódulos desde un paquete para que varios directorios diferentes se puedan combinar como si fueran uno. Esto se puede utilizar para anular versiones instaladas de paquetes con versiones de desarrollo, o para combinar módulos específicos de plataforma y compartidos en un solo espacio de nombres de paquete.

La forma más común de llamar a extend_path() es agregando dos líneas al __init__.py dentro del paquete.

import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)

extend_path() escanea sys.path en busca de directorios que incluyan un subdirectorio nombrado para el paquete dado como segundo argumento. La lista de directorios se combina con el valor de ruta pasado como primer argumento y devuelto como una lista única, adecuada para usar como ruta de importación de paquetes.

Un paquete de ejemplo llamado demopkg incluye dos archivos, __init__.py y shared.py. El archivo __init__.py en demopkg1 contiene declaraciones print para mostrar la ruta de búsqueda antes y después de que se modifique, para resaltar la diferencia.

demopkg1/__init__.py
import pkgutil
import pprint

print('demopkg1.__path__ before:')
pprint.pprint(__path__)
print()

__path__ = pkgutil.extend_path(__path__, __name__)

print('demopkg1.__path__ after:')
pprint.pprint(__path__)
print()

El directorio extension, con características adicionales para demopkg, contiene tres archivos fuente más. Hay un __init__.py en cada nivel de directorio y un not_shared.py.

$ find extension -name '*.py'

extension/__init__.py
extension/demopkg1/__init__.py
extension/demopkg1/not_shared.py

Este sencillo programa de prueba importa el paquete demopkg1.

pkgutil_extend_path.py
import demopkg1
print('demopkg1           :', demopkg1.__file__)

try:
    import demopkg1.shared
except Exception as err:
    print('demopkg1.shared    : Not found ({})'.format(err))
else:
    print('demopkg1.shared    :', demopkg1.shared.__file__)

try:
    import demopkg1.not_shared
except Exception as err:
    print('demopkg1.not_shared: Not found ({})'.format(err))
else:
    print('demopkg1.not_shared:', demopkg1.not_shared.__file__)

Cuando este programa de prueba se ejecuta directamente desde la línea de comandos, no se encuentra el módulo not_shared.

Nota

Las rutas completas del sistema de archivos en estos ejemplos se han acortado para enfatizar las partes que cambian.

$ python3 pkgutil_extend_path.py

demopkg1.__path__ before:
['.../demopkg1']

demopkg1.__path__ after:
['.../demopkg1']

demopkg1           : .../demopkg1/__init__.py
demopkg1.shared    : .../demopkg1/shared.py
demopkg1.not_shared: Not found (No module named 'demopkg1.not_sh
ared')

Sin embargo, si el directorio extension se agrega a PYTHONPATH y el programa se ejecuta nuevamente, se producen resultados diferentes.

$ PYTHONPATH=extension python3 pkgutil_extend_path.py

demopkg1.__path__ before:
['.../demopkg1']

demopkg1.__path__ after:
['.../demopkg1',
 '.../extension/demopkg1']

demopkg1           : .../demopkg1/__init__.py
demopkg1.shared    : .../demopkg1/shared.py
demopkg1.not_shared: .../extension/demopkg1/not_shared.py

La versión de demopkg1 dentro del directorio extension se ha agregado a la ruta de búsqueda, por lo que el módulo not_shared se encuentra allí.

Ampliar la ruta de esta manera es útil para combinar versiones específicas de plataforma de paquetes con paquetes comunes, especialmente si las versiones específicas de plataforma incluyen módulos de extensión C.

Versiones de desarrollo de paquetes

Mientras se desarrollan mejoras para un proyecto, es común tener que probar los cambios en un paquete instalado. Reemplazar la copia instalada con una versión de desarrollo puede ser una mala idea, ya que no es necesariamente correcta y es probable que otras herramientas del sistema dependan del paquete instalado.

Se podría configurar una copia completamente separada del paquete en un entorno de desarrollo utilizando virtualenv o venv, pero para pequeñas modificaciones, la sobrecarga de configurar un entorno virtual con todas las dependencias puede ser excesiva.

Otra opción es usar pkgutil para modificar la ruta de búsqueda del módulo para los módulos que pertenecen al paquete en desarrollo. En este caso, sin embargo, la ruta debe invertirse para que la versión de desarrollo anule la versión instalada.

Dado un paquete demopkg2 que contiene un __init__.py y overloaded.py, con la función en desarrollo ubicada en demopkg2/overloaded.py. La versión instalada contiene

demopkg2/overloaded.py

def func():
    print('This is the installed version of func().')

y demopkg2/__init__.py contiene

demopkg2/__init__.py
import pkgutil

__path__ = pkgutil.extend_path(__path__, __name__)
__path__.reverse()

reverse() se usa para garantizar que los directorios agregados a la ruta de búsqueda por pkgutil se analicen en busca de importaciones antes de la ubicación predeterminada.

Este programa importa demopkg2.overloaded y llama a func().

pkgutil_devel.py
import demopkg2
print('demopkg2           :', demopkg2.__file__)

import demopkg2.overloaded
print('demopkg2.overloaded:', demopkg2.overloaded.__file__)

print()
demopkg2.overloaded.func()

Ejecutarlo sin ningún tratamiento de ruta especial produce la salida de la versión instalada de func().

$ python3 pkgutil_devel.py

demopkg2           : .../demopkg2/__init__.py
demopkg2.overloaded: .../demopkg2/overloaded.py

This is the installed version of func().

Un directorio de desarrollo que contiene

$ find develop/demopkg2 -name '*.py'

develop/demopkg2/__init__.py
develop/demopkg2/overloaded.py

y una versión modificada de overloaded

develop/demopkg2/overloaded.py

def func():
    print('This is the development version of func().')

se cargará cuando el programa de prueba se ejecute con el directorio develop en la ruta de búsqueda.

$ PYTHONPATH=develop python3 pkgutil_devel.py

demopkg2           : .../demopkg2/__init__.py
demopkg2.overloaded: .../develop/demopkg2/overloaded.py

This is the development version of func().

Administrar rutas con archivos PKG

El primer ejemplo ilustra cómo extender la ruta de búsqueda utilizando directorios adicionales incluidos en PYTHONPATH. También es posible agregar a la ruta de búsqueda utilizando archivos *.pkg que contienen nombres de directorio. Los archivos PKG son similares a los archivos PTH utilizados por el módulo site. Pueden contener nombres de directorio, uno por línea, que se agregarán a la ruta de búsqueda del paquete.

Otra forma de estructurar las porciones específicas de la plataforma de la aplicación del primer ejemplo es usar un directorio separado para cada sistema operativo e incluir un archivo .pkg para extender la ruta de búsqueda.

Este ejemplo usa los mismos archivos de demopkg1, y también incluye los siguientes archivos.

$ find os_* -type f

os_one/demopkg1/__init__.py
os_one/demopkg1/not_shared.py
os_one/demopkg1.pkg
os_two/demopkg1/__init__.py
os_two/demopkg1/not_shared.py
os_two/demopkg1.pkg

Los archivos PKG se denominan demopkg1.pkg para que coincidan con el paquete que se está extendiendo. Ambos contienen una línea.

demopkg

Este programa de demostración muestra la versión del módulo que se está importando.

pkgutil_os_specific.py
import demopkg1
print('demopkg1:', demopkg1.__file__)

import demopkg1.shared
print('demopkg1.shared:', demopkg1.shared.__file__)

import demopkg1.not_shared
print('demopkg1.not_shared:', demopkg1.not_shared.__file__)

Se puede usar una secuencia de comandos simple para cambiar entre los dos paquetes.

with_os.sh
#!/bin/sh

export PYTHONPATH=os_${1}
echo "PYTHONPATH=$PYTHONPATH"
echo

python3 pkgutil_os_specific.py

Cuando se ejecuta con "one" o "two" como argumentos, la ruta se ajusta.

$ ./with_os.sh one

PYTHONPATH=os_one

demopkg1.__path__ before:
['.../demopkg1']

demopkg1.__path__ after:
['.../demopkg1',
 '.../os_one/demopkg1',
 'demopkg']

demopkg1: .../demopkg1/__init__.py
demopkg1.shared: .../demopkg1/shared.py
demopkg1.not_shared: .../os_one/demopkg1/not_shared.py

$ ./with_os.sh two

PYTHONPATH=os_two

demopkg1.__path__ before:
['.../demopkg1']

demopkg1.__path__ after:
['.../demopkg1',
 '.../os_two/demopkg1',
 'demopkg']

demopkg1: .../demopkg1/__init__.py
demopkg1.shared: .../demopkg1/shared.py
demopkg1.not_shared: .../os_two/demopkg1/not_shared.py

Los archivos PKG pueden aparecer en cualquier lugar de la ruta de búsqueda normal, por lo que un solo archivo PKG en el directorio de trabajo actual también podría usarse para incluir un árbol de desarrollo.

Paquetes anidados

Para paquetes anidados, solo es necesario modificar la ruta del paquete de nivel superior. Por ejemplo, con esta estructura de directorios

$ find nested -name '*.py'

nested/__init__.py
nested/second/__init__.py
nested/second/deep.py
nested/shallow.py

Donde nested/__init__.py contiene

nested/__init__.py
import pkgutil

__path__ = pkgutil.extend_path(__path__, __name__)
__path__.reverse()

y el árbol de desarrollo

$ find develop/nested -name '*.py'

develop/nested/__init__.py
develop/nested/second/__init__.py
develop/nested/second/deep.py
develop/nested/shallow.py

Los módulos shallow y deep contienen una función simple para imprimir un mensaje que indica si provienen o no de la versión instalada o de desarrollo.

Este programa de prueba ejercita los nuevos paquetes.

pkgutil_nested.py
import nested

import nested.shallow
print('nested.shallow:', nested.shallow.__file__)
nested.shallow.func()

print()
import nested.second.deep
print('nested.second.deep:', nested.second.deep.__file__)
nested.second.deep.func()

Cuando se ejecuta pkgutil_nested.py sin ninguna manipulación de ruta, se utiliza la versión instalada de ambos módulos.

$ python3 pkgutil_nested.py

nested.shallow: .../nested/shallow.py
This func() comes from the installed version of nested.shallow

nested.second.deep: .../nested/second/deep.py
This func() comes from the installed version of nested.second.de
ep

Cuando se agrega el directorio develop a la ruta, la versión de desarrollo de ambas funciones anula las versiones instaladas.

$ PYTHONPATH=develop python3 pkgutil_nested.py

nested.shallow: .../develop/nested/shallow.py
This func() comes from the development version of nested.shallow

nested.second.deep: .../develop/nested/second/deep.py
This func() comes from the development version of nested.second.
deep

Datos del paquete

Además del código, los paquetes de Python pueden contener archivos de datos como plantillas, archivos de configuración predeterminados, imágenes y otros archivos de soporte utilizados por el código en el paquete. La función get_data() da acceso a los datos en los archivos de forma independiente del formato, por lo que no importa si el paquete se distribuye como un EGG, parte de un archivo binario congelado o archivos regulares en el archivo sistema.

Con un paquete pkgwithdata que contiene un directorio de templates

$ find pkgwithdata -type f

pkgwithdata/__init__.py
pkgwithdata/templates/base.html

El archivo pkgwithdata/templates/base.html contiene una plantilla HTML simple.

pkgwithdata/templates/base.html
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>PyMOTW Template</title>
</head>

<body>
<h1>Example Template</h1>

<p>This is a sample data file.</p>

</body>
</html>

Este programa usa get_data() para recuperar el contenido de la plantilla e imprimirlo.

pkgutil_get_data.py
import pkgutil

template = pkgutil.get_data('pkgwithdata', 'templates/base.html')
print(template.decode('utf-8'))

Los argumentos de get_data() son el nombre punteado del paquete y un nombre de archivo relativo a la parte superior del paquete. El valor de retorno es una secuencia de bytes, por lo que se decodifica desde UTF-8 antes de imprimirse.

$ python3 pkgutil_get_data.py

<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>PyMOTW Template</title>
</head>

<body>
<h1>Example Template</h1>

<p>This is a sample data file.</p>

</body>
</html>

get_data() es independiente del formato de distribución porque utiliza los ganchos de importación definidos en PEP 302 para acceder al contenido del paquete. Se puede utilizar cualquier cargador que proporcione los ganchos, incluido el importador del archivo ZIP en zipfile.

pkgutil_get_data_zip.py
import pkgutil
import zipfile
import sys

# Create a ZIP file with code from the current directory
# and the template using a name that does not appear on the
# local filesystem.
with zipfile.PyZipFile('pkgwithdatainzip.zip', mode='w') as zf:
    zf.writepy('.')
    zf.write('pkgwithdata/templates/base.html',
             'pkgwithdata/templates/fromzip.html',
             )

# Add the ZIP file to the import path.
sys.path.insert(0, 'pkgwithdatainzip.zip')

# Import pkgwithdata to show that it comes from the ZIP archive.
import pkgwithdata
print('Loading pkgwithdata from', pkgwithdata.__file__)

# Print the template body
print('\nTemplate:')
data = pkgutil.get_data('pkgwithdata', 'templates/fromzip.html')
print(data.decode('utf-8'))

Este ejemplo usa PyZipFile.writepy() para crear un archivo ZIP que contiene una copia del paquete pkgwithdata, incluida una versión renombrada del archivo de plantilla. Luego agrega el archivo ZIP a la ruta de importación antes de usar pkgutil para cargar la plantilla e imprimirla. Consulta la discusión de zipfile para obtener más detalles sobre el uso de writepy().

$ python3 pkgutil_get_data_zip.py

Loading pkgwithdata from
pkgwithdatainzip.zip/pkgwithdata/__init__.pyc

Template:
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML//EN">
<html> <head>
<title>PyMOTW Template</title>
</head>

<body>
<h1>Example Template</h1>

<p>This is a sample data file.</p>

</body>
</html>

Ver también