blob: 4535619f4f014339c5a3e0cb02b7e9c1896964c2 [file] [log] [blame]
Barry Warsawdeae6b42017-12-30 15:18:06 -05001import os
Barry Warsawdeae6b42017-12-30 15:18:06 -05002
Jason R. Coombs7f7e7062020-05-08 19:20:26 -04003from . import _common
Jason R. Coombs843c2772020-06-07 21:00:51 -04004from ._common import as_file, files
Barry Warsawdeae6b42017-12-30 15:18:06 -05005from contextlib import contextmanager, suppress
Barry Warsawdeae6b42017-12-30 15:18:06 -05006from importlib.abc import ResourceLoader
7from io import BytesIO, TextIOWrapper
8from pathlib import Path
9from types import ModuleType
Jason R. Coombs843c2772020-06-07 21:00:51 -040010from typing import ContextManager, Iterable, Union
Barry Warsawdeae6b42017-12-30 15:18:06 -050011from typing import cast
12from typing.io import BinaryIO, TextIO
Barry Warsawdeae6b42017-12-30 15:18:06 -050013
14
Barry Warsaw0ed66df2018-05-17 11:41:53 -040015__all__ = [
16 'Package',
17 'Resource',
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040018 'as_file',
Barry Warsaw0ed66df2018-05-17 11:41:53 -040019 'contents',
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040020 'files',
Barry Warsaw0ed66df2018-05-17 11:41:53 -040021 'is_resource',
22 'open_binary',
23 'open_text',
24 'path',
25 'read_binary',
26 'read_text',
27 ]
28
29
Barry Warsawdeae6b42017-12-30 15:18:06 -050030Package = Union[str, ModuleType]
31Resource = Union[str, os.PathLike]
32
33
Barry Warsawdeae6b42017-12-30 15:18:06 -050034def open_binary(package: Package, resource: Resource) -> BinaryIO:
35 """Return a file-like object opened for binary reading of the resource."""
Jason R. Coombs843c2772020-06-07 21:00:51 -040036 resource = _common.normalize_path(resource)
37 package = _common.get_package(package)
38 reader = _common.get_resource_reader(package)
Barry Warsawdeae6b42017-12-30 15:18:06 -050039 if reader is not None:
40 return reader.open_resource(resource)
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040041 absolute_package_path = os.path.abspath(
42 package.__spec__.origin or 'non-existent file')
Barry Warsawdeae6b42017-12-30 15:18:06 -050043 package_path = os.path.dirname(absolute_package_path)
44 full_path = os.path.join(package_path, resource)
45 try:
Barry Warsaw0ed66df2018-05-17 11:41:53 -040046 return open(full_path, mode='rb')
Barry Warsawdeae6b42017-12-30 15:18:06 -050047 except OSError:
48 # Just assume the loader is a resource loader; all the relevant
49 # importlib.machinery loaders are and an AttributeError for
50 # get_data() will make it clear what is needed from the loader.
51 loader = cast(ResourceLoader, package.__spec__.loader)
52 data = None
53 if hasattr(package.__spec__.loader, 'get_data'):
54 with suppress(OSError):
55 data = loader.get_data(full_path)
56 if data is None:
57 package_name = package.__spec__.name
58 message = '{!r} resource not found in {!r}'.format(
59 resource, package_name)
60 raise FileNotFoundError(message)
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040061 return BytesIO(data)
Barry Warsawdeae6b42017-12-30 15:18:06 -050062
63
64def open_text(package: Package,
65 resource: Resource,
66 encoding: str = 'utf-8',
67 errors: str = 'strict') -> TextIO:
68 """Return a file-like object opened for text reading of the resource."""
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040069 return TextIOWrapper(
70 open_binary(package, resource), encoding=encoding, errors=errors)
Barry Warsawdeae6b42017-12-30 15:18:06 -050071
72
73def read_binary(package: Package, resource: Resource) -> bytes:
74 """Return the binary contents of the resource."""
Barry Warsawdeae6b42017-12-30 15:18:06 -050075 with open_binary(package, resource) as fp:
76 return fp.read()
77
78
79def read_text(package: Package,
80 resource: Resource,
81 encoding: str = 'utf-8',
82 errors: str = 'strict') -> str:
83 """Return the decoded string of the resource.
84
85 The decoding-related arguments have the same semantics as those of
86 bytes.decode().
87 """
Barry Warsawdeae6b42017-12-30 15:18:06 -050088 with open_text(package, resource, encoding, errors) as fp:
89 return fp.read()
90
91
Jason R. Coombs7f7e7062020-05-08 19:20:26 -040092def path(
93 package: Package, resource: Resource,
94 ) -> 'ContextManager[Path]':
Barry Warsawdeae6b42017-12-30 15:18:06 -050095 """A context manager providing a file path object to the resource.
96
97 If the resource does not already exist on its own on the file system,
98 a temporary file will be created. If the file was created, the file
99 will be deleted upon exiting the context manager (no exception is
100 raised if the file was deleted prior to the context manager
101 exiting).
102 """
Jason R. Coombs843c2772020-06-07 21:00:51 -0400103 reader = _common.get_resource_reader(_common.get_package(package))
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400104 return (
105 _path_from_reader(reader, resource)
106 if reader else
Jason R. Coombs843c2772020-06-07 21:00:51 -0400107 _common.as_file(
108 _common.files(package).joinpath(_common.normalize_path(resource)))
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400109 )
110
111
112@contextmanager
113def _path_from_reader(reader, resource):
Jason R. Coombs843c2772020-06-07 21:00:51 -0400114 norm_resource = _common.normalize_path(resource)
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400115 with suppress(FileNotFoundError):
116 yield Path(reader.resource_path(norm_resource))
117 return
118 opener_reader = reader.open_resource(norm_resource)
119 with _common._tempfile(opener_reader.read, suffix=norm_resource) as res:
120 yield res
Barry Warsawdeae6b42017-12-30 15:18:06 -0500121
122
123def is_resource(package: Package, name: str) -> bool:
124 """True if 'name' is a resource inside 'package'.
125
126 Directories are *not* resources.
127 """
Jason R. Coombs843c2772020-06-07 21:00:51 -0400128 package = _common.get_package(package)
129 _common.normalize_path(name)
130 reader = _common.get_resource_reader(package)
Barry Warsawdeae6b42017-12-30 15:18:06 -0500131 if reader is not None:
132 return reader.is_resource(name)
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400133 package_contents = set(contents(package))
Barry Warsawdeae6b42017-12-30 15:18:06 -0500134 if name not in package_contents:
135 return False
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400136 return (_common.from_package(package) / name).is_file()
Barry Warsawdeae6b42017-12-30 15:18:06 -0500137
138
Brett Cannon3ab93652018-04-30 11:31:45 -0700139def contents(package: Package) -> Iterable[str]:
140 """Return an iterable of entries in 'package'.
Barry Warsawdeae6b42017-12-30 15:18:06 -0500141
142 Note that not all entries are resources. Specifically, directories are
143 not considered resources. Use `is_resource()` on each entry returned here
144 to check if it is a resource or not.
145 """
Jason R. Coombs843c2772020-06-07 21:00:51 -0400146 package = _common.get_package(package)
147 reader = _common.get_resource_reader(package)
Barry Warsawdeae6b42017-12-30 15:18:06 -0500148 if reader is not None:
Brett Cannon3ab93652018-04-30 11:31:45 -0700149 return reader.contents()
Barry Warsawdeae6b42017-12-30 15:18:06 -0500150 # Is the package a namespace package? By definition, namespace packages
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400151 # cannot have resources.
152 namespace = (
153 package.__spec__.origin is None or
154 package.__spec__.origin == 'namespace'
155 )
156 if namespace or not package.__spec__.has_location:
Brett Cannon3ab93652018-04-30 11:31:45 -0700157 return ()
Jason R. Coombs7f7e7062020-05-08 19:20:26 -0400158 return list(item.name for item in _common.from_package(package).iterdir())