Skip to content

Commit 8475c05

Browse files
authored
Support text open(), though not by default (python#32)
Closes python#31
1 parent f1ccb15 commit 8475c05

File tree

4 files changed

+42
-18
lines changed

4 files changed

+42
-18
lines changed

importlib_resources/_py2.py

+10-6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
import tempfile
33

44
from ._compat import FileNotFoundError
5-
from __builtin__ import open as builtin_open
5+
from ._util import _wrap_file
66
from contextlib import contextmanager
77
from importlib import import_module
8-
from io import BytesIO
8+
from io import BytesIO, open as io_open
99
from pathlib2 import Path
1010

1111

@@ -32,17 +32,21 @@ def _normalize_path(path):
3232
return file_name
3333

3434

35-
def open(package, file_name):
36-
"""Return a file-like object opened for binary-reading of the resource."""
35+
def open(package, file_name, encoding=None, errors=None):
36+
"""Return a file-like object opened for reading of the resource."""
3737
file_name = _normalize_path(file_name)
3838
package = _get_package(package)
3939
# Using pathlib doesn't work well here due to the lack of 'strict' argument
4040
# for pathlib.Path.resolve() prior to Python 3.6.
4141
package_path = os.path.dirname(package.__file__)
4242
relative_path = os.path.join(package_path, file_name)
4343
full_path = os.path.abspath(relative_path)
44+
if encoding is None:
45+
args = dict(mode='rb')
46+
else:
47+
args = dict(mode='r', encoding=encoding, errors=errors)
4448
try:
45-
return builtin_open(full_path, 'rb')
49+
return io_open(full_path, **args)
4650
except IOError:
4751
# This might be a package in a zip file. zipimport provides a loader
4852
# with a functioning get_data() method, however we have to strip the
@@ -60,7 +64,7 @@ def open(package, file_name):
6064
file_name, package_name)
6165
raise FileNotFoundError(message)
6266
else:
63-
return BytesIO(data)
67+
return _wrap_file(BytesIO(data), encoding, errors)
6468

6569

6670
def read(package, file_name, encoding='utf-8', errors='strict'):

importlib_resources/_py3.py

+14-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import tempfile
44

55
from . import abc as resources_abc
6+
from ._util import _wrap_file
67
from builtins import open as builtins_open
78
from contextlib import contextmanager
89
from importlib import import_module
@@ -12,7 +13,7 @@
1213
from types import ModuleType
1314
from typing import Iterator, Union
1415
from typing import cast
15-
from typing.io import BinaryIO
16+
from typing.io import IO
1617

1718

1819
Package = Union[ModuleType, str]
@@ -46,21 +47,28 @@ def _normalize_path(path) -> str:
4647
return file_name
4748

4849

49-
def open(package: Package, file_name: FileName) -> BinaryIO:
50-
"""Return a file-like object opened for binary-reading of the resource."""
50+
def open(package: Package,
51+
file_name: FileName,
52+
encoding: str = None,
53+
errors: str = None) -> IO:
54+
"""Return a file-like object opened for reading of the resource."""
5155
file_name = _normalize_path(file_name)
5256
package = _get_package(package)
5357
if hasattr(package.__spec__.loader, 'open_resource'):
5458
reader = cast(resources_abc.ResourceReader, package.__spec__.loader)
55-
return reader.open_resource(file_name)
59+
return _wrap_file(reader.open_resource(file_name), encoding, errors)
5660
else:
5761
# Using pathlib doesn't work well here due to the lack of 'strict'
5862
# argument for pathlib.Path.resolve() prior to Python 3.6.
5963
absolute_package_path = os.path.abspath(package.__spec__.origin)
6064
package_path = os.path.dirname(absolute_package_path)
6165
full_path = os.path.join(package_path, file_name)
66+
if encoding is None:
67+
args = dict(mode='rb')
68+
else:
69+
args = dict(mode='r', encoding=encoding, errors=errors)
6270
try:
63-
return builtins_open(full_path, 'rb')
71+
return builtins_open(full_path, **args) # type: ignore
6472
except IOError:
6573
# Just assume the loader is a resource loader; all the relevant
6674
# importlib.machinery loaders are and an AttributeError for
@@ -74,7 +82,7 @@ def open(package: Package, file_name: FileName) -> BinaryIO:
7482
file_name, package_name)
7583
raise FileNotFoundError(message)
7684
else:
77-
return BytesIO(data)
85+
return _wrap_file(BytesIO(data), encoding, errors)
7886

7987

8088
def read(package: Package,

importlib_resources/_util.py

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from io import TextIOWrapper
2+
3+
4+
def _wrap_file(resource, encoding, errors):
5+
if encoding is None:
6+
return resource
7+
return TextIOWrapper(resource, encoding=encoding, errors=errors)

importlib_resources/tests/test_open.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -17,22 +17,27 @@ class OpenTests:
1717

1818
# Subclasses are expected to set the 'data' attribute.
1919

20-
def test_opened_for_reading(self):
21-
# The file-like object is ready for reading.
22-
with resources.open(self.data, 'utf-8.file') as file:
23-
self.assertEqual(b"Hello, UTF-8 world!\n", file.read())
20+
def test_open_for_binary(self):
21+
# By default, the resource is opened for binary reads.
22+
with resources.open(self.data, 'utf-8.file') as fp:
23+
self.assertEqual(b'Hello, UTF-8 world!\n', fp.read())
2424

2525
def test_wrap_for_text(self):
2626
# The file-like object can be wrapped for text reading.
27-
with resources.open(self.data, 'utf-8.file') as file:
28-
text = file.read().decode(encoding='utf-8')
27+
with resources.open(self.data, 'utf-8.file') as fp:
28+
text = fp.read().decode(encoding='utf-8')
2929
self.assertEqual('Hello, UTF-8 world!\n', text)
3030

3131
def test_FileNotFoundError(self):
3232
with self.assertRaises(FileNotFoundError):
3333
with resources.open(self.data, 'does-not-exist'):
3434
pass
3535

36+
def test_open_for_text(self):
37+
# open() takes an optional encoding and errors parameter.
38+
with resources.open(self.data, 'utf-8.file', 'utf-8', 'strict') as fp:
39+
self.assertEqual('Hello, UTF-8 world!\n', fp.read())
40+
3641

3742
class OpenDiskTests(OpenTests, unittest.TestCase):
3843

0 commit comments

Comments
 (0)