Comment lire un fichier (statique) depuis l'intérieur d'un package Python?

107

Pourriez-vous me dire comment puis-je lire un fichier qui se trouve dans mon package Python?

Ma situation

Un package que je charge a un certain nombre de modèles (fichiers texte utilisés comme chaînes) que je souhaite charger à partir du programme. Mais comment spécifier le chemin vers un tel fichier?

Imaginez que je souhaite lire un fichier à partir de:

package\templates\temp_file

Une sorte de manipulation de chemin? Suivi du chemin de base du package?

Ronszon
la source
1
duplication possible de Recherche d'un fichier dans une distribution de module Python
Andreas Jung
duplication possible des données d'accès Python dans le sous
répertoire du

Réponses:

-13

[ajouté 15/06/2016: apparemment, cela ne fonctionne pas dans toutes les situations. veuillez vous référer aux autres réponses]


import os, mypackage
template = os.path.join(mypackage.__path__[0], 'templates', 'temp_file')
jcomeau_ictx
la source
176

TLDR; Utilisez le importlib.resourcesmodule standard-library comme expliqué dans la méthode n ° 2 ci-dessous.

Le traditionnel pkg_resourcesfromsetuptools n'est plus recommandé car la nouvelle méthode:

  • il est nettement plus performant ;
  • C'est plus sûr puisque l'utilisation de packages (au lieu de path-stings) soulève des erreurs de compilation;
  • il est plus intuitif car vous n'avez pas à «rejoindre» les chemins;
  • il est plus rapide lors du développement car vous n'avez pas besoin d'une dépendance supplémentaire ( setuptools), mais comptez uniquement sur la bibliothèque standard de Python.

J'ai gardé la liste traditionnelle en premier, pour expliquer les différences avec la nouvelle méthode lors du portage du code existant (le portage est également expliqué ici ).



Supposons que vos modèles se trouvent dans un dossier imbriqué dans le package de votre module:

  <your-package>
    +--<module-asking-the-file>
    +--templates/
          +--temp_file                         <-- We want this file.

Note 1: Bien sûr, nous ne devons PAS jouer avec l' __file__attribut (par exemple, le code se cassera lorsqu'il est servi à partir d'un zip).

Remarque 2: Si vous créez ce package, n'oubliez pas de déclater vos fichiers de données en tant que package_dataoudata_files dans votre fichier setup.py.

1) Utilisation pkg_resourcesde setuptools(lent)

Vous pouvez utiliser le pkg_resourcespackage de la distribution setuptools , mais cela a un coût en termes de performances :

import pkg_resources

# Could be any dot-separated package/module name or a "Requirement"
resource_package = __name__
resource_path = '/'.join(('templates', 'temp_file'))  # Do not use os.path.join()
template = pkg_resources.resource_string(resource_package, resource_path)
# or for a file-like stream:
template = pkg_resources.resource_stream(resource_package, resource_path)

Conseils:

  • Cela lira les données même si votre distribution est compressée, vous pouvez donc les définir zip_safe=Truedans votre setup.pyet / ou utiliser le zipapppacker tant attendu de python-3.5 pour créer des distributions autonomes.

  • N'oubliez pas d'ajouter setuptoolsà vos exigences d'exécution (par exemple dans install_requires`).

... et notez que selon les Setuptools / pkg_resourcesdocs, vous ne devez pas utiliser os.path.join:

Accès aux ressources de base

Notez que les noms de ressources doivent être des /chemins séparés et ne peuvent pas être absolus (c'est-à-dire sans début /) ni contenir des noms relatifs tels que " ..". Ne pas utiliser des os.pathroutines pour manipuler les chemins de ressources, car ils sont pas des chemins système de fichiers.

2) Python> = 3.7, ou en utilisant la importlib_resourcesbibliothèque rétroportée

Utilisez le importlib.resourcesmodule de la bibliothèque standard qui est plus efficace que setuptoolsci-dessus:

try:
    import importlib.resources as pkg_resources
except ImportError:
    # Try backported to PY<37 `importlib_resources`.
    import importlib_resources as pkg_resources

