diff --git a/importlib_resources/_py2.py b/importlib_resources/_py2.py index 670ffc05781a3ff..6646940058da6d1 100644 --- a/importlib_resources/_py2.py +++ b/importlib_resources/_py2.py @@ -2,10 +2,10 @@ import tempfile from ._compat import FileNotFoundError -from __builtin__ import open as builtin_open +from ._util import _wrap_file from contextlib import contextmanager from importlib import import_module -from io import BytesIO +from io import BytesIO, open as io_open from pathlib2 import Path @@ -32,8 +32,8 @@ def _normalize_path(path): return file_name -def open(package, file_name): - """Return a file-like object opened for binary-reading of the resource.""" +def open(package, file_name, encoding=None, errors=None): + """Return a file-like object opened for reading of the resource.""" file_name = _normalize_path(file_name) package = _get_package(package) # Using pathlib doesn't work well here due to the lack of 'strict' argument @@ -41,8 +41,12 @@ def open(package, file_name): package_path = os.path.dirname(package.__file__) relative_path = os.path.join(package_path, file_name) full_path = os.path.abspath(relative_path) + if encoding is None: + args = dict(mode='rb') + else: + args = dict(mode='r', encoding=encoding, errors=errors) try: - return builtin_open(full_path, 'rb') + return io_open(full_path, **args) except IOError: # This might be a package in a zip file. zipimport provides a loader # with a functioning get_data() method, however we have to strip the @@ -60,7 +64,7 @@ def open(package, file_name): file_name, package_name) raise FileNotFoundError(message) else: - return BytesIO(data) + return _wrap_file(BytesIO(data), encoding, errors) def read(package, file_name, encoding='utf-8', errors='strict'): diff --git a/importlib_resources/_py3.py b/importlib_resources/_py3.py index 6b5721c252ee8ed..4b8ef80afb26ac3 100644 --- a/importlib_resources/_py3.py +++ b/importlib_resources/_py3.py @@ -3,6 +3,7 @@ import tempfile from . import abc as resources_abc +from ._util import _wrap_file from builtins import open as builtins_open from contextlib import contextmanager from importlib import import_module @@ -12,7 +13,7 @@ from types import ModuleType from typing import Iterator, Union from typing import cast -from typing.io import BinaryIO +from typing.io import IO Package = Union[ModuleType, str] @@ -46,21 +47,28 @@ def _normalize_path(path) -> str: return file_name -def open(package: Package, file_name: FileName) -> BinaryIO: - """Return a file-like object opened for binary-reading of the resource.""" +def open(package: Package, + file_name: FileName, + encoding: str = None, + errors: str = None) -> IO: + """Return a file-like object opened for reading of the resource.""" file_name = _normalize_path(file_name) package = _get_package(package) if hasattr(package.__spec__.loader, 'open_resource'): reader = cast(resources_abc.ResourceReader, package.__spec__.loader) - return reader.open_resource(file_name) + return _wrap_file(reader.open_resource(file_name), encoding, errors) else: # 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) + if encoding is None: + args = dict(mode='rb') + else: + args = dict(mode='r', encoding=encoding, errors=errors) try: - return builtins_open(full_path, 'rb') + return builtins_open(full_path, **args) # type: ignore except IOError: # Just assume the loader is a resource loader; all the relevant # importlib.machinery loaders are and an AttributeError for @@ -74,7 +82,7 @@ def open(package: Package, file_name: FileName) -> BinaryIO: file_name, package_name) raise FileNotFoundError(message) else: - return BytesIO(data) + return _wrap_file(BytesIO(data), encoding, errors) def read(package: Package, diff --git a/importlib_resources/_util.py b/importlib_resources/_util.py new file mode 100644 index 000000000000000..6edbdd13bdff01f --- /dev/null +++ b/importlib_resources/_util.py @@ -0,0 +1,7 @@ +from io import TextIOWrapper + + +def _wrap_file(resource, encoding, errors): + if encoding is None: + return resource + return TextIOWrapper(resource, encoding=encoding, errors=errors) diff --git a/importlib_resources/tests/test_open.py b/importlib_resources/tests/test_open.py index 2e5cc67102ea3d9..c7acb3d317d7f7b 100644 --- a/importlib_resources/tests/test_open.py +++ b/importlib_resources/tests/test_open.py @@ -17,15 +17,15 @@ class OpenTests: # Subclasses are expected to set the 'data' attribute. - def test_opened_for_reading(self): - # The file-like object is ready for reading. - with resources.open(self.data, 'utf-8.file') as file: - self.assertEqual(b"Hello, UTF-8 world!\n", file.read()) + def test_open_for_binary(self): + # By default, the resource is opened for binary reads. + with resources.open(self.data, 'utf-8.file') as fp: + self.assertEqual(b'Hello, UTF-8 world!\n', fp.read()) def test_wrap_for_text(self): # The file-like object can be wrapped for text reading. - with resources.open(self.data, 'utf-8.file') as file: - text = file.read().decode(encoding='utf-8') + with resources.open(self.data, 'utf-8.file') as fp: + text = fp.read().decode(encoding='utf-8') self.assertEqual('Hello, UTF-8 world!\n', text) def test_FileNotFoundError(self): @@ -33,6 +33,11 @@ def test_FileNotFoundError(self): with resources.open(self.data, 'does-not-exist'): pass + def test_open_for_text(self): + # open() takes an optional encoding and errors parameter. + with resources.open(self.data, 'utf-8.file', 'utf-8', 'strict') as fp: + self.assertEqual('Hello, UTF-8 world!\n', fp.read()) + class OpenDiskTests(OpenTests, unittest.TestCase):