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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy