Re: Loading Resources From a Python Module/Package

Brett Cannon <[email protected]> Sat, 31 Jan 2015 14:48:12 +0000
Newsgroups gmane.comp.python.import
Message-ID <CAP1=2W4Ev6YKCy6AMyfQ5m2TVC7vMyLMzsX6_Fm3CQKixqgNmQ@mail.gmail.com>
 On Fri, Jan 30, 2015, 19:52 Donald Stufft <[email protected]> wrote:


> On Jan 30, 2015, at 7:18 PM, Paul Moore <[email protected]> wrote:
>
> On 30 January 2015 at 23:37, Donald Stufft <[email protected]> wrote:
>> A. What do people think about pkgutil.get_data_filename and
>>   Loader.get_data_filename?
>
> Sounds reasonable. It's a relatively rare, but useful use case. One
> possible issue, though, would people assume that if they get a
> filename it'd be writeable? For the filesystem loader it would be, but
> that would break subtly (writes work but would get discarded) for
> loaders that don't have a native get_data_filename.

I don’t think you can assume it’s writeable since that’ll break in a lot
of common cases even with the filesystem loader since often times things
in the filesystem will be installed in the system and users won’t have
permissions to write to them anyways.

>
> Related question - how would the temp files be cleaned up? At exit?

My patch registers an atexit handler that cleans up the temporary files yea.

>
>> B. What do people think about modifying Loader.get_data so it can support
>>   relative filenames instead of the calling code needing to handle that?
>
> I'd have to think about that one, but in principle it seems reasonable.
>
> While we're extending the loaders, a far more commonly requested
> feature would be to list available data files. At the moment, code can
> only load data from known paths, which is not ideal. While it's
> unrelated to the original proposal, it makes sense if we're changing
> the spec of loaders to do it in one go, rather than having multiple
> iterations.

Well both pkgutil.get_data and pkgutil.get_data_filename have parallels in
the
pkg_resources library for similar reasons. If we want to extend this to more
things it might make sense to take a look at what all exists there
currently:

resource_exists(package_or_requirement, resource_name)
    Does the named resource exist? Return True or False accordingly.

resource_stream(package_or_requirement, resource_name)
    Return a readable file-like object for the specified resource; it may
be an
    actual file, a StringIO, or some similar object. The stream is in
    “binary mode”, in the sense that whatever bytes are in the resource
will be
    read as-is.

resource_string(package_or_requirement, resource_name)
    Return the specified resource as a string. The resource is read in
binary
    fashion, such that the returned string contains exactly the bytes that
are
    stored in the resource.

resource_isdir(package_or_requirement, resource_name)
    Is the named resource a directory? Return True or False accordingly.

resource_listdir(package_or_requirement, resource_name)
    List the contents of the named resource directory, just like os.listdir
    except that it works even if the resource is in a zipfile.

resource_filename(package_or_requirement, resource_name)
    Sometimes, it is not sufficient to access a resource in string or stream
    form, and a true filesystem filename is needed. In such cases, you can
use
    this method (or module-level function) to obtain a filename for a
resource.
    If the resource is in an archive distribution (such as a zipped egg), it
    will be extracted to a cache directory, and the filename within the
cache
    will be returned. If the named resource is a directory, then all
resources
    within that directory (including subdirectories) are also extracted. If
the
    named resource is a C extension or “eager resource” (see the setuptools
    documentation for details), then all C extensions and eager resources
are
    extracted at the same time.

See
https://pythonhosted.org/setuptools/pkg_resources.html#basic-resource-access
and
https://pythonhosted.org/setuptools/pkg_resources.html#resource-extraction

Obviously the similar functions here are:

* pkgutil.get_data is pkg_resources.resource_string
* pkgutil.get_data_filename is pkg_resources.resource_filename

The major difference being that pkg_resource.resource_filename will extract
to
a cache directory (controllable with an environment variable or
programatically) and won't clean up the extracted files. This means that
they
are (by default) extracted once per user and reused between extractions. I
felt
like it made more sense to just extract to a temporary location (even though
this is less performant) in the stdlib.

That leaves:

* resource_exists
* resource_stream
* resource_isdir
* resource_listdir

Which can be done via pkg_resources but not via the standard library, I
don't
have a major opinion on whether or not the standard library should do all of
them but I don't think it would hurt if it did.

Another interesting question if we're going to add more methods is where
they
should all live. As far as I know pkgutil.get_data predates the importlib
module.

 It does, so you really have to think in terms of finders and loaders.

Perhaps deprecating pkgutil.get_data and adding a importlib.resources
module which supports functions like:

* get_bytes(package, resource)
* get_stream(package, resource)
* get_filename(package, resource)
* exists(package, resource)
* isdir(package, resource)
* listdir(package, resource)

Changing the names (particular get_data -> get_bytes) could also provide the
mechanism for allowing relative files and deprecating the "you must pass in
a full file path to the Loader()" behavior since the get_data method could
be
left alone and a new get_bytes method could be added.


The reason Loader.get_data() takes absolute paths is to do away with
ambiguity. If you have a relative path and ask a loader to read that path,
where should that relative path be anchored? Should it be the top-level
package? What about the module that loader ewas returned to handle? But
then what about if a finder caches loaders and reuses them across modules
(nothing in PEP 302 says you can't do this and in actuality the frozen and
built-in loaders are just static and class methods). The choice of dealing
exclusively in absolute paths was a conscious choice on my part.

Now having said that, there is nothing to say absolute paths require file
system I based paths. What you should really do is think of these paths as
opaque, non-ambiguous paths for the loader which claimed it knew what file
path was needed to pass to get_data(). If you think that way then you
realize you can use markers in the path as necessary, e.g.
some/path/file.zip/pkg/sub/data.txt. As long as loader.get_data() can
unambiguously read that path as returned by get_data_filename() or whatever
the method is called then you have fully abstracted paths out while still
being able to read data from a loader.

Basically any API dealing with paths for loaders needs to abstract away the
concept of files, file-like paths, etc. and rely on using the loader API on
pretty much everything as a simple os.path of its own. This is why I have
not tried to tackle the issue of the list_contents() or some such API to
list modules and potentially data files as it needs to not really have a
concrete concept of file paths (and it really should be on finders and not
loaders which complicates discovery, selecting the right finder, etc.).
This is also why APIs wanting a file path instead of taking a file-like
object simply cannot play well with importlib and loaders which have
alternative back end storage without simply being lucky that the loader
they are working with uses filesystem paths (or writing out to a temp file).

-brett

This would mean people can do things like:

    import importlib.resources
    import socket
    import ssl

    context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
    context.verify_mode = ssl.CERT_REQUIRED
    context.check_hostname = True
    context.load_verify_locations(
        cafile=importlib.resources.get_filename("certifi", "cacert.pem"),
    )

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    ssl_sock = context.wrap_socket(s, server_hostname='www.verisign.com')
    ssl_sock.connect(('www.verisign.com', 443))

---
Donald Stufft
PGP: 7C6B 7C5D 5E2B 6356 A926 F04F 6E3C BCE9 3372 DCFA

_______________________________________________
Import-SIG mailing list
[email protected]
https://mail.python.org/mailman/listinfo/import-sig

_______________________________________________
Import-SIG mailing list
[email protected]
https://mail.python.org/mailman/listinfo/import-sig