33import tempfile
44
55from . import abc as resources_abc
6- from ._util import _wrap_file
76from builtins import open as builtins_open
87from contextlib import contextmanager
98from importlib import import_module
1312from types import ModuleType
1413from typing import Iterator , Optional , Set , Union # noqa: F401
1514from typing import cast
16- from typing .io import IO
15+ from typing .io import BinaryIO , TextIO
1716from 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.
0 commit comments