Skip to content

Commit

Permalink
Merge branch 'renames' into 'master'
Browse files Browse the repository at this point in the history
"filename" -> "resource"

Closes python#33

See merge request python-devs/importlib_resources!39
  • Loading branch information
warsaw committed Dec 6, 2017
2 parents 5d53bf9 + 2249ef8 commit 8d6e24d
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 206 deletions.
129 changes: 12 additions & 117 deletions README.rst
Original file line number Diff line number Diff line change
@@ -1,118 +1,13 @@
``importlib.resources``
=======================
This repository is to house the design and implementation of a planned
``importlib.resources`` module for Python's stdlib -- aiming for
Python 3.7 -- along with a backport to target Python 3.4 - 3.6.

The key goal of this module is to replace
`pkg_resources <https://setuptools.readthedocs.io/en/latest/pkg_resources.html>`_
with a solution in Python's stdlib that relies on well-defined APIs.
This should not only make reading resources included in packages easier,
but have the semantics be stable and consistent.

Goals
-----

- Provide a reasonable replacement for ``pkg_resources.resource_stream()``
- Provide a reasonable replacement for ``pkg_resources.resource_string()``
- Provide a reasonable replacement for ``pkg_resources.resource_filename()``
- Define an ABC for loaders to implement for reading resources
- Implement this in the stdlib for Python 3.7
- Implement a package for PyPI which will work on Python >=3.4

Non-goals
---------
- Replace all of ``pkg_resources``
- For what is replaced in ``pkg_resources``, provide an **exact**
replacement

Design
======
Low-level
---------
For `importlib.abc <https://docs.python.org/3/library/importlib.html#module-importlib.abc>`_::

import abc
from typing.io import BinaryIO


class ResourceReader(abc.ABC):

def open_resource(self, path: str) -> BinaryIO:
"""Return a file-like object opened for binary reading.

The 'path' argument is expected to represent only a file name.
If the resource cannot be found, FileNotFoundError is raised.
"""
raise FileNotFoundError

def resource_path(self, path: str) -> str:
"""Return the file system path to the specified resource.


The 'path' argument is expected to represent only a file name.
If the resource does not exist on the file system, raise
FileNotFoundError.
"""
raise FileNotFoundError

High-level
----------
For ``importlib.resources``::

import pathlib
import types
from typing import ContextManager, Union
from typing.io import BinaryIO
=========================
``importlib.resources``
=========================


Package = Union[str, types.ModuleType]
FileName = Union[str, os.PathLike]


def open(package: Package, file_name: FileName) -> BinaryIO:
"""Return a file-like object opened for binary-reading of the resource."""
...


def read(package: Package, file_name: FileName, encoding: str = "utf-8",
errors: str = "strict") -> str:
"""Return the decoded string of the resource.

The decoding-related arguments have the same semantics as those of
bytes.decode().
"""
...


@contextlib.contextmanager
def path(package: Package, file_name: FileName) -> ContextManager[pathlib.Path]:
"""A context manager providing a file path object to the resource.

If the resource does not already exist on its own on the file system,
a temporary file will be created. If the file was created, the file
will be deleted upon exiting the context manager (no exception is
raised if the file was deleted prior to the context manager
exiting).
"""
...

If *package* is an actual package, it is used directly. Otherwise the
argument is used in calling ``importlib.import_module()``. The found
package is expected to be an actual package, otherwise ``TypeError`` is
raised.

For the *file_name* argument, it is expected to be only a file name
with no other path parts. If any parts beyond a file name are found, a
``ValueError`` will be raised. The expectation is that all data files
will exist within a directory that can be imported by Python as a
package.

All functions raise ``FileNotFoundError`` if the resource does not exist
or cannot be found.


Issues
======
Please see the
`issue tracker <https://github.com/brettcannon/importlib_resources/issues>`_.
This repository is to house the design and implementation of a planned
``importlib.resources`` module for Python's stdlib -- aiming for Python 3.7 --
along with a backport to target Python 2.7, and 3.4 - 3.6.

