¿Cómo leer un archivo (estático) desde dentro de un paquete de Python?

106

¿Podría decirme cómo puedo leer un archivo que está dentro de mi paquete de Python?

Mi situación

Un paquete que cargo tiene varias plantillas (archivos de texto que se usan como cadenas) que quiero cargar desde el programa. Pero, ¿cómo especifico la ruta a dicho archivo?

Imagina que quiero leer un archivo de:

package\templates\temp_file

¿Algún tipo de manipulación del camino? ¿Seguimiento de la ruta base del paquete?

Ronszon
fuente
posible duplicado de los datos de acceso
ankostis

Respuestas:

-12

[agregado 2016-06-15: aparentemente esto no funciona en todas las situaciones. consulte las otras respuestas]


import os, mypackage
template = os.path.join(mypackage.__path__[0], 'templates', 'temp_file')
jcomeau_ictx
fuente
175

TLDR; Utilice el importlib.resourcesmódulo de biblioteca estándar como se explica en el método no 2, a continuación.

El tradicional pkg_resourcesdesetuptools que no se recomienda más porque el nuevo método:

  • es significativamente más eficaz ;
  • Esto es más seguro ya que el uso de paquetes (en lugar de picaduras de ruta) genera errores en tiempo de compilación;
  • es más intuitivo porque no es necesario "unir" rutas;
  • es más rápido cuando se desarrolla, ya que no necesita una dependencia adicional ( setuptools), sino que confía solo en la biblioteca estándar de Python.

Mantuve la lista tradicional primero, para explicar las diferencias con el nuevo método al portar el código existente (el portar también se explica aquí ).



Supongamos que sus plantillas están ubicadas en una carpeta anidada dentro del paquete de su módulo:

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

Nota 1: Por supuesto, NO debemos jugar con el __file__atributo (por ejemplo, el código se romperá cuando se sirva desde un zip).

Nota 2: Si está creando este paquete, recuerde declarar sus archivos de datos como package_dataodata_files en su setup.py.

1) Usando pkg_resourcesdesde setuptools(lento)

Puede usar el pkg_resourcespaquete de la distribución setuptools , pero eso tiene un costo, en términos de rendimiento :

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)

