pkgutil - Utilità per Pacchetti

Scopo: Aggiunge al percorso di ricerca dei moduli quello di uno specifico pacchetto e lavora con risorse incluse in un pacchetto.

Il modulo pkgutil include funzioni per modificare le regole di importazione dei pacchetti Python e per caricare risorse diverse dal codice da file distribuiti all'interno di un pacchetto.

Percorsi di Importazione Pacchetti

La funzione extend_path() è usata per modificare il percorso di ricerca e modificare il modo nel quale i sotto moduli sono importati dall'interno di un pacchetto in modo che diverse directory differenti possano essere combinate come se fossero una sola. Questa caratteristica può essere utilizzata per sovrascrivere versioni di pacchetti installati con versioni di sviluppo, oppure per combinare moduli specifici per la piattaforma e moduli condivisi in un singolo spazio dei nomi per un pacchetto.

Il modo più comune per chiamare extend_path() è aggiungendo due righe al file __init__.py all'interno del pacchetto.

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

extend_path() scorre sys.path alla ricerca di directory che includano una certa sottodirectory per il pacchetto fornita come secondo argomento. L'elenco di directory viene combinato con il valore del percorso passato come primo argomento e viene ritornato come una singola lista, adatta all'uso come percorso di importazione del pacchetto.

Un pacchetto di esempio chiamato demopkg include due file, __init__.py e shared.py. Il file __init__.py in demopkg1 contiene istruzioni print per mostrare il percorso di ricerca prima e dopo che è stato modificato, per evidenziarne la differenza.

# 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()

La directory extension, con caratteristiche aggiunte per demopkg, contiene altri tre file sorgente. C'è un file __init__.py a ogni livello di directory e un not_shared.py.

$ find extension -name '*.py'

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

Questo semplice programma di test importa il pacchetto demopkg1.

# pkgutil_extend_path.py

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

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

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

Quando il programma di test viene eseguito direttamente da riga di comando, il modulo not_shared.py non viene trovato.

In questi esempi i percorsi completi del file system sono stati abbreviati per enfatizzare le parti che cambiano.
$ python3 pkgutil_extend_path.py

demopkg1.__path__ before:
['/dati/dev/python/pymotw3restyling/dumpscripts/demopkg1']

demopkg1.__path__ after:
['/dati/dev/python/pymotw3restyling/dumpscripts/demopkg1']

demopkg1           : /dati/dev/python/pymotw3restyling/dumpscripts/demopkg1/__init__.py
demopkg1.shared    : /dati/dev/python/pymotw3restyling/dumpscripts/demopkg1/shared.py
demopkg1.not_shared: Non trovato (No module named 'demopkg1.not_shared')

Tuttavia, se la directory extension viene aggiunta a PYTHONPATH e il programma viene rieseguito, si hanno risultati differenti.

$ PYTHONPATH=extension python3 pkgutil_extend_path.py

demopkg1.__path__ before:
['/dati/dev/python/pymotw3restyling/dumpscripts/demopkg1']

demopkg1.__path__ after:
['/dati/dev/python/pymotw3restyling/dumpscripts/demopkg1',
 '/dati/dev/python/pymotw3restyling/dumpscripts/extension/demopkg1']

demopkg1           : /dati/dev/python/pymotw3restyling/dumpscripts/demopkg1/__init__.py
demopkg1.shared    : /dati/dev/python/pymotw3restyling/dumpscripts/demopkg1/shared.py
demopkg1.not_shared: /dati/dev/python/pymotw3restyling/dumpscripts/extension/demopkg1/not_shared.py

La versione di demopkg1 all'interno della directory extension è stata aggiunta al percorso di ricerca, quindi not_shared.py viene trovato.

L'estensione del percorso è in questo modo utile per combinare versioni di pacchetti specifiche per la piattaforma con pacchetti comuni, specialmente se le versioni specifiche per la piattaforma includono moduli di estensione C.