from . import templates  # relative-import the *package* containing the templates

template = pkg_resources.read_text(templates, 'temp_file')
# or for a file-like stream:
template = pkg_resources.open_text(templates, 'temp_file')

Attention:

Concernant la fonction read_text(package, resource):

  • Le packagepeut être une chaîne ou un module.
  • Le resourcen'est plus un chemin, mais juste le nom de fichier de la ressource à ouvrir, dans un package existant; il peut ne pas contenir de séparateurs de chemin et ne pas avoir de sous-ressources (c'est-à-dire qu'il ne peut pas être un répertoire).

Pour l'exemple posé dans la question, il faut maintenant:

  • transformer le <your_package>/templates/ en un package approprié, en créant un __init__.pyfichier vide dedans
  • donc maintenant nous pouvons utiliser une importinstruction simple (éventuellement relative) (plus d'analyse des noms de package / module),
  • et demandez simplement resource_name = "temp_file"(pas de chemin).

Conseils:

  • Pour accéder à un fichier à l'intérieur du module actuel, définissez l'argument package sur __package__, par exemple pkg_resources.read_text(__package__, 'temp_file')(grâce à @ ben-mares).
  • Les choses deviennent intéressantes quand un nom de fichier réel est demandé avec path(), puisque maintenant les gestionnaires de contexte sont utilisés pour les fichiers créés temporairement (lire ceci ).
  • Ajoutez la bibliothèque rétroportée, conditionnellement pour les Pythons plus anciens, avec install_requires=[" importlib_resources ; python_version<'3.7'"](cochez ceci si vous empaquetez votre projet avec setuptools<36.2.1).
  • N'oubliez pas de supprimer la setuptoolsbibliothèque de vos exigences d'exécution , si vous avez migré à partir de la méthode traditionnelle.
  • N'oubliez pas de personnaliser setup.pyou MANIFESTd' inclure des fichiers statiques .
  • Vous pouvez également définir zip_safe=Truedans votre setup.py.
Ankostis
la source
1
str.join prend la séquence resource_path = '/'.join(('templates', 'temp_file'))
Alex Punnen
1
J'ai toujours NotImplementedError: Can't perform this operation for loaders without 'get_data()'des idées?
leoschet
Notez que importlib.resourceset nepkg_resources sont pas nécessairement compatibles . importlib.resourcesfonctionne avec les fichiers zip ajoutés à sys.path, setuptools et pkg_resourcesfonctionne avec les fichiers egg, qui sont des fichiers zip stockés dans un répertoire auquel il est lui-même ajouté sys.path. Par exemple, avec sys.path = [..., '.../foo', '.../bar.zip'], les œufs entrent .../foo, mais les emballages bar.zippeuvent également être importés. Vous ne pouvez pas utiliser pkg_resourcespour extraire des données de packages dans bar.zip. Je n'ai pas vérifié si setuptools enregistre le chargeur nécessaire pour importlib.resourcestravailler avec des œufs.
Martijn Pieters
Une configuration supplémentaire de setup.py est-elle requise si une erreur Package has no locationapparaît?
zygimantus
1
Si vous souhaitez accéder à un fichier à l'intérieur du module actuel (et non à un sous-module comme templatesdans l'exemple), vous pouvez définir l' packageargument sur __package__, par exemplepkg_resources.read_text(__package__, 'temp_file')
Ben Mares
43

Un prélude packaging:

Avant même de pouvoir vous soucier de la lecture des fichiers de ressources, la première étape consiste à vous assurer que les fichiers de données sont emballés dans votre distribution en premier lieu - il est facile de les lire directement à partir de l'arborescence source, mais la partie importante est de faire assurez-vous que ces fichiers de ressources sont accessibles à partir du code dans un package installé .

Structurez votre projet comme celui - ci, en plaçant les fichiers de données dans un sous - répertoire à l' intérieur du paquet:

.
├── package
   ├── __init__.py
   ├── templates
      └── temp_file
   ├── mymodule1.py
   └── mymodule2.py
├── README.rst
├── MANIFEST.in
└── setup.py

