Skip to content

Commit 021a488

Browse files
committed
Merge branch 'issue41' into 'master'
Split open() and read() into separate binary and text versions Closes python#41 and python#42 See merge request python-devs/importlib_resources!45
2 parents ef6ee24 + d6ab725 commit 021a488

File tree

10 files changed

+245
-147
lines changed

10 files changed

+245
-147
lines changed

importlib_resources/__init__.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import sys
44

5-
__version__ = '0.1.0'
5+
__version__ = '0.2'
66

77

88
if sys.version_info >= (3,):
99
from importlib_resources._py3 import (
10-
contents, is_resource, open, path, read, Package, Resource)
10+
Package, Resource, contents, is_resource, open_binary, open_text, path,
11+
read_binary, read_text)
1112
from importlib_resources.abc import ResourceReader
1213
else:
1314
from importlib_resources._py2 import (
14-
contents, is_resource, open, path, read)
15+
contents, is_resource, open_binary, open_text, path, read_binary,
16+
read_text)

importlib_resources/_py2.py

Lines changed: 49 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
import tempfile
44

55
from ._compat import FileNotFoundError
6-
from ._util import _wrap_file
76
from contextlib import contextmanager
87
from importlib import import_module
9-
from io import BytesIO, open as io_open
8+
from io import BytesIO, TextIOWrapper, open as io_open
109
from pathlib2 import Path
1110
from zipfile import ZipFile
1211

@@ -34,21 +33,48 @@ def _normalize_path(path):
3433
return file_name
3534

3635

37-
def open(package, resource, encoding=None, errors=None):
38-
"""Return a file-like object opened for reading of the resource."""
36+
def open_binary(package, resource):
37+
"""Return a file-like object opened for binary reading of the resource."""
38+
resource = _normalize_path(resource)
39+
package = _get_package(package)
40+
# Using pathlib doesn't work well here due to the lack of 'strict' argument
41+
# for pathlib.Path.resolve() prior to Python 3.6.
42+
package_path = os.path.dirname(package.__file__)
43+
relative_path = os.path.join(package_path, resource)
44+
full_path = os.path.abspath(relative_path)
45+
try:
46+
return io_open(full_path, 'rb')
47+
except IOError:
48+
# This might be a package in a zip file. zipimport provides a loader
49+
# with a functioning get_data() method, however we have to strip the
50+
# archive (i.e. the .zip file's name) off the front of the path. This
51+
# is because the zipimport loader in Python 2 doesn't actually follow
52+
# PEP 302. It should allow the full path, but actually requires that
53+
# the path be relative to the zip file.
54+
try:
55+
loader = package.__loader__
56+
full_path = relative_path[len(loader.archive)+1:]
57+
data = loader.get_data(full_path)
58+
except (IOError, AttributeError):
59+
package_name = package.__name__
60+
message = '{!r} resource not found in {!r}'.format(
61+
resource, package_name)
62+
raise FileNotFoundError(message)
63+
else:
64+
return BytesIO(data)
65+
66+
67+
def open_text(package, resource, encoding='utf-8', errors='strict'):
68+
"""Return a file-like object opened for text reading of the resource."""
3969
resource = _normalize_path(resource)
4070
package = _get_package(package)
4171
# Using pathlib doesn't work well here due to the lack of 'strict' argument
4272
# for pathlib.Path.resolve() prior to Python 3.6.
4373
package_path = os.path.dirname(package.__file__)
4474
relative_path = os.path.join(package_path, resource)
4575
full_path = os.path.abspath(relative_path)
46-
if encoding is None:
47-
args = dict(mode='rb')
48-
else:
49-
args = dict(mode='r', encoding=encoding, errors=errors)
5076
try:
51-
return io_open(full_path, **args)
77+
return io_open(full_path, mode='r', encoding=encoding, errors=errors)
5278
except IOError:
5379
# This might be a package in a zip file. zipimport provides a loader
5480
# with a functioning get_data() method, however we have to strip the
@@ -66,23 +92,27 @@ def open(package, resource, encoding=None, errors=None):
6692
resource, package_name)
6793
raise FileNotFoundError(message)
6894
else:
69-
return _wrap_file(BytesIO(data), encoding, errors)
95+
return TextIOWrapper(BytesIO(data), encoding, errors)
96+
97+
98+
def read_binary(package, resource):
99+
"""Return the binary contents of the resource."""
100+
resource = _normalize_path(resource)
101+
package = _get_package(package)
102+
with open_binary(package, resource) as fp:
103+
return fp.read()
70104

71105

72-
def read(package, resource, encoding='utf-8', errors='strict'):
106+
def read_text(package, resource, encoding='utf-8', errors='strict'):
73107
"""Return the decoded string of the resource.
74108
75109
The decoding-related arguments have the same semantics as those of
76110
bytes.decode().
77111
"""
78112
resource = _normalize_path(resource)
79113
package = _get_package(package)
80-
# Note this is **not** builtins.open()!
81-
with open(package, resource) as binary_file:
82-
contents = binary_file.read()
83-
if encoding is None:
84-
return contents
85-
return contents.decode(encoding=encoding, errors=errors)
114+
with open_text(package, resource, encoding, errors) as fp:
115+
return fp.read()
86116

87117

88118
@contextmanager
@@ -106,9 +136,8 @@ def path(package, resource):
106136
if file_path.exists():
107137
yield file_path
108138
else:
109-
# Note this is **not** builtins.open()!
110-
with open(package, resource) as fileobj:
111-
data = fileobj.read()
139+
with open_binary(package, resource) as fp:
140+
data = fp.read()
112141
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
113142
# blocks due to the need to close the temporary file to work on Windows
114143
# properly.

importlib_resources/_py3.py

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import tempfile
44