Versioni di Sviluppo di Pacchetti

Nella fase di sviluppo di migliorie per un progetto, è normale la necessità di dover verificare modifiche a un pacchetto installato. Sostituire la copia installata con una versione di sviluppo potrebbe essere una cattiva idea, visto che potrebbe non essere corretta e altri strumenti nel sistema potrebbero dipendere dal pacchetto installato.

Una copia completamente separata del pacchetto potrebbe essere configurata in un ambiente di sviluppo usando virtualenv oppure venv, ma per piccole modifiche potrebbe essere eccessivo l'impegno di impostare un ambiente virtuale con tutte le dipendenze.

Un'altra opzione è usare pkgutil per modificare il percorso di ricerca dei moduli per i moduli che appartengono al pacchetto in sviluppo. In questo caso, tuttavia, il percorso deve essere invertito in modo che il percorso della versione di sviluppo venga esaminato prima di quello della versione installata.

In questo caso un pacchetto demopkg2 contiene un file __init__.py e overloaded.py, con la funzione in fase di sviluppo collocata in demopkg2/overloaded.py.

# develop/demopkg2/overloaded.py


def func():
    print("Questa e' la versione installata di func().")

e demopkg2/__init__.py contiene:

# demopkg2/__init__.py

import pkgutil

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

reverse() viene utilizzato per assicurarsi che qualunque directory aggiunta al percorso di ricerca da pkgutil venga esaminata per trovare importazioni relative al percorso di sviluppo prima della importazioni della versione predefinita.

Il programma importa demopkg2.overloaded e chiama func().

# pkgutil_devel.py

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

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

print()
demopkg2.overloaded.func()

Eseguendolo senza particolari trattamenti del percorso produce il risultato dalla versione installata di func().

$ python3 pkgutil_devel.py

demopkg2           : /dati/dev/python/pymotw3restyling/dumpscripts/demopkg2/__init__.py
demopkg2.overloaded: /dati/dev/python/pymotw3restyling/dumpscripts/demopkg2/overloaded.py

Questa e' la versione installata di func().

La directory di sviluppo contiene:

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

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

All'interno di overloaded.py si trova la funzione modificata.

# develop/demopkg2/overloaded.py


def func():
    print("Questa e' la versione di sviluppo di func().")

che verrà caricata quando il programma di test viene eseguito con la directory develop nel percorso di ricerca.

$ PYTHONPATH=develop python3 pkgutil_devel.py

demopkg2           : /dati/dev/python/pymotw3restyling/dumpscripts/demopkg2/__init__.py
demopkg2.overloaded: /dati/dev/python/pymotw3restyling/dumpscripts/develop/demopkg2/overloaded.py

Questa e' la versione di sviluppo di func().

Gestire Percorsi con File PKG

Il primo esempio illustrava come estendere il percorso di ricerca usando directory extra incluse in PYTHONPATH. E' anche possibile aggiungere elementi al percorso di ricerca usando dei file *.pkg che contengono nomi di directory. I file PKG sono simili ai file PTH usati dal modulo site. Possono contenere nomi di directory, uno per riga, da aggiungere al percorso di ricerca per il pacchetto.

Un altro modo per strutturare porzioni specfiche per la piattaforma dell'applicazione dal primo esempio è usare una directory separata per ogni sistema operativo, e includere un file .pkg per estendere il percorso di ricerca.

Questo esempio usa gli stessi file in demopkg1, e include anche i seguenti file:

$ find so_* -type f

so_due/demopkg1/__init__.py
so_due/demopkg1/not_shared.py
so_due/demopkg1/__pycache__/not_shared.cpython-36.pyc
so_due/demopkg1/__pycache__/not_shared.cpython-38.pyc
so_due/demopkg1.pkg
so_uno/demopkg1/__init__.py
so_uno/demopkg1/not_shared.py
so_uno/demopkg1/__pycache__/not_shared.cpython-36.pyc
so_uno/demopkg1/__pycache__/not_shared.cpython-38.pyc
so_uno/demopkg1.pkg