Vous devriez passer include_package_data=Truel' setup()appel. Le fichier manifeste n'est nécessaire que si vous souhaitez utiliser setuptools / distutils et créer des distributions source. Pour vous assurer que le templates/temp_filefichier est empaqueté pour cet exemple de structure de projet, ajoutez une ligne comme celle-ci dans le fichier manifeste:

recursive-include package *

Note cruelle historique: l' utilisation d'un fichier manifeste n'est pas nécessaire pour les backends de construction modernes tels que flit, poetry, qui incluront les fichiers de données du package par défaut. Donc, si vous utilisez pyproject.tomlet que vous n'avez pas de setup.pyfichier, vous pouvez ignorer tout ce qui concerne MANIFEST.in.

Maintenant, avec l'emballage à l'écart, sur la partie lecture ...

Recommandation:

Utilisez les pkgutilAPI de bibliothèque standard . Cela va ressembler à ceci dans le code de la bibliothèque:

# within package/mymodule1.py, for example
import pkgutil

data = pkgutil.get_data(__name__, "templates/temp_file")
print("data:", repr(data))
text = pkgutil.get_data(__name__, "templates/temp_file").decode()
print("text:", repr(text))

Cela fonctionne en zips. Il fonctionne sur Python 2 et Python 3. Il ne nécessite pas de dépendances tierces. Je ne suis pas vraiment au courant des inconvénients (si vous l'êtes, veuillez commenter la réponse).

Mauvaises façons d'éviter:

Mauvaise façon n ° 1: utiliser des chemins relatifs à partir d'un fichier source

C'est actuellement la réponse acceptée. Au mieux, cela ressemble à ceci:

from pathlib import Path

resource_path = Path(__file__).parent / "templates"
data = resource_path.joinpath("temp_file").read_bytes()
print("data", repr(data))

Qu'est-ce qui ne va pas avec ça? L'hypothèse selon laquelle vous avez des fichiers et des sous-répertoires disponibles n'est pas correcte. Cette approche ne fonctionne pas si vous exécutez du code qui est emballé dans un zip ou une roue, et il peut être entièrement hors du contrôle de l'utilisateur que votre paquet soit extrait ou non dans un système de fichiers.

Mauvaise façon n ° 2: utiliser les API pkg_resources

Ceci est décrit dans la réponse la plus votée. Cela ressemble à quelque chose comme ceci:

from pkg_resources import resource_string

data = resource_string(__name__, "templates/temp_file")
print("data", repr(data))

Qu'est-ce qui ne va pas avec ça? Il ajoute une dépendance d' exécution sur setuptools , qui devrait de préférence être une dépendance de temps d' installation uniquement. L'importation et l'utilisation pkg_resourcespeuvent devenir très lentes, car le code crée un ensemble de travail de tous les packages installés, même si vous n'étiez intéressé que par vos propres ressources de package. Ce n'est pas un gros problème au moment de l'installation (puisque l'installation est unique), mais c'est moche au moment de l'exécution.

Mauvaise façon n ° 3: utiliser les API importlib.resources

C'est actuellement la recommandation dans la réponse la plus votée. C'est un ajout récent de bibliothèque standard ( nouveau dans Python 3.7 ), mais il existe également un backport disponible. Cela ressemble à ceci:

try:
    from importlib.resources import read_binary
    from importlib.resources import read_text
except ImportError:
    # Python 2.x backport
    from importlib_resources import read_binary
    from importlib_resources import read_text

data = read_binary("package.templates", "temp_file")
print("data", repr(data))
text = read_text("package.templates", "temp_file")
print("text", repr(text))

Qu'est-ce qui ne va pas avec ça? Eh bien, malheureusement, cela ne fonctionne pas ... encore. Il s'agit toujours d'une API incomplète, l'utilisation importlib.resourcesvous obligera à ajouter un fichier vide templates/__init__.pyafin que les fichiers de données résident dans un sous-package plutôt que dans un sous-répertoire. Il exposera également le package/templatessous - répertoire en tant que package.templatessous-package importable à part entière. Si ce n'est pas un gros problème et que cela ne vous dérange pas, vous pouvez alors y ajouter le __init__.pyfichier et utiliser le système d'importation pour accéder aux ressources. Cependant, pendant que vous y êtes, vous pouvez aussi bien en faire un my_resources.pyfichier à la place, et définir simplement des octets ou des variables de chaîne dans le module, puis les importer dans du code Python. C'est le système d'importation qui fait le gros du travail ici de toute façon.

Exemple de projet:

J'ai créé un exemple de projet sur github et téléchargé sur PyPI , qui illustre les quatre approches décrites ci-dessus. Essayez-le avec:

$ pip install resources-example
$ resources-example

Voir https://github.com/wimglenn/resources-example pour plus d'informations.

wim
la source
1
Il a été modifié en mai dernier. Mais je suppose qu'il est facile de rater les explications de l'intro. Pourtant, vous conseillez les gens contre la norme - c'est une balle difficile à mordre :-)
ankostis
1
@ankostis Permettez-moi plutôt de vous poser la question, pourquoi recommanderiez-vous importlib.resourcesmalgré toutes ces lacunes une API incomplète qui est déjà en attente de dépréciation ? Plus récent n'est pas nécessairement meilleur. Dites-moi quels avantages offre-t-il réellement par rapport à stdlib pkgutil, dont votre réponse ne fait aucune mention?
wim
1
Cher @wim, la dernière réponse de Brett Canon sur l'utilisation de a pkgutil.get_data()confirmé mon instinct - c'est une API sous-développée et à déconseiller. Cela dit, je suis d'accord avec vous, ce importlib.resourcesn'est pas une bien meilleure alternative, mais jusqu'à ce que PY3.10 résout ce problème, je maintiens ce choix, il a appris qu'il ne s'agit pas simplement d'un autre "standard" recommandé par la documentation.
ankostis le
1
@ankostis Je prendrais les commentaires de Brett avec un grain de sel. pkgutiln'est pas du tout mentionné dans le calendrier de dépréciation de PEP 594 - Retrait des piles épuisées de la bibliothèque standard , et il est peu probable qu'il soit supprimé sans une bonne raison. Il existe depuis Python 2.3 et est spécifié dans le cadre du protocole de chargement dans PEP 302 . Utiliser une "API sous-définie" n'est pas une réponse très convaincante, qui pourrait décrire la majorité de la bibliothèque standard Python!
wim le
2
Permettez-moi d'ajouter: je veux aussi voir les ressources importlib réussir! Je suis tout à fait pour des API rigoureusement définies. C'est juste que dans son état actuel, il ne peut pas vraiment être recommandé. L'API est toujours en cours de modification, elle est inutilisable pour de nombreux packages existants et n'est disponible que dans les versions Python relativement récentes. En pratique, c'est pire que pkgutildans presque tous les sens. Votre "instinct" et votre appel à l'autorité n'ont aucun sens pour moi, s'il y a des problèmes avec les get_datachargeurs, montrez-leur des preuves et des exemples pratiques.
wim le
14

Dans le cas où vous avez cette structure

lidtk
├── bin
   └── lidtk
├── lidtk
   ├── analysis
      ├── char_distribution.py
      └── create_cm.py
   ├── classifiers
      ├── char_dist_metric_train_test.py
      ├── char_features.py
      ├── cld2
         ├── cld2_preds.txt
         └── cld2wili.py
      ├── get_cld2.py
      ├── text_cat
         ├── __init__.py
         ├── README.md   <---------- say you want to get this
         └── textcat_ngram.py
      └── tfidf_features.py
   ├── data
      ├── __init__.py
      ├── create_ml_dataset.py
      ├── download_documents.py
      ├── language_utils.py
      ├── pickle_to_txt.py
      └── wili.py
   ├── __init__.py
   ├── get_predictions.py
   ├── languages.csv
   └── utils.py
├── README.md
├── setup.cfg
└── setup.py

vous avez besoin de ce code:

import pkg_resources

# __name__ in case you're within the package
# - otherwise it would be 'lidtk' in this example as it is the package name
path = 'classifiers/text_cat/README.md'  # always use slash
filepath = pkg_resources.resource_filename(__name__, path)

L'étrange partie "toujours utiliser la barre oblique" provient des setuptoolsAPI

Notez également que si vous utilisez des chemins, vous devez utiliser une barre oblique (/) comme séparateur de chemin, même si vous êtes sous Windows. Setuptools convertit automatiquement les barres obliques en séparateurs appropriés spécifiques à la plate-forme au moment de la construction

Au cas où vous vous demandez où se trouve la documentation:

Martin Thoma
la source
Merci pour votre réponse concise
Paolo
pkg_resourcesa des frais généraux qui pkgutilsurmonte. De plus, si le code fourni est exécuté comme point d'entrée, __name__il évaluera à __main__, pas le nom du package.
A. Hendry il y a
8

Le contenu de "10.8. Reading Datafiles Within a Package" de Python Cookbook, Third Edition par David Beazley et Brian K. Jones donnant les réponses.

Je vais juste l'amener ici:

Supposons que vous ayez un package avec des fichiers organisés comme suit:

mypackage/
    __init__.py
    somedata.dat
    spam.py

Supposons maintenant que le fichier spam.py veuille lire le contenu du fichier somedata.dat. Pour ce faire, utilisez le code suivant:

import pkgutil
data = pkgutil.get_data(__package__, 'somedata.dat')

Les données variables résultantes seront une chaîne d'octets contenant le contenu brut du fichier.

Le premier argument de get_data () est une chaîne contenant le nom du package. Vous pouvez le fournir directement ou utiliser une variable spéciale, telle que __package__. Le deuxième argument est le nom relatif du fichier dans le package. Si nécessaire, vous pouvez naviguer dans différents répertoires en utilisant les conventions de nom de fichier Unix standard tant que le répertoire final se trouve toujours dans le package.

De cette façon, le package peut être installé en tant que répertoire, .zip ou .egg.

chaokunyang
la source
J'aime que vous ayez référencé le livre de cuisine!
A. Hendry il y a
0

La réponse acceptée devrait être d'utiliser importlib.resources. pkgutil.get_dataexige également que l'argument packagesoit un package non-espace de noms ( voir la documentation pkgutil ). Par conséquent, le répertoire contenant la ressource doit avoir un __init__.pyfichier, ce qui lui confère exactement les mêmes limitations que importlib.resources. Si le problème des frais généraux pkg_resourcesn'est pas un problème, c'est également une alternative acceptable.

A. Hendry
la source
-1

Chaque module python de votre package a un __file__attribut

Vous pouvez l'utiliser comme:

import os 
from mypackage

templates_dir = os.path.join(os.path.dirname(mypackage.__file__), 'templates')
template_file = os.path.join(templates_dir, 'template.txt')

Pour les ressources sur les œufs, voir: http://peak.telecommunity.com/DevCenter/PythonEggs#accessing-package-resources

Zaur Nasibov
la source
Cela ne fonctionnera pas pour le code source contenu dans les fichiers zip.
A. Hendry il y a
-3

en supposant que vous utilisez un fichier œuf; non extrait:

J'ai "résolu" ceci dans un projet récent, en utilisant un script de post-installation, qui extrait mes modèles de l'oeuf (fichier zip) dans le répertoire approprié du système de fichiers. C'était la solution la plus rapide et la plus fiable que j'ai trouvée, car travailler avec __path__[0]peut parfois mal tourner (je ne me souviens pas du nom, mais je suis tombé sur au moins une bibliothèque, cela a ajouté quelque chose devant cette liste!).

De plus, les fichiers d'œufs sont généralement extraits à la volée vers un emplacement temporaire appelé «cache d'œufs». Vous pouvez changer cet emplacement en utilisant une variable d'environnement, soit avant de démarrer votre script, soit même plus tard, par exemple.

os.environ['PYTHON_EGG_CACHE'] = path

Cependant, il existe pkg_resources qui pourrait faire le travail correctement.

Florian
la source