Acceda a los datos en el subdirectorio del paquete

130

Estoy escribiendo un paquete de Python con módulos que necesitan abrir archivos de datos en un ./data/subdirectorio. En este momento tengo las rutas a los archivos codificados en mis clases y funciones. Me gustaría escribir un código más robusto que pueda acceder al subdirectorio independientemente de dónde esté instalado en el sistema del usuario.

He intentado una variedad de métodos, pero hasta ahora no he tenido suerte. Parece que la mayoría de los comandos del "directorio actual" devuelven el directorio del intérprete de Python del sistema, y ​​no el directorio del módulo.

Parece que debería ser un problema trivial y común. Sin embargo, parece que no puedo entenderlo. Parte del problema es que mis archivos de datos no son .pyarchivos, por lo que no puedo usar las funciones de importación y similares.

¿Alguna sugerencia?

En este momento mi directorio de paquetes se ve así:

/
__init__.py
module1.py
module2.py
data/   
   data.txt

Estoy tratando de acceder data.txtdesde module*.py!

Jacob Lyles
fuente

Respuestas:

24

Puede usar __file__para obtener la ruta al paquete, de esta manera:

import os
this_dir, this_filename = os.path.split(__file__)
DATA_PATH = os.path.join(this_dir, "data", "data.txt")
print open(DATA_PATH).read()
RichieHindle
fuente
44
Esto no funcionará si los archivos están en una distribución (IE. Egg). Use pkg_resources para acceder al archivo de datos.
Chris
2
De hecho, esto está roto.
Federico
1
Además, __file__no funciona con py2exe, ya que el valor será la ruta al archivo zip.
Pod
1
Esto realmente funcionó para mí. No tuve ningún problema. Estoy usando python 3.6
Jorge
1
Esto no funcionará en caso de distribución (huevo, etc.).
Adarsh ​​Trivedi
166

La forma estándar de hacerlo es con paquetes setuptools y pkg_resources.

Puede diseñar su paquete de acuerdo con la siguiente jerarquía y configurar el archivo de configuración del paquete para que señale sus recursos de datos, según este enlace:

http://docs.python.org/distutils/setupscript.html#installing-package-data

Luego puede volver a buscar y usar esos archivos usando pkg_resources, según este enlace:

http://peak.telecommunity.com/DevCenter/PkgResources#basic-resource-access

import pkg_resources

DATA_PATH = pkg_resources.resource_filename('<package name>', 'data/')
DB_FILE = pkg_resources.resource_filename('<package name>', 'data/sqlite.db')
elliot42
fuente
77
¿No creará pkg_resources una dependencia de tiempo de ejecución en setuptools ? Por ejemplo, redistribuyo un paquete Debian, entonces ¿por qué dependería python-setuptoolssolo para eso? Hasta ahora __file__funciona bien para mí.
mlt
44
Por qué esto es mejor: la clase ResourceManager proporciona acceso uniforme a los recursos del paquete, ya sea que esos recursos existan como archivos y directorios o estén comprimidos en algún tipo de archivo
vrdhn
44
Sugerencia brillante, gracias. Implementé un archivo estándar abierto usandofrom pkg_resources import resource_filename open(resource_filename('data', 'data.txt'), 'rb')
eageranalyst
55
¿Cómo funcionará esto para usar el paquete cuando no esté instalado? Solo prueba localmente, quiero decir
Claudiu,
11
En python 3.7, importlib.resourcesreemplaza pkg_resourcespara este propósito (debido a problemas de rendimiento).
benjimin
13

Para proporcionar una solución que funcione hoy. Definitivamente use esta API para no reinventar todas esas ruedas.

Se necesita un verdadero nombre de archivo del sistema de archivos. Los huevos comprimidos se extraerán a un directorio de caché:

from pkg_resources import resource_filename, Requirement

path_to_vik_logo = resource_filename(Requirement.parse("enb.portals"), "enb/portals/reports/VIK_logo.png")

Devuelve un objeto similar a un archivo legible para el recurso especificado; Puede ser un archivo real, un StringIO o algún objeto similar. La secuencia está en "modo binario", en el sentido de que los bytes que estén en el recurso se leerán tal cual.