I file PKG sono chiamati demopkg1.pkg per farli corrispondere con il pacchetto che viene esteso. Entrambi contengono questa riga:

demopkg

Questo programma mostra le versioni dei moduli importati.

# 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__)

Un semplice script può essere usato per alternare i due pacchetti.

# with_os.sh

#!/bin/sh

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

python3 pkgutil_os_specific.py

Quando viene eseguito con uno o due come argomento, il percorso viene modificato.

$ ./with_os.sh uno

PYTHONPATH=so_uno

demopkg1.__path__ before:
['/dati/dev/python/pymotw3restyling/dumpscripts/demopkg1']

demopkg1.__path__ after:
['/dati/dev/python/pymotw3restyling/dumpscripts/demopkg1',
 '/dati/dev/python/pymotw3restyling/dumpscripts/so_uno/demopkg1',
 'demopkg']

demopkg1: /dati/dev/python/pymotw3restyling/dumpscripts/demopkg1/__init__.py
demopkg1.shared: /dati/dev/python/pymotw3restyling/dumpscripts/demopkg1/shared.py
demopkg1.not_shared: /dati/dev/python/pymotw3restyling/dumpscripts/so_uno/demopkg1/not_shared.py
$ ./with_os.sh due

PYTHONPATH=so_due

demopkg1.__path__ before:
['/dati/dev/python/pymotw3restyling/dumpscripts/demopkg1']

demopkg1.__path__ after:
['/dati/dev/python/pymotw3restyling/dumpscripts/demopkg1',
 '/dati/dev/python/pymotw3restyling/dumpscripts/so_due/demopkg1',
 'demopkg']

demopkg1: /dati/dev/python/pymotw3restyling/dumpscripts/demopkg1/__init__.py
demopkg1.shared: /dati/dev/python/pymotw3restyling/dumpscripts/demopkg1/shared.py
demopkg1.not_shared: /dati/dev/python/pymotw3restyling/dumpscripts/so_due/demopkg1/not_shared.py

I file PKG possono apparire in qualunque parte del normale percorso di ricerca, quindi anche un singolo file PKG nella directory di lavoro corrente potrebbe essere usato per includere un ramo di sviluppo.

Pacchetti Annidati

Per pacchetti annidati, è solamente necessario modificare il percorso del pacchetto di livello superiore. Ad esempio, con questa struttura di directory.

$ find nested -name '*.py'

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

Dove nested/__init__.py contiene.

# nested/__Init__.py

import pkgutil

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

ed una branca di sviluppo tipo.

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

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

Entrambi i moduli shallow e deep contengono una semplice funzione che stampa un messaggio che indica se provengono o meno dalla versione installata oppure quella di sviluppo.

Questo programma di test si avvale dei nuovi pacchetti.

# 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()

Quando viene eseguito pkgutil_nested.py senza alcuna manipolazione del percorso, vengono usate le versioni installate di entrambi i moduli.

$ python3 pkgutil_nested.py

nested.shallow: /dati/dev/python/pymotw3restyling/dumpscripts/nested/shallow.py
Questa func() proviene dalla versione installata di nested.shallow

nested.second.deep: /dati/dev/python/pymotw3restyling/dumpscripts/nested/second/deep.py
Questa func() proviene dalla versione installata di nested.second.deep

Quando viene aggiunta la directory develop al percorso, le versioni di sviluppo di entrambi i metodi vengono eseguite in luogo di quelle installate.

$ PYTHONPATH=develop python3 pkgutil_nested.py

nested.shallow: /dati/dev/python/pymotw3restyling/dumpscripts/develop/nested/shallow.py
Questa func() proviene dalla versione di sviluppo di nested.shallow