The key goal of this module is to replace parts of `pkg_resources
<https://setuptools.readthedocs.io/en/latest/pkg_resources.html>`_ with a
solution in Python's stdlib that relies on well-defined APIs. This should not
only make reading resources included in packages easier, but have the
semantics be stable and consistent.
34 changes: 17 additions & 17 deletions importlib_resources/_py2.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ def _normalize_path(path):
return file_name


def open(package, file_name, encoding=None, errors=None):
def open(package, resource, encoding=None, errors=None):
"""Return a file-like object opened for reading of the resource."""
file_name = _normalize_path(file_name)
resource = _normalize_path(resource)
package = _get_package(package)
# Using pathlib doesn't work well here due to the lack of 'strict' argument
# for pathlib.Path.resolve() prior to Python 3.6.
package_path = os.path.dirname(package.__file__)
relative_path = os.path.join(package_path, file_name)
relative_path = os.path.join(package_path, resource)
full_path = os.path.abspath(relative_path)
if encoding is None:
args = dict(mode='rb')
Expand All @@ -63,30 +63,30 @@ def open(package, file_name, encoding=None, errors=None):
except (IOError, AttributeError):
package_name = package.__name__
message = '{!r} resource not found in {!r}'.format(
file_name, package_name)
resource, package_name)
raise FileNotFoundError(message)
else:
return _wrap_file(BytesIO(data), encoding, errors)


def read(package, file_name, encoding='utf-8', errors='strict'):
def read(package, resource, encoding='utf-8', errors='strict'):
"""Return the decoded string of the resource.
The decoding-related arguments have the same semantics as those of
bytes.decode().
"""
file_name = _normalize_path(file_name)
resource = _normalize_path(resource)
package = _get_package(package)
# Note this is **not** builtins.open()!
with open(package, file_name) as binary_file:
with open(package, resource) as binary_file:
contents = binary_file.read()
if encoding is None:
return contents
return contents.decode(encoding=encoding, errors=errors)


@contextmanager
def path(package, file_name):
def path(package, resource):
"""A context manager providing a file path object to the resource.
If the resource does not already exist on its own on the file system,
Expand All @@ -95,10 +95,10 @@ def path(package, file_name):
raised if the file was deleted prior to the context manager
exiting).
"""
file_name = _normalize_path(file_name)
resource = _normalize_path(resource)
package = _get_package(package)
package_directory = Path(package.__file__).parent
file_path = package_directory / file_name
file_path = package_directory / resource
# If the file actually exists on the file system, just return it.
# Otherwise, it's probably in a zip file, so we need to create a temporary
# file and copy the contents into that file, hence the contextmanager to
Expand All @@ -107,7 +107,7 @@ def path(package, file_name):
yield file_path
else:
# Note this is **not** builtins.open()!
with open(package, file_name) as fileobj:
with open(package, resource) as fileobj:
data = fileobj.read()
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
# blocks due to the need to close the temporary file to work on Windows
Expand All @@ -124,13 +124,13 @@ def path(package, file_name):
pass


def is_resource(package, file_name):
"""True if file_name is a resource inside package.
def is_resource(package, name):
"""True if name is a resource inside package.
Directories are *not* resources.
"""
package = _get_package(package)
_normalize_path(file_name)
_normalize_path(name)
try:
package_contents = set(contents(package))
except OSError as error:
Expand All @@ -142,12 +142,12 @@ def is_resource(package, file_name):
# worth it.
raise # pragma: ge3
return False
if file_name not in package_contents:
if name not in package_contents:
return False
# Just because the given file_name lives as an entry in the package's
# contents doesn't necessarily mean it's a resource. Directories are not
# resources, so let's try to find out if it's a directory or not.
path = Path(package.__file__).parent / file_name
path = Path(package.__file__).parent / name
if path.is_file():
return True
if path.is_dir():
Expand All @@ -161,7 +161,7 @@ def is_resource(package, file_name):
with ZipFile(archive_path) as zf:
toc = zf.namelist()
relpath = package_directory.relative_to(archive_path)
candidate_path = relpath / file_name
candidate_path = relpath / name
for entry in toc: # pragma: nobranch
try:
relative_to_candidate = Path(entry).relative_to(candidate_path)
Expand Down
Loading

0 comments on commit 8d6e24d

Please sign in to comment.