blob: 4169171b189cca8f918d9f14f32df3a0b037f4fa [file] [log] [blame]
Barry Warsawdeae6b42017-12-30 15:18:06 -05001import os
Jason R. Coombsdf8d4c82020-10-25 14:21:46 -04002import io
Barry Warsawdeae6b42017-12-30 15:18:06 -05003
Jason R. Coombs7f7e7062020-05-08 19:20:26 -04004from . import _common
Jason R. Coombs843c2772020-06-07 21:00:51 -04005from ._common import as_file, files
Jason R. Coombsdf8d4c82020-10-25 14:21:46 -04006from contextlib import suppress
Barry Warsawdeae6b42017-12-30 15:18:06 -05007from importlib.abc import ResourceLoader
8from io import BytesIO, TextIOWrapper
9from pathlib import Path
10from types import ModuleType
Jason R. Coombs843c2772020-06-07 21:00:51 -040011from typing import ContextManager, Iterable, Union
Barry Warsawdeae6b42017-12-30 15:18:06 -050012from typing import cast
13from typing.io import BinaryIO, TextIO
Jason R. Coombsdf8d4c82020-10-25 14:21:46 -040014from collections.abc import Sequence
15from functools import singledispatch
Barry Warsawdeae6b42017-12-30 15:18:06 -050016
17
Barry Warsaw0ed66df2018-05-17 11:41:53 -040018__all__ = [
19 'Package',
20 'Resource',
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040021 'as_file',
Barry Warsaw0ed66df2018-05-17 11:41:53 -040022 'contents',
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040023 'files',
Barry Warsaw0ed66df2018-05-17 11:41:53 -040024 'is_resource',
25 'open_binary',
26 'open_text',
27 'path',
28 'read_binary',
29 'read_text',
30 ]
31
32
Barry Warsawdeae6b42017-12-30 15:18:06 -050033Package = Union[str, ModuleType]
34Resource = Union[str, os.PathLike]
35
36
Barry Warsawdeae6b42017-12-30 15:18:06 -050037def open_binary(package: Package, resource: Resource) -> BinaryIO:
38 """Return a file-like object opened for binary reading of the resource."""
Jason R. Coombs843c2772020-06-07 21:00:51 -040039 resource = _common.normalize_path(resource)
40 package = _common.get_package(package)
41 reader = _common.get_resource_reader(package)
Barry Warsawdeae6b42017-12-30 15:18:06 -050042 if reader is not None:
43 return reader.open_resource(resource)
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040044 absolute_package_path = os.path.abspath(
45 package.__spec__.origin or 'non-existent file')
Barry Warsawdeae6b42017-12-30 15:18:06 -050046 package_path = os.path.dirname(absolute_package_path)
47 full_path = os.path.join(package_path, resource)
48 try:
Barry Warsaw0ed66df2018-05-17 11:41:53 -040049 return open(full_path, mode='rb')
Barry Warsawdeae6b42017-12-30 15:18:06 -050050 except OSError:
51 # Just assume the loader is a resource loader; all the relevant
52 # importlib.machinery loaders are and an AttributeError for
53 # get_data() will make it clear what is needed from the loader.
54 loader = cast(ResourceLoader, package.__spec__.loader)
55 data = None
56 if hasattr(package.__spec__.loader, 'get_data'):
57 with suppress(OSError):
58 data = loader.get_data(full_path)
59 if data is None:
60 package_name = package.__spec__.name
61 message = '{!r} resource not found in {!r}'.format(
62 resource, package_name)
63 raise FileNotFoundError(message)
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040064 return BytesIO(data)
Barry Warsawdeae6b42017-12-30 15:18:06 -050065
66
67def open_text(package: Package,
68 resource: Resource,
69 encoding: str = 'utf-8',
70 errors: str = 'strict') -> TextIO:
71 """Return a file-like object opened for text reading of the resource."""
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040072 return TextIOWrapper(
73 open_binary(package, resource), encoding=encoding, errors=errors)
Barry Warsawdeae6b42017-12-30 15:18:06 -050074
75
76def read_binary(package: Package, resource: Resource) -> bytes:
77 """Return the binary contents of the resource."""
Barry Warsawdeae6b42017-12-30 15:18:06 -050078 with open_binary(package, resource) as fp:
79 return fp.read()
80
81
82def read_text(package: Package,
83 resource: Resource,
84 encoding: str = 'utf-8',
85 errors: str = 'strict') -> str:
86 """Return the decoded string of the resource.
87
88 The decoding-related arguments have the same semantics as those of
89 bytes.decode().
90 """
Barry Warsawdeae6b42017-12-30 15:18:06 -050091 with open_text(package, resource, encoding, errors) as fp:
92 return fp.read()
93
94
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040095def path(
96 package: Package, resource: Resource,
97 ) -> 'ContextManager[Path]':
Barry Warsawdeae6b42017-12-30 15:18:06 -050098 """A context manager providing a file path object to the resource.
99
100 If the resource does not already exist on its own on the file system,
101 a temporary file will be created. If the file was created, the file
102 will be deleted upon exiting the context manager (no exception is
103 raised if the file was deleted prior to the context manager
104 exiting).
105 """
Jason R. Coombs843c2772020-06-07 21:00:51 -0400106 reader = _common.get_resource_reader(_common.get_package(package))
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400107 return (
Jason R. Coombsdf8d4c82020-10-25 14:21:46 -0400108 _path_from_reader(reader, _common.normalize_path(resource))
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400109 if reader else
Jason R. Coombs843c2772020-06-07 21:00:51 -0400110 _common.as_file(
111 _common.files(package).joinpath(_common.normalize_path(resource)))
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400112 )
113
114
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400115def _path_from_reader(reader, resource):
Jason R. Coombsdf8d4c82020-10-25 14:21:46 -0400116 return _path_from_resource_path(reader, resource) or \
117 _path_from_open_resource(reader, resource)
118
119
120def _path_from_resource_path(reader, resource):
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400121 with suppress(FileNotFoundError):
Jason R. Coombsdf8d4c82020-10-25 14:21:46 -0400122 return Path(reader.resource_path(resource))
123
124
125def _path_from_open_resource(reader, resource):
126 saved = io.BytesIO(reader.open_resource(resource).read())
127 return _common._tempfile(saved.read, suffix=resource)
Barry Warsawdeae6b42017-12-30 15:18:06 -0500128
129
130def is_resource(package: Package, name: str) -> bool:
131 """True if 'name' is a resource inside 'package'.
132
133 Directories are *not* resources.
134 """
Jason R. Coombs843c2772020-06-07 21:00:51 -0400135 package = _common.get_package(package)
136 _common.normalize_path(name)
137 reader = _common.get_resource_reader(package)
Barry Warsawdeae6b42017-12-30 15:18:06 -0500138 if reader is not None:
139 return reader.is_resource(name)
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400140 package_contents = set(contents(package))
Barry Warsawdeae6b42017-12-30 15:18:06 -0500141 if name not in package_contents:
142 return False
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400143 return (_common.from_package(package) / name).is_file()
Barry Warsawdeae6b42017-12-30 15:18:06 -0500144
145
Brett Cannon3ab93652018-04-30 11:31:45 -0700146def contents(package: Package) -> Iterable[str]:
147 """Return an iterable of entries in 'package'.
Barry Warsawdeae6b42017-12-30 15:18:06 -0500148
149 Note that not all entries are resources. Specifically, directories are
150 not considered resources. Use `is_resource()` on each entry returned here
151 to check if it is a resource or not.
152 """
Jason R. Coombs843c2772020-06-07 21:00:51 -0400153 package = _common.get_package(package)
154 reader = _common.get_resource_reader(package)
Barry Warsawdeae6b42017-12-30 15:18:06 -0500155 if reader is not None:
Jason R. Coombsdf8d4c82020-10-25 14:21:46 -0400156 return _ensure_sequence(reader.contents())
Barry Warsawdeae6b42017-12-30 15:18:06 -0500157 # Is the package a namespace package? By definition, namespace packages
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400158 # cannot have resources.
159 namespace = (
160 package.__spec__.origin is None or
161 package.__spec__.origin == 'namespace'
162 )
163 if namespace or not package.__spec__.has_location:
Brett Cannon3ab93652018-04-30 11:31:45 -0700164 return ()
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400165 return list(item.name for item in _common.from_package(package).iterdir())
Jason R. Coombsdf8d4c82020-10-25 14:21:46 -0400166
167
168@singledispatch
169def _ensure_sequence(iterable):
170 return list(iterable)
171
172
173@_ensure_sequence.register(Sequence)
174def _(iterable):
175 return iterable