Consejos:

  • Esto leerá los datos incluso si su distribución está comprimida, por lo que puede configurar zip_safe=Truesu setup.pyy / o usar el tan esperado zipappempaquetador de python-3.5 para crear distribuciones autónomas.

  • Recuerde agregar setuptoolssus requisitos de tiempo de ejecución (por ejemplo, en install_requires`).

... y tenga en cuenta que de acuerdo con Setuptools / pkg_resourcesdocs, no debe usar os.path.join:

Acceso a recursos básicos

Tenga en cuenta que los nombres de los recursos deben ser /rutas separadas y no pueden ser absolutos (es decir, sin principio /) ni contener nombres relativos como " ..". No , no utilizar os.pathrutinas para manipular caminos de recursos, ya que son no trayectorias del sistema de archivos.

2) Python> = 3.7, o usando la importlib_resourcesbiblioteca backportada

Utilice el importlib.resourcesmódulo de la biblioteca estándar que es más eficiente que el setuptoolsanterior:

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')

Atención:

Respecto a la función read_text(package, resource):

  • El packagepuede ser una cadena o un módulo.
  • El resourceno es un camino más, pero sólo el nombre de archivo del recurso a abierta, dentro de un paquete existente; puede que no contenga separadores de ruta y no tenga sub-recursos (es decir, no puede ser un directorio).

Para el ejemplo planteado en la pregunta, ahora debemos:

  • conviértalo <your_package>/templates/ en un paquete adecuado, creando un __init__.pyarchivo vacío en él,
  • así que ahora podemos usar una importdeclaración simple (posiblemente relativa) (no más análisis de nombres de paquetes / módulos),
  • y simplemente pedir resource_name = "temp_file"(sin camino).

Consejos:

  • Para acceder a un archivo dentro del módulo actual, establezca el argumento del paquete en __package__, por ejemplo pkg_resources.read_text(__package__, 'temp_file')(gracias a @ ben-mares).
  • Las cosas se vuelven interesantes cuando se pregunta un nombre de archivo realpath() , ya que ahora los administradores de contexto se utilizan para archivos creados temporalmente (lea esto ).
  • Agregue la biblioteca backportada, condicionalmente para Pythons más antiguos, con install_requires=[" importlib_resources ; python_version<'3.7'"](marque esto si empaqueta su proyecto con setuptools<36.2.1).
  • Recuerde eliminar la setuptoolsbiblioteca de sus requisitos de tiempo de ejecución , si migró desde el método tradicional.
  • Recuerde personalizar setup.pyo MANIFESTpara incluir todos los archivos estáticos .
  • También puede establecer zip_safe=Trueen su setup.py.
ankostis
fuente
1
str.join toma la secuencia resource_path = '/'.join(('templates', 'temp_file'))
Alex Punnen
Sigo teniendo NotImplementedError: Can't perform this operation for loaders without 'get_data()'alguna idea?
leoschet
Tenga en cuenta que importlib.resourcesy nopkg_resources son necesariamente compatibles . importlib.resourcestrabaja con zipfiles agregados sys.path, setuptools y pkg_resourcestrabaja con archivos egg, que son zipfiles almacenados en un directorio al que se agrega sys.path. Por ejemplo sys.path = [..., '.../foo', '.../bar.zip'], con los huevos entran .../foo, pero los paquetes bar.ziptambién se pueden importar. No se puede utilizar pkg_resourcespara extraer datos de paquetes en formato bar.zip. No he comprobado si setuptools registra el cargador necesario para importlib.resourcestrabajar con huevos.
Martijn Pieters
¿Se requiere una configuración adicional de setup.py si Package has no locationaparece un error ?
zygimantus
1
En caso de que desee acceder a un archivo dentro del módulo actual (y no a un submódulo como templatesen el ejemplo), puede establecer el packageargumento en __package__, por ejemplopkg_resources.read_text(__package__, 'temp_file')
Ben Mares
42

Un preludio de empaque:

Antes de que pueda preocuparse por leer archivos de recursos, el primer paso es asegurarse de que los archivos de datos se empaqueten en su distribución en primer lugar; es fácil leerlos directamente desde el árbol de fuentes, pero la parte importante es hacer asegúrese de que estos archivos de recursos sean accesibles desde el código dentro de un paquete instalado .

Estructura tu proyecto de esta manera, poniendo los archivos de datos en un subdirectorio dentro del paquete:

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

Deberías pasar include_package_data=Truela setup()llamada. El archivo de manifiesto solo es necesario si desea utilizar setuptools / distutils y compilar distribuciones de origen. Para asegurarse de que templates/temp_filese empaquete para esta estructura de proyecto de ejemplo, agregue una línea como esta en el archivo de manifiesto:

recursive-include package *

Nota cruft histórica: no es necesario usar un archivo de manifiesto para backends de compilación modernos como flit, poetry, que incluirán los archivos de datos del paquete de forma predeterminada. Entonces, si está usando pyproject.tomly no tiene un setup.pyarchivo, puede ignorar todo lo relacionado con MANIFEST.in.

Ahora, con el embalaje fuera del camino, en la parte de lectura ...

Recomendación:

Utilice pkgutilAPI de biblioteca estándar . Se verá así en el código de la biblioteca:

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

Funciona en cremalleras. Funciona en Python 2 y Python 3. No requiere dependencias de terceros. Realmente no estoy al tanto de ninguna desventaja (si es así, por favor comente la respuesta).

Malas formas de evitar:

Manera mala # 1: usar rutas relativas de un archivo fuente

Esta es actualmente la respuesta aceptada. En el mejor de los casos, se parece a esto:

from pathlib import Path

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

¿Qué está mal con eso? La suposición de que tiene archivos y subdirectorios disponibles no es correcta. Este enfoque no funciona si se ejecuta código que está empaquetado en un zip o una rueda, y puede estar completamente fuera del control del usuario si su paquete se extrae o no en un sistema de archivos.

Mal camino # 2: usar las API pkg_resources

Esto se describe en la respuesta más votada. Se parece a esto:

from pkg_resources import resource_string

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

¿Qué está mal con eso? Agrega una dependencia del tiempo de ejecución en las herramientas de configuración , que preferiblemente debería ser solo una dependencia del tiempo de instalación . La importación y el uso pkg_resourcespueden volverse muy lentos, ya que el código crea un conjunto de trabajo de todos los paquetes instalados, aunque solo le interesen los recursos de su propio paquete. Eso no es un gran problema en el momento de la instalación (ya que la instalación es única), pero es feo en el tiempo de ejecución.

Mal camino # 3: usar las API de importlib.resources

Esta es actualmente la recomendación en la respuesta más votada. Es una adición de biblioteca estándar reciente ( nueva en Python 3.7 ), pero también hay un backport disponible. Se parece a esto:

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á mal con eso? Bueno, desafortunadamente, no funciona ... todavía. Esta es todavía una API incompleta, el uso importlib.resourcesrequerirá que agregue un archivo vacío templates/__init__.pypara que los archivos de datos residan dentro de un subpaquete en lugar de en un subdirectorio. También expondrá el package/templatessubdirectorio como un subpaquete importable package.templatespor derecho propio. Si eso no es un gran problema y no le molesta, puede continuar y agregar el __init__.pyarchivo allí y usar el sistema de importación para acceder a los recursos. Sin embargo, mientras lo hace, también puede convertirlo en un my_resources.pyarchivo, y simplemente definir algunos bytes o variables de cadena en el módulo, luego importarlos en código Python. Es el sistema de importación el que hace el trabajo pesado aquí de cualquier manera.

Proyecto de ejemplo:

Creé un proyecto de ejemplo en github y lo cargué en PyPI , que demuestra los cuatro enfoques discutidos anteriormente. Pruébelo con:

$ pip install resources-example
$ resources-example

Consulte https://github.com/wimglenn/resources-example para obtener más información.

wim
fuente
1
Ha sido editado el pasado mes de mayo. Pero supongo que es fácil pasar por alto las explicaciones de la introducción. Aún así, aconsejas a la gente contra el estándar, es una bala difícil de morder :-)
ankostis
1
@ankostis Permíteme pasar la pregunta a ti en su lugar, ¿por qué recomendarías a importlib.resourcespesar de todas estas deficiencias con una API incompleta que ya está pendiente de desaprobación ? Lo nuevo no es necesariamente mejor. Dígame qué ventajas ofrece realmente sobre stdlib pkgutil, que su respuesta no menciona.
wim
1
Estimado @wim, la última respuesta de Brett Canon sobre el uso de pkgutil.get_data()confirmó mi instinto: es una API subdesarrollada y que quedará obsoleta. Dicho esto, estoy de acuerdo con usted, importlib.resourcesno es una alternativa mucho mejor, pero hasta que PY3.10 resuelva esto, mantengo esta elección, habiendo aprendido que no es simplemente otro "estándar" recomendado por los documentos.
ankostis
1
@ankostis Tomaría los comentarios de Brett con un grano de sal. pkgutilno se menciona en absoluto en el programa de obsolescencia de PEP 594 - Eliminación de baterías agotadas de la biblioteca estándar , y es poco probable que se eliminen sin una buena razón. Ha existido desde Python 2.3 y se especifica como parte del protocolo de carga en PEP 302 . Usar una "API subdefinida" no es una respuesta muy convincente, ¡que podría describir la mayoría de la biblioteca estándar de Python!
wim
2
Permítanme agregar: ¡también quiero que los recursos de importlib tengan éxito! Estoy a favor de las API rigurosamente definidas. Es solo que en su estado actual, realmente no se puede recomendar. La API aún está experimentando cambios, no se puede utilizar para muchos paquetes existentes y solo está disponible en versiones de Python relativamente recientes. En la práctica es peor que pkgutilen casi todos los sentidos. Su "instinto" y apelación a la autoridad no tiene sentido para mí, si hay problemas con los get_datacargadores, muestre pruebas y ejemplos prácticos.
wim
15

En caso de que tengas esta estructura

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

necesitas este código:

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)

La extraña parte de "usar siempre barra oblicua" proviene de las setuptoolsAPI

También observe que si usa rutas, debe usar una barra inclinada (/) como separador de ruta, incluso si está en Windows. Setuptools convierte automáticamente las barras inclinadas en separadores específicos de la plataforma adecuados en el momento de la compilación

En caso de que se pregunte dónde está la documentación:

Martín Thoma
fuente
Gracias por su respuesta concisa
Paolo
8

El contenido de "10.8. Lectura de archivos de datos dentro de un paquete" de Python Cookbook, tercera edición por David Beazley y Brian K. Jones dando las respuestas.

Lo llevaré hasta aquí:

Suponga que tiene un paquete con archivos organizados de la siguiente manera:

mypackage/
    __init__.py
    somedata.dat
    spam.py

Ahora suponga que el archivo spam.py quiere leer el contenido del archivo somedata.dat. Para hacerlo, use el siguiente código:

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

Los datos variables resultantes serán una cadena de bytes que contiene el contenido sin procesar del archivo.

El primer argumento de get_data () es una cadena que contiene el nombre del paquete. Puede suministrarlo directamente o utilizar una variable especial, como __package__. El segundo argumento es el nombre relativo del archivo dentro del paquete. Si es necesario, puede navegar en diferentes directorios utilizando las convenciones estándar de nombres de archivo de Unix siempre que el directorio final todavía se encuentre dentro del paquete.

De esta forma, el paquete se puede instalar como directorio, .zip o .egg.

chaokunyang
fuente
-2

asumiendo que está utilizando una lima de huevo; no extraído:

"Resolví" esto en un proyecto reciente, usando un script postinstall, que extrae mis plantillas del huevo (archivo zip) al directorio apropiado en el sistema de archivos. Fue la solución más rápida y confiable que encontré, ya que trabajar con __path__[0]puede salir mal a veces (no recuerdo el nombre, pero encontré al menos una biblioteca, ¡eso agregó algo al frente de esa lista!).

Además, los archivos de huevos generalmente se extraen sobre la marcha a una ubicación temporal llamada "caché de huevos". Puede cambiar esa ubicación utilizando una variable de entorno, ya sea antes de iniciar su script o incluso más tarde, por ejemplo.

os.environ['PYTHON_EGG_CACHE'] = path

Sin embargo, hay pkg_resources que podrían hacer el trabajo correctamente.

Florian
fuente