Skip to content

Commit

Permalink
Support text open(), though not by default (python#32)
Browse files Browse the repository at this point in the history
Closes python#31
  • Loading branch information
warsaw authored Nov 30, 2017
1 parent f1ccb15 commit 8475c05
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 18 deletions.
16 changes: 10 additions & 6 deletions importlib_resources/_py2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand All @@ -32,17 +32,21 @@ 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
# 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)
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
Expand All @@ -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'):
Expand Down
20 changes: 14 additions & 6 deletions importlib_resources/_py3.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
7 changes: 7 additions & 0 deletions importlib_resources/_util.py
Original file line number Diff line number Diff line change
@@ -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)
17 changes: 11 additions & 6 deletions importlib_resources/tests/test_open.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,22 +17,27 @@ 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):
with self.assertRaises(FileNotFoundError):
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):

Expand Down

0 comments on commit 8475c05

Please sign in to comment.