55
from . import abc as resources_abc
6-
from ._util import _wrap_file
76
from builtins import open as builtins_open
87
from contextlib import contextmanager
98
from importlib import import_module
@@ -13,7 +12,7 @@
1312
from types import ModuleType
1413
from typing import Iterator, Optional, Set, Union # noqa: F401
1514
from typing import cast
16-
from typing.io import IO
15+
from typing.io import BinaryIO, TextIO
1716
from zipfile import ZipFile
1817

1918

@@ -60,27 +59,54 @@ def _get_resource_reader(
6059
return None
6160

6261

63-
def open(package: Package,
64-
resource: Resource,
65-
encoding: str = None,
66-
errors: str = None) -> IO:
67-
"""Return a file-like object opened for reading of the resource."""
62+
def open_binary(package: Package, resource: Resource) -> BinaryIO:
63+
"""Return a file-like object opened for binary reading of the resource."""
6864
resource = _normalize_path(resource)
6965
package = _get_package(package)
7066
reader = _get_resource_reader(package)
7167
if reader is not None:
72-
return _wrap_file(reader.open_resource(resource), encoding, errors)
68+
return reader.open_resource(resource)
69+
# Using pathlib doesn't work well here due to the lack of 'strict'
70+
# argument for pathlib.Path.resolve() prior to Python 3.6.
71+
absolute_package_path = os.path.abspath(package.__spec__.origin)
72+
package_path = os.path.dirname(absolute_package_path)
73+
full_path = os.path.join(package_path, resource)
74+
try:
75+
return builtins_open(full_path, mode='rb')
76+
except IOError:
77+
# Just assume the loader is a resource loader; all the relevant
78+
# importlib.machinery loaders are and an AttributeError for
79+
# get_data() will make it clear what is needed from the loader.
80+
loader = cast(ResourceLoader, package.__spec__.loader)
81+
try:
82+
data = loader.get_data(full_path)
83+
except IOError:
84+
package_name = package.__spec__.name
85+
message = '{!r} resource not found in {!r}'.format(
86+
resource, package_name)
87+
raise FileNotFoundError(message)
88+
else:
89+
return BytesIO(data)
90+
91+
92+
def open_text(package: Package,
93+
resource: Resource,
94+
encoding: str = 'utf-8',
95+
errors: str = 'strict') -> TextIO:
96+
"""Return a file-like object opened for text reading of the resource."""
97+
resource = _normalize_path(resource)
98+
package = _get_package(package)
99+
reader = _get_resource_reader(package)
100+
if reader is not None:
101+
return TextIOWrapper(reader.open_resource(resource), encoding, errors)
73102
# Using pathlib doesn't work well here due to the lack of 'strict'
74103
# argument for pathlib.Path.resolve() prior to Python 3.6.
75104
absolute_package_path = os.path.abspath(package.__spec__.origin)
76105
package_path = os.path.dirname(absolute_package_path)
77106
full_path = os.path.join(package_path, resource)
78-
if encoding is None:
79-
args = dict(mode='rb')
80-
else:
81-
args = dict(mode='r', encoding=encoding, errors=errors)
82107
try:
83-
return builtins_open(full_path, **args) # type: ignore
108+
return builtins_open(
109+
full_path, mode='r', encoding=encoding, errors=errors)
84110
except IOError:
85111
# Just assume the loader is a resource loader; all the relevant
86112
# importlib.machinery loaders are and an AttributeError for
@@ -94,29 +120,30 @@ def open(package: Package,
94120
resource, package_name)
95121
raise FileNotFoundError(message)
96122
else:
97-
return _wrap_file(BytesIO(data), encoding, errors)
123+
return TextIOWrapper(BytesIO(data), encoding, errors)
124+
125+
126+
def read_binary(package: Package, resource: Resource) -> bytes:
127+
"""Return the binary contents of the resource."""
128+
resource = _normalize_path(resource)
129+
package = _get_package(package)
130+
with open_binary(package, resource) as fp:
131+
return fp.read()
98132

99133

100-
def read(package: Package,
101-
resource: Resource,
102-
encoding: str = 'utf-8',
103-
errors: str = 'strict') -> Union[str, bytes]:
134+
def read_text(package: Package,
135+
resource: Resource,
136+
encoding: str = 'utf-8',
137+
errors: str = 'strict') -> str:
104138
"""Return the decoded string of the resource.
105139
106140
The decoding-related arguments have the same semantics as those of
107141
bytes.decode().
108142
"""
109143
resource = _normalize_path(resource)
110144
package = _get_package(package)
111-
# Note this is **not** builtins.open()!
112-
with open(package, resource) as binary_file:
113-
if encoding is None:
114-
return binary_file.read()
115-
# Decoding from io.TextIOWrapper() instead of str.decode() in hopes
116-
# that the former will be smarter about memory usage.
117-
text_file = TextIOWrapper(
118-
binary_file, encoding=encoding, errors=errors)
119-
return text_file.read()
145+
with open_text(package, resource, encoding, errors) as fp:
146+
return fp.read()
120147

121148

122149
@contextmanager
@@ -145,8 +172,8 @@ def path(package: Package, resource: Resource) -> Iterator[Path]:
145172
if file_path.exists():
146173
yield file_path
147174
else:
148-
with open(package, resource) as file:
149-
data = file.read()
175+
with open_binary(package, resource) as fp:
176+
data = fp.read()
150177
# Not using tempfile.NamedTemporaryFile as it leads to deeper 'try'
151178
# blocks due to the need to close the temporary file to work on
152179
# Windows properly.

importlib_resources/_util.py

Lines changed: 0 additions & 7 deletions
This file was deleted.

0 commit comments

Comments
 (0)