from pkg_resources import resource_stream, Requirement

vik_logo_as_stream = resource_stream(Requirement.parse("enb.portals"), "enb/portals/reports/VIK_logo.png")

Descubrimiento de paquetes y acceso a recursos utilizando pkg_resources

Sascha Gottfried
fuente
10

A menudo no tiene sentido hacer una respuesta que detalle el código que no funciona como es, pero creo que esto es una excepción. Python 3.7 agregó importlib.resourcesque se supone que debe reemplazar pkg_resources. Funcionaría para acceder a archivos dentro de paquetes que no tienen barras en sus nombres, es decir

foo/
    __init__.py
    module1.py
    module2.py
    data/   
       data.txt
    data2.txt

es decir, puede acceder al data2.txtpaquete interno foocon, por ejemplo,

importlib.resources.open_binary('foo', 'data2.txt')

pero fallaría con una excepción para

>>> importlib.resources.open_binary('foo', 'data/data.txt')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/usr/lib/python3.7/importlib/resources.py", line 87, in open_binary
    resource = _normalize_path(resource)
  File "/usr/lib/python3.7/importlib/resources.py", line 61, in _normalize_path
    raise ValueError('{!r} must be only a file name'.format(path))
ValueError: 'data/data2.txt' must be only a file name

Esto no se puede fijar excepto mediante la colocación __init__.pyen datay después de usarlo como un paquete:

importlib.resources.open_binary('foo.data', 'data.txt')

La razón de este comportamiento es "es por diseño" ; pero el diseño puede cambiar ...

Antti Haapala
fuente
¿Tiene un mejor enlace para "es por diseño" que un video de YouTube, preferiblemente uno con texto?
gerrit
@gerrit el segundo contiene texto. "This was a deliberate choice, but I think you have a valid use case. @brettcannon what do you think? And if we allow this, should we make sure it gets into Python 3.7?"
Antti Haapala
8

Necesita un nombre para todo el módulo, se le da que el árbol de directorios no incluye ese detalle, para mí esto funcionó:

import pkg_resources
print(    
    pkg_resources.resource_filename(__name__, 'data/data.txt')
)

No parece que setuptools resuelva los archivos basándose en una coincidencia de nombre con los archivos de datos empaquetados, por lo que tendrá que incluir el data/prefijo prácticamente sin importar qué. Puede usarlo os.path.join('data', 'data.txt)si necesita separadores de directorio alternativos. Sin embargo, generalmente no encuentro problemas de compatibilidad con los separadores de directorio de estilo Unix codificados.

ThorSummoner
fuente
docs.python.org/3.6/distutils/… > Tenga en cuenta que los nombres de ruta (archivos o directorios) suministrados en el script de configuración deben escribirse utilizando la convención de Unix, es decir, separados por barras. Distutils se encargará de convertir esta representación neutral de la plataforma en lo que sea apropiado en su plataforma actual antes de usar el nombre de ruta. Esto hace que su script de configuración sea portátil en todos los sistemas operativos, lo que, por supuesto, es uno de los principales objetivos de Distutils. En este espíritu, todos los nombres de ruta en este documento están separados por barras.
changyuheng
6

Creo que busqué una respuesta.

Hago un módulo data_path.py, que importo en mis otros módulos que contienen:

data_path = os.path.join(os.path.dirname(__file__),'data')

Y luego abro todos mis archivos con

open(os.path.join(data_path,'filename'), <param>)
Jacob Lyles
fuente
2
Esto no funcionará cuando el recurso esté en una distribución de archivo (como un huevo comprimido). Prefiero algo así:pkg_resources.resource_string('pkg_name', 'data/file.txt')
ankostis 01 de
@ankostis setuptools es lo suficientemente inteligente como para extraer el archivo si detecta que lo usó en __file__alguna parte. En mi caso, uso una biblioteca que realmente quiere rutas y no secuencias. Por supuesto, podría escribir los archivos temporalmente en el disco, pero siendo flojo, solo uso la función setuptools.
letmaik