From 2249ef8fcdc96cb2d721c0a4bbf921b72a5eee7c Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Wed, 6 Dec 2017 01:11:35 +0000 Subject: [PATCH] "filename" -> "resource" --- README.rst | 129 +++---------------------- importlib_resources/_py2.py | 34 +++---- importlib_resources/_py3.py | 52 +++++----- importlib_resources/docs/api.rst | 80 +++++++-------- importlib_resources/docs/migration.rst | 16 +-- 5 files changed, 105 insertions(+), 206 deletions(-) diff --git a/README.rst b/README.rst index edec3d1dbe13b65..b1f25e64add04b3 100644 --- a/README.rst +++ b/README.rst @@ -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 `_ -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 `_:: - - 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 `_. +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 +`_ 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. diff --git a/importlib_resources/_py2.py b/importlib_resources/_py2.py index 53a290ca5ec33fd..bf6ff4196e20597 100644 --- a/importlib_resources/_py2.py +++ b/importlib_resources/_py2.py @@ -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') @@ -63,22 +63,22 @@ 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 @@ -86,7 +86,7 @@ def read(package, file_name, encoding='utf-8', errors='strict'): @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, @@ -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 @@ -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 @@ -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: @@ -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(): @@ -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) diff --git a/importlib_resources/_py3.py b/importlib_resources/_py3.py index 8e7d1476c8c68ba..46e1bf4d54ce61f 100644 --- a/importlib_resources/_py3.py +++ b/importlib_resources/_py3.py @@ -19,22 +19,22 @@ Package = Union[ModuleType, str] if sys.version_info >= (3, 6): - FileName = Union[str, os.PathLike] # pragma: ge35 + Resource = Union[str, os.PathLike] # pragma: ge35 else: - FileName = str # pragma: le35 + Resource = str # pragma: le35 def _get_package(package) -> ModuleType: if hasattr(package, '__spec__'): if package.__spec__.submodule_search_locations is None: - raise TypeError("{!r} is not a package".format( + raise TypeError('{!r} is not a package'.format( package.__spec__.name)) else: return package else: module = import_module(package) if module.__spec__.submodule_search_locations is None: - raise TypeError("{!r} is not a package".format(package)) + raise TypeError('{!r} is not a package'.format(package)) else: return module @@ -43,7 +43,7 @@ def _normalize_path(path) -> str: str_path = str(path) parent, file_name = os.path.split(str_path) if parent: - raise ValueError("{!r} must be only a file name".format(path)) + raise ValueError('{!r} must be only a file name'.format(path)) else: return file_name @@ -61,20 +61,20 @@ def _get_resource_reader( def open(package: Package, - file_name: FileName, + resource: Resource, encoding: str = None, errors: str = None) -> IO: """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) reader = _get_resource_reader(package) if reader is not None: - return _wrap_file(reader.open_resource(file_name), encoding, errors) + return _wrap_file(reader.open_resource(resource), encoding, errors) # Using pathlib doesn't work well here due to the lack of 'strict' # argument for pathlib.Path.resolve() prior to Python 3.6. absolute_package_path = os.path.abspath(package.__spec__.origin) package_path = os.path.dirname(absolute_package_path) - full_path = os.path.join(package_path, file_name) + full_path = os.path.join(package_path, resource) if encoding is None: args = dict(mode='rb') else: @@ -91,14 +91,14 @@ def open(package: Package, except IOError: package_name = package.__spec__.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: Package, - file_name: FileName, + resource: Resource, encoding: str = 'utf-8', errors: str = 'strict') -> Union[str, bytes]: """Return the decoded string of the resource. @@ -106,10 +106,10 @@ def read(package: Package, 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: if encoding is None: return binary_file.read() # Decoding from io.TextIOWrapper() instead of str.decode() in hopes @@ -120,7 +120,7 @@ def read(package: Package, @contextmanager -def path(package: Package, file_name: FileName) -> Iterator[Path]: +def path(package: Package, resource: Resource) -> Iterator[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, @@ -129,23 +129,23 @@ def path(package: Package, file_name: FileName) -> Iterator[Path]: 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) reader = _get_resource_reader(package) if reader is not None: try: - yield Path(reader.resource_path(file_name)) + yield Path(reader.resource_path(resource)) return except FileNotFoundError: pass # Fall-through for both the lack of resource_path() *and* if # resource_path() raises FileNotFoundError. package_directory = Path(package.__spec__.origin).parent - file_path = package_directory / file_name + file_path = package_directory / resource if file_path.exists(): yield file_path else: - with open(package, file_name) as file: + with open(package, resource) as file: data = file.read() # Not using tempfile.NamedTemporaryFile as it leads to deeper 'try' # blocks due to the need to close the temporary file to work on @@ -162,26 +162,26 @@ def path(package: Package, file_name: FileName) -> Iterator[Path]: pass -def is_resource(package: Package, file_name: str) -> bool: - """True if file_name is a resource inside package. +def is_resource(package: Package, name: str) -> bool: + """True if `name` is a resource inside `package`. Directories are *not* resources. """ package = _get_package(package) - _normalize_path(file_name) + _normalize_path(name) reader = _get_resource_reader(package) if reader is not None: - return reader.is_resource(file_name) + return reader.is_resource(name) try: package_contents = set(contents(package)) except (NotADirectoryError, FileNotFoundError): 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.__spec__.origin).parent / file_name + path = Path(package.__spec__.origin).parent / name if path.is_file(): return True if path.is_dir(): @@ -195,7 +195,7 @@ def is_resource(package: Package, file_name: str) -> bool: 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) @@ -217,7 +217,7 @@ def is_resource(package: Package, file_name: str) -> bool: def contents(package: Package) -> Iterator[str]: - """Return the list of entries in package. + """Return the list of entries in `package`. Note that not all entries are resources. Specifically, directories are not considered resources. Use `is_resource()` on each entry returned here diff --git a/importlib_resources/docs/api.rst b/importlib_resources/docs/api.rst index d08a93c08b41b94..2034c83dd78956e 100644 --- a/importlib_resources/docs/api.rst +++ b/importlib_resources/docs/api.rst @@ -18,7 +18,7 @@ Types must have a resolvable ``__spec__.submodule_search_locations`` that is not ``None``. -.. py:class:: FileName +.. py:class:: Resource This type describes the resource names passed into the various functions in this package. For Python 3.6 and later, this is defined as @@ -29,62 +29,61 @@ Types Functions ========= -.. py:function:: importlib_resources.open(package, file_name, encoding=None, errors=None) +.. py:function:: importlib_resources.open(package, resource, encoding=None, errors=None) - Open for reading the resource named ``file_name`` within the ``package`` + Open for reading the resource named **resource** within the **package** package. By default, the resource is opened for reading in binary mode. - With a non-``None`` ``encoding`` argument, the resource is opened in text - mode, with ``errors`` having the same meaning as for built-in + With a non-``None`` **encoding** argument, the resource is opened in text + mode, with **errors** having the same meaning as for built-in :py:func:`open`. :param package: A package name or module object. See above for the API that such module objects must support. :type package: ``Package`` - :param file_name: The name of the resource to open within ``package``. - ``file_name`` may not contain path separators and it may - not have sub-resources (i.e. it cannot be a directory). - :type file_name: ``FileName`` + :param resource: The name of the resource to open within **package**. + **resource** may not contain path separators and it may + not have sub-resources (i.e. it cannot be a directory). + :type resource: ``Resource`` :param encoding: When ``None``, the resource is opened in binary mode. When an encoding is given, the resource is opened in text - mode. ``encoding`` has the same meaning as with + mode. **encoding** has the same meaning as with :py:func:`open`. :type encoding: str - :param errors: This parameter is ignored when ``encoding`` is ``None``. + :param errors: This parameter is ignored when **encoding** is ``None``. Otherwise it has the same meaning as with :py:func:`open`. :type errors: str :returns: an I/O stream open for reading. :rtype: ``typing.IO`` -.. py:function:: importlib_resources.read(package, file_name, encoding='utf-8', errors='strict') +.. py:function:: importlib_resources.read(package, resource, encoding='utf-8', errors='strict') - Read and return the contents of the resource named ``file_name`` within - the ``package`` package. By default, the contents are read in UTF-8 and - returned as a ``str`` (in Python 3 - ``unicode`` in Python 2). With - ``encoding`` set to ``None``, the resource contents are read in binary - mode and returned as ``bytes``. + Read and return the contents of the resource named **resource** within the + **package** package. By default, the contents are read in UTF-8 and + returned as a ``str`` [#fn1]_. With **encoding** set to ``None``, the + resource contents are read in binary mode and returned as ``bytes``. :param package: A package name or module object. See above for the API that such module objects must support. :type package: ``Package`` - :param file_name: The name of the resource to read within ``package``. - ``file_name`` may not contain path separators and it may - not have sub-resources (i.e. it cannot be a directory). - :type file_name: ``FileName`` + :param resource: The name of the resource to read within **package**. + **resource** may not contain path separators and it may + not have sub-resources (i.e. it cannot be a directory). + :type resource: ``Resource`` :param encoding: When ``None``, the resource is read in binary mode. When an encoding is given, the resource is read in text - mode. ``encoding`` has the same meaning as with + mode. **encoding** has the same meaning as with :py:func:`open`. :type encoding: str - :param errors: This parameter is ignored when ``encoding`` is ``None``. + :param errors: This parameter is ignored when **encoding** is ``None``. Otherwise it has the same meaning as with :py:func:`open`. :type errors: str :returns: the contents of the resource. :rtype: ``bytes`` or ``str`` -.. py:function:: importlib_resources.path(package, file_name) +.. py:function:: importlib_resources.path(package, resource) - Return the path to the resource as an actual file system path. This + Return the path to the **resource** as an actual file system path. This function returns a `context manager`_ for use in a ``with``-statement. The context manager provides a :py:class:`pathlib.Path` object. @@ -94,29 +93,29 @@ Functions :param package: A package name or module object. See above for the API that such module objects must support. :type package: ``Package`` - :param file_name: The name of the resource to read within ``package``. - ``file_name`` may not contain path separators and it may - not have sub-resources (i.e. it cannot be a directory). - :type file_name: ``FileName`` + :param resource: The name of the resource to read within **package**. + **resource** may not contain path separators and it may + not have sub-resources (i.e. it cannot be a directory). + :type resource: ``Resource`` :returns: A context manager for use in a ``with``-statement. Entering the context manager provides a :py:class:`pathlib.Path` object. :rtype: context manager providing a :py:class:`pathlib.Path` object -.. py:function:: importlib_resources.is_resource(package, file_name) +.. py:function:: importlib_resources.is_resource(package, name) - Return True if there is a resource named ``file_name`` in the package, - otherwise False. Remember that directories are *not* resources! + Return ``True`` if there is a resource named **name** in the package, + otherwise ``False``. Remember that directories are *not* resources! :param package: A package name or module object. See above for the API that such module objects must support. :type package: ``Package`` - :param file_name: The name of the resource to read within ``package``. - ``file_name`` may not contain path separators and it may - not have sub-resources (i.e. it cannot be a directory). - :type file_name: ``FileName`` + :param name: The name of the resource to read within **package**. + **resource** may not contain path separators and it may + not have sub-resources (i.e. it cannot be a directory). + :type name: ``str`` :returns: A flag indicating whether the resource exists or not. - :rtype: bool + :rtype: ``bool`` .. py:function:: importlib_resources.contents(package) @@ -129,7 +128,12 @@ Functions that such module objects must support. :type package: ``Package`` :returns: The contents of the package, both resources and non-resources. - :rtype: An iterator over strings. + :rtype: An iterator over ``str`` +.. rubric:: Footnotes + +.. [#fn1] The contents are returned as a ``str`` in Python 3, but as a + ``unicode`` in Python 2. + .. _`context manager`: https://docs.python.org/3/library/stdtypes.html#typecontextmanager diff --git a/importlib_resources/docs/migration.rst b/importlib_resources/docs/migration.rst index 19e39d395f112f5..9e1119201301363 100644 --- a/importlib_resources/docs/migration.rst +++ b/importlib_resources/docs/migration.rst @@ -23,8 +23,8 @@ since only files are allowed as resources, file names in the ``importlib_resources`` API may *not* include path separators (e.g. slashes). -``pkg_resources.resource_filename()`` -===================================== +pkg_resources.resource_filename() +================================= ``resource_filename()`` is one of the more interesting APIs because it guarantees that the return value names a file on the file system. This means @@ -74,8 +74,8 @@ Assuming your Python interpreter exits gracefully, the temporary file will be cleaned up when Python exits. -``pkg_resources.resource_stream()`` -=================================== +pkg_resources.resource_stream() +=============================== ``pkg_resources.resource_stream()`` returns a readable file-like object opened in binary mode. When you read from the returned file-like object, you get @@ -90,8 +90,8 @@ The equivalent code in ``importlib_resources`` is pretty straightforward:: my_bytes = fp.read() -``pkg_resources.resource_string()`` -=================================== +pkg_resources.resource_string() +=============================== In Python 2, ``pkg_resources.resource_string()`` returns the contents of a resource as a ``str``. In Python 3, this function is a misnomer; it actually @@ -111,8 +111,8 @@ a ``unicode`` in Python 2 or a ``str`` in Python 3, read and decoded with the ``utf-8`` encoding. -``pkg_resources.resource_listdir()`` -==================================== +pkg_resources.resource_listdir() +================================ This function lists the entries in the package, both files and directories, but it does not recurse into subdirectories, e.g.::