nested.second.deep: /dati/dev/python/pymotw3restyling/dumpscripts/develop/nested/second/deep.py
Questa func() proviene dalla versione di sviluppo di nested.second.deep

Pacchetti con Dati

Oltre al codice, i pacchetti Python possono contenere file dati tipo template, file di configurazione predefiniti, immagini e altri file di supporto usati dal codice nel pacchetto. La funzione get_date() fornisce accesso ai dati nei file in modo agnostico, quindi non importa se il pacchetto viene distribuito come EGG, parte di un binario fissato, o normali file nel sistema.

Con un pacchetto pkgwithdata che contiene una directory templates.

$ find pkgwithdata -type f

pkgwithdata/templates/fromzip.html
pkgwithdata/templates/base.html
pkgwithdata/__init__.py
pkgwithdata/__pycache__/__init__.cpython-36.pyc

Il file pkgwithdata/templates/base.html contiene un semplice template HTML.

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

<body>
<h1>Template di esempio</h1>

<p>Questo e' un file di dati di esempio..</p>

</body>
</html>

Il programma usa get_data() per ottenere il contenuto del template e lo stampa.

# pkgutil_get_data.py

import pkgutil

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

Gli argomenti di get_data() sono il nome del pacchetto e un nome di file relativo al livello superiore del pacchetto. Il valore di ritorno è una sequenza di byte, quindi deve essere decodificato in UTF-8 prima della stampa.

$ python3 pkgutil_get_data.py

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

<body>
<h1>Template di esempio</h1>

<p>Questo e' un file di dati di esempio..</p>

</body>
</html>

get_data() è in formato agnostico rispetto alla distribuzione in quanto usa gli agganci di importazione definiti per accedere ai contenuti di un pacchetto definiti in PEP 302. E' possibile usare un qualunque caricatore che fornisca gli agganci, compreso l'importatore di archivi ZIP in zipfile.

# pkgutil_get_data_zip.py

import pkgutil
import zipfile
import sys

# Crea un file ZIP con il codice dalla directory corrente
# ed il template usando un nome che non appare nel filesystem.locale
with zipfile.PyZipFile('pkgwithdatainzip.zip', mode='w') as zf:
    zf.writepy('pkgwithdata/.')
    zf.write('pkgwithdata/templates/base.html',
             'pkgwithdata/templates/fromzip.html',
             )

# Aggiunge il file ZIP al percorso di importazione.
sys.path.insert(0, 'pkgwithdatainzip.zip')

# Importa pkgwithdata per mostrare che proviene dall'archivio  ZIP.
import pkgwithdata
print('Loading pkgwithdata from', pkgwithdata.__file__)

# Stampa il contenuto del template.
print('\nTemplate:')
data = pkgutil.get_data('pkgwithdata', 'templates/fromzip.html')
print(data.decode('utf-8'))

Questo esempio usa PyZipFile.writepy() per creare un archivio ZIP che contiene una copia del pacchetto pkgwithdata, inclusa una versione rinominata del file template. Quindi aggiunge l'archivio ZIP al percorso di importazione prima di usare pkgutil per caricare il template e stamparlo. Si faccia riferimento a zipfile per maggiori dettagli sull'uso di writepy().

$ python3 pkgutil_get_data_zip.py

$ python3 pkgutil_get_data_zip.py
Loading pkgwithdata from .../pkgwithdata/__init__.py

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

<body>
<h1>Template di esempio</h1>

<p>Questo e' un file di dati di esempio..</p>

</body>
</html>

Vedere anche:

pkgutil
La documentazione della libreria standard per questo modulo.
virtualenv
Lo script di ambiente virtuale di Ian Bicking
distutils
Strumenti per gestione pacchetti dalla libreria standard di Python
setuptools
Strumenti di gestione pacchetti della prossima generazione.
PEP 302
Agganci di importazione.
zipfile
Crea archivi ZIP importabili
zipimport
Importatore di pacchetti in archivi ZIP.