Parmi les nombreux problèmes, la question suivante se pose souvent : comment je livre proprement un fichier de données fourni avec mon package Python ?
Le problème est double:
On pourrait croire qu’un truc aussi basique serait simple, mais non.
Ca ne l’est pas car un package Python peut être livré sous forme de code source, ou d’un binaire, ou d’une archive eggs/whl/pyz… Et en prime il peut être transformé par les packageurs des repos debian, centos, homebrew, etc.
Mais skippy a la solution à tous vos soucis !
A la racine de votre projet, il faut un fichier MANIFEST.IN
bien rempli et include_package_data
sur True
dans setup.py
. On a un article là dessus, ça tombe bien.
N’utilisez pas package_data
, ça ne marche pas à tous les coups.
Si vous êtes pressés, copiez ça dans un fichier utils.py:
import os
import io
from types import ModuleType
class RessourceError(EnvironmentError):
def __init__(self, msg, errors):
super().__init__(msg)
self.errors = errors
def binary_resource_stream(resource, locations):
""" Return a resource from this path or package """
# convert
errors = []
# If location is a string or a module, we wrap it in a tuple
if isinstance(locations, (str, ModuleType)):
locations = (locations,)
for location in locations:
# Assume location is a module and try to load it using pkg_resource
try:
import pkg_resources
module_name = getattr(location, '__name__', location)
return pkg_resources.resource_stream(module_name, resource)
except (ImportError, EnvironmentError) as e:
errors.append(e)
# Falling back to load it from path.
try:
# location is a module
module_path = __import__(location).__file__
parent_dir = os.path_dirname(module_path)
except (AttributeError, ImportError):
# location is a dir path
parent_dir = os.path.realpath(location)
# Now we got a resource full path. Just open it as a regular file.
canonical_path = os.path.join(parent_dir, resource)
try:
return open(os.path.join(canonical_path), mode="rb")
except EnvironmentError as e:
errors.append(e)
msg = ('Unable to find resource "%s" in "%s". '
'Inspect RessourceError.errors for list of encountered erros.')
raise RessourceError(msg % (resource, locations), errors)
def text_resource_stream(path, locations, encoding="utf8", errors=None,
newline=None, line_buffering=False):
""" Return a resource from this path or package. Transparently decode the stream. """
stream = binary_resource_stream(path, locations)
return io.TextIOWrapper(stream, encoding, errors, newline, line_buffering)
Et faites:
from utils import binary_resource_stream, text_resource_stream
data_stream = binary_resource_stream('./chemin/relatif', package) # pour du binaire
data = data_stream.read()
txt_stream = text_resource_stream('./chemin/relatif', package) # pour du texte text = txt_stream.read()
Par exemple:
image_data = binary_resource_stream('./static/img/logo.png', "super_package").read()
text = text_resource_stream('./config/default.ini', "super_package", encoding="cp850").read()
Si vous n’êtes pas pressés, voilà toute l’histoire…
A la base, on fait généralement un truc du genre:
PROJECT_DIR = os.path.dirname(os.path.realpath(__file__)) # ou pathlib/path.py
data = open(os.path.join(PROJECT_DIR, chemin_relatif)).read())
Mais ça ne marche pas dans le cas d’une installation binaire, zippée, trafiquée, en plein questionnement existentiel après avoir regardé un film des frères Cohen, etc.
La manière la plus sûre de le faire est:
import pkg_resources
data = pkg_resources.resource_stream('nom_de_votre_module', chemin_relatif).read()
Ça va marcher tant que votre package est importable. On se fout d’où il est, de sa forme, Python se démerde.
Mais ça pose plusieurs autres problèmes:
pkg_resources
fait parti de setuptools, qui n’est pas forcément installé sur votre système. En effet, malgré l’existence de ensure_pip
depuis la 3.4, beaucoup de distributions n’installent ni pip, ni setuptools par défaut et en font des paquets séparés.data
sera forcément de type bytes
. Il faut le décoder manuellement derrière.pkg_resources
ne fonctionnera pas puisque par essence il utilise le nom du package pour trouver la ressource.try
/except
.pkgutil
qui est bien installé partout, mais ça load tout en mémoire d’un coup. Si vous avez un XML de 10Mo à charger ça fait mal au cul.pkg_resource
est aussi claire que l’anus de la mère du premier commentateur de cet article. On va voir qui lit l’article en entier là…Le snippet fourni corrige ces problèmes en gérant les cas tordus pour vous. Moi je l’utilise souvent en faisant:
with text_resource_stream('./chemin/relatif', ['package_name', 'chemin_vers_au_cas_ou_c_est_pas_un_package']) as f:
print('boom !', f.read())
Je devrais peut-être rajouter ça dans batbelt et minibelt…
Ça, c’est pour lire les ressources fournies. Mais si vous devez écrire des trucs, il vous faut un dossier de travail.
Si c’est pour un boulot jetable, faites-le dans un dossier temporaire. Le module tempfile est votre ami.
Si c’est pour un boulot persistant, trouvez le dossier approprié à votre (fichier de config, fichier de log, etc) et travaillez dedans. path.py a des méthodes dédiées à cela (un des nombreuses raisons qui rendent ce module meilleur que pathlib
).
La solution est de lancer :
sudo pip install setuptools --no-use-wheel --upgrade
Et on peut réutiliser pip sans problème.
Zarb.
]]>pip install
par là. "Pour installer cette lib, il vous suffit de faire pip install".
Mais merde, c'est quoi pip
?]]>Pip install
par-ci, pip install
par là. “Pour installer cette lib, il vous suffit de faire pip install”.
Mais merde, c’est quoi pip
?
La beauté avec Python, c’est qu’on peut prendre une lib, la balancer dans le répertoire courant, et l’importer. Rien à faire, ça marche tout seul.
Mais.
Car oui, il y a toujours un mais (souvent après le mois de mars).
Quand il faut mettre à jour ses libs, c’est chiant. Quand il faut recréer un environnement complet en prod, c’est chiant. Quand il faut trouver la dernière version, avec le bon site Web, et le lien de téléchargement, c’est chiant. Quand on veut installer une lib automatiquement, vous l’avez deviné, c’est chiant.
Mais surtout, quand on a une lib qui a des parties en C à compiler comme les libs de crypto, d’accès à la base de données, de traitement XML, de parsing ou de sérialisation, de calculs scientifiques, etc. ça ne marche tout simplement pas.
Là, il y a deux écoles. Les mecs qui utilisent les packages *.exe ou *.deb precompilés et qui se retrouvent avec d’autres problèmes.
Et les mecs qui utilisent setuptools (et qui se retrouvent avec encore d’autres problèmes, mais c’est mieux parce que je le dis).
setuptools est une lib et un outil de distribution de code Python, et elle permet notament d’installer des libs externes automatiquement en les téléchargeant depuis le grand InterBoule.
Comme gem install
sous ruby, npm
pour NodeJs, Python possède sa commande d’installation appelée easy_install
, qui va piocher dans le site Web pypi.python.org, un équivalent de Cpan de Perl ou des channels pear pour PHP.
Bien entendu, ça serait trop simple sinon, Python ne vient pas par défaut installé avec cet outil pourtant indispensable.
Sous Ubuntu c’est facile:
sudo apt-get install python-setuptools
Et il existe un paquet similaire sur toutes les distribs.
Sous windows, c’est un exe à installer. Choisissez la bonne version selon votre version de Python (python --version
). Généralement, la 2.7.
Sous mac, il faut télécharger l’egg correspondant à sa version puis:
sh setuptools-votre_version.egg --prefix=~
Et vous avez alors accès à la commande magique, easy_install
. Par exemple, pour installer Django:
easy_install --user django
(ou easy_install django
en mode administrateur – ou avec sudo
– pour l’installer pour tout le système et pas juste l’utilisateur courant)
Il faut bien entendu être connecté à internet pour que ça marche. Notez par ailleurs que la recherche est insensible à la casse, et que tout underscore sera automatiquement remplacé par un tiret.
Sous Windows, si la commande n’est pas trouvée, il vous faudra rajouter le répertoire qui contient la commande easy_install
(le plus souvent C:\Python27\Scripts) à la main dans le PATH.
easy_install
? Oubliez-le !Vous n’utiliserez normalement easy_install
que pour une seule chose: installer pip
.
easy_install --user pip
pip
est un easy_install
en plus court à taper, plus puissant, plus souple, plus mieux.
Il s’utilise comme easy_install
:
pip install --user bottle
(ou sudo pip install bottle
, ou en root, mais je sauterai ce genre de détails pour la suite)
Grâce à pip
, vous allez avoir accès à des milliers de libs Python en quelques frappes, et donc pouvoir beaucoup plus facilement expérimenter avec des nouveaux bidules.
Mais pip
va plus loin qu’easy_install
, et permet carrément de gérer toutes les dépendances de son projet.
pip
que easy_install
ne fait pasDéjà, pip
télécharge toutes les dépendances avant de lancer l’installation, ce qui évite une installation partielle.
Mais surtout, pip
ajoute des opérations qu’on pourrait attendre de base, notamment, la désinstallation :-)
pip uninstall bottle
Évidement, on peut choisir d’installer une version particulière:
pip install bottle==0.9
Et mettre à jour une lib à la dernière version :
pip install bottle --upgrade
Ou downgrader vers une version précédente:
pip install bottle==0.9 --upgrade
Cependant, le plus intéressant avec pip
, c’est sa capacité à recréer tout un environnement de développement en une commande:
$ pip freeze
Axiom==0.6.0
BeautifulSoup==3.2.0
Brlapi==0.5.6
BzrTools==2.5.0
CherryPy==3.2.2
Coherence==0.6.6.2
[...]
pip freeze
liste toutes les librairies installées et leurs versions. On peut ensuite les réinstaller sur une autre machine facilement:
pip freeze > requirements.txt
Et sur l’autre machine:
pip install -r requirements.txt
On est garanti d’avoir les mêmes libs, avec les mêmes versions. Ca marche de la même manière sous tous les OS, et comme des milliers de libs ne sont pas disponibles en paquets *.deb, *.rpm ou *.exe, c’est extrêmement pratique.
Si vous vous attendez à être privé de connexion internet ou juste pour accélérer le procédé, pip
permet de créer un bundle, c’est à dire un gros zip qui contient toutes les dépendances et qui peut être installé indépendamment :
pip bundle nom_du_projet.pybundle -r requirements.txt
Et sur l’autre machine:
pip install nom_du_projet.pybundle
pip
Si vous installez beaucoup de fois les mêmes paquets, par exemple dans de nombreux virtualenvs j’installe toujours ipython + ipdb + requests + clize + peewee, les télécharger à chaque fois est une perte de temps. On peut demander à pip
de mettre en cache les paquets déjà installés, pour de futures installations:
export PIP_DOWNLOAD_CACHE='~/.pip/cache';
dans le .bashrc
Pour les windowsiens, je ne sais pas du tout où mettre ça…
Sachez cependant que le cache ne dispense pas d’être connecté à Internet.
Si une bibliothèque n’est pas encore sur pypi, pip
est capable, pourvu que le code source vienne avec un fichier setup.py, de l’installer depuis un fichier local, une URL, ou même un simple repo git. Donc si vous trouvez une lib chouette sur github, on peut l’installer ainsi:
pip install git+git://github.com/sametmax/0bin.git
Et elle sera même référencée par le pip freeze
!
Remarquez qu’il faut avoir Git installé, et que le protocole doit être changé pour git+git.
Parfois, pypi est indisponible: un problème sur le site, un problème de dns, un problème de votre FAI… Dans ce cas là on est un peu désemparé. Heureusement, pip
permet aussi de faire un fallback sur des miroirs:
pip install --use-mirrors requests
Dans ce cas pip essayera de trouver le paquet sur une liste de sites miroirs de pypi qu’il possède en interne. Il va les essayer un par un jusqu’à en trouver un de disponible.
Dans le cas les plus extrêmes, certains vont jusqu’à créer leur propre miroir en local. Mais c’est un article à part entière.
Les bibliothèques avec des extensions en C sont très courantes dans les domaines qui ont besoin de performances: drivers de base de données (mysql-python
), calculs scientifiques (numpy
), parsing (lxml
), cryptographie (pycrypto
), etc.
Pip ne fonctionnera que partiellement pour installer ces libs: il devra en effet lancer la compilation de l’extension. Bien que le processus soit automatique et bien fait, il arrive souvent qu’il manque des dépendances.
Pour ce genre de lib, soit vous utilisez un paquet tout fait (*.deb, *.exe, *.rpm, etc.), soit vous tentez d’installer les dépendances.
La première chose à faire, c’est installer les headers de Python. Sous Ubuntu, c’est:
sudo apt-get install python-dev
(et sous CentOs, c’est un yum install python-devel
. Pour Mac, aucune idée.)
Ensuite, on lance l’installation une fois de la lib, et on voit ce qui plante, pour installer les dépendances en conséquence. Par exemple, lxml
a besoin des headers de libxml2
et libxslt1
. Donc on va installer libxml2-dev
et libxslt1-dev
. Pour python-mysql
, il va falloir installer libmysqlclient-dev
.
Quand on est une grosse feignasse, mais que l’on a quand même envie d’installer la lib avec pip
(typiquement, dans le cas d’un virtualenv). on peut faire le compromis de l’installer sur le système avec un paquet, et ensuite de l’installer avec pip
. Ca évite de faire la chasse aux dépendances, mais ça fait double installation.
Enfin, il y a des libs qui ne s’installeront jamais avec pip
. Par exemple, un mastodonte genre PyQT
. Oubliez, c’est tout.