| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | import os | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  | from . import abc as resources_abc | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  | from . import _common | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  | from ._common import as_file | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | from contextlib import contextmanager, suppress | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  | from importlib import import_module | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | from importlib.abc import ResourceLoader | 
					
						
							|  |  |  | from io import BytesIO, TextIOWrapper | 
					
						
							|  |  |  | from pathlib import Path | 
					
						
							|  |  |  | from types import ModuleType | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  | from typing import ContextManager, Iterable, Optional, Union | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | from typing import cast | 
					
						
							|  |  |  | from typing.io import BinaryIO, TextIO | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-05-17 11:41:53 -04:00
										 |  |  | __all__ = [ | 
					
						
							|  |  |  |     'Package', | 
					
						
							|  |  |  |     'Resource', | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     'as_file', | 
					
						
							| 
									
										
										
										
											2018-05-17 11:41:53 -04:00
										 |  |  |     'contents', | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     'files', | 
					
						
							| 
									
										
										
										
											2018-05-17 11:41:53 -04:00
										 |  |  |     'is_resource', | 
					
						
							|  |  |  |     'open_binary', | 
					
						
							|  |  |  |     'open_text', | 
					
						
							|  |  |  |     'path', | 
					
						
							|  |  |  |     'read_binary', | 
					
						
							|  |  |  |     'read_text', | 
					
						
							|  |  |  |     ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | Package = Union[str, ModuleType] | 
					
						
							|  |  |  | Resource = Union[str, os.PathLike] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  | def _resolve(name) -> ModuleType: | 
					
						
							|  |  |  |     """If name is a string, resolve to a module.""" | 
					
						
							|  |  |  |     if hasattr(name, '__spec__'): | 
					
						
							|  |  |  |         return name | 
					
						
							|  |  |  |     return import_module(name) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _get_package(package) -> ModuleType: | 
					
						
							|  |  |  |     """Take a package name or module object and return the module.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     If a name, the module is imported.  If the resolved module | 
					
						
							|  |  |  |     object is not a package, raise an exception. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     module = _resolve(package) | 
					
						
							|  |  |  |     if module.__spec__.submodule_search_locations is None: | 
					
						
							|  |  |  |         raise TypeError('{!r} is not a package'.format(package)) | 
					
						
							|  |  |  |     return module | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _normalize_path(path) -> str: | 
					
						
							|  |  |  |     """Normalize a path by ensuring it is a string.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     If the resulting string contains path separators, an exception is raised. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     parent, file_name = os.path.split(path) | 
					
						
							|  |  |  |     if parent: | 
					
						
							|  |  |  |         raise ValueError('{!r} must be only a file name'.format(path)) | 
					
						
							|  |  |  |     return file_name | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _get_resource_reader( | 
					
						
							|  |  |  |         package: ModuleType) -> Optional[resources_abc.ResourceReader]: | 
					
						
							|  |  |  |     # Return the package's loader if it's a ResourceReader.  We can't use | 
					
						
							|  |  |  |     # a issubclass() check here because apparently abc.'s __subclasscheck__() | 
					
						
							|  |  |  |     # hook wants to create a weak reference to the object, but | 
					
						
							|  |  |  |     # zipimport.zipimporter does not support weak references, resulting in a | 
					
						
							|  |  |  |     # TypeError.  That seems terrible. | 
					
						
							|  |  |  |     spec = package.__spec__ | 
					
						
							|  |  |  |     if hasattr(spec.loader, 'get_resource_reader'): | 
					
						
							|  |  |  |         return cast(resources_abc.ResourceReader, | 
					
						
							|  |  |  |                     spec.loader.get_resource_reader(spec.name)) | 
					
						
							|  |  |  |     return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def _check_location(package): | 
					
						
							|  |  |  |     if package.__spec__.origin is None or not package.__spec__.has_location: | 
					
						
							|  |  |  |         raise FileNotFoundError(f'Package has no location {package!r}') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | def open_binary(package: Package, resource: Resource) -> BinaryIO: | 
					
						
							|  |  |  |     """Return a file-like object opened for binary reading of the resource.""" | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  |     resource = _normalize_path(resource) | 
					
						
							|  |  |  |     package = _get_package(package) | 
					
						
							|  |  |  |     reader = _get_resource_reader(package) | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  |     if reader is not None: | 
					
						
							|  |  |  |         return reader.open_resource(resource) | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     absolute_package_path = os.path.abspath( | 
					
						
							|  |  |  |         package.__spec__.origin or 'non-existent file') | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  |     package_path = os.path.dirname(absolute_package_path) | 
					
						
							|  |  |  |     full_path = os.path.join(package_path, resource) | 
					
						
							|  |  |  |     try: | 
					
						
							| 
									
										
										
										
											2018-05-17 11:41:53 -04:00
										 |  |  |         return open(full_path, mode='rb') | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  |     except OSError: | 
					
						
							|  |  |  |         # Just assume the loader is a resource loader; all the relevant | 
					
						
							|  |  |  |         # importlib.machinery loaders are and an AttributeError for | 
					
						
							|  |  |  |         # get_data() will make it clear what is needed from the loader. | 
					
						
							|  |  |  |         loader = cast(ResourceLoader, package.__spec__.loader) | 
					
						
							|  |  |  |         data = None | 
					
						
							|  |  |  |         if hasattr(package.__spec__.loader, 'get_data'): | 
					
						
							|  |  |  |             with suppress(OSError): | 
					
						
							|  |  |  |                 data = loader.get_data(full_path) | 
					
						
							|  |  |  |         if data is None: | 
					
						
							|  |  |  |             package_name = package.__spec__.name | 
					
						
							|  |  |  |             message = '{!r} resource not found in {!r}'.format( | 
					
						
							|  |  |  |                 resource, package_name) | 
					
						
							|  |  |  |             raise FileNotFoundError(message) | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |         return BytesIO(data) | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def open_text(package: Package, | 
					
						
							|  |  |  |               resource: Resource, | 
					
						
							|  |  |  |               encoding: str = 'utf-8', | 
					
						
							|  |  |  |               errors: str = 'strict') -> TextIO: | 
					
						
							|  |  |  |     """Return a file-like object opened for text reading of the resource.""" | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     return TextIOWrapper( | 
					
						
							|  |  |  |         open_binary(package, resource), encoding=encoding, errors=errors) | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def read_binary(package: Package, resource: Resource) -> bytes: | 
					
						
							|  |  |  |     """Return the binary contents of the resource.""" | 
					
						
							|  |  |  |     with open_binary(package, resource) as fp: | 
					
						
							|  |  |  |         return fp.read() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def read_text(package: Package, | 
					
						
							|  |  |  |               resource: Resource, | 
					
						
							|  |  |  |               encoding: str = 'utf-8', | 
					
						
							|  |  |  |               errors: str = 'strict') -> str: | 
					
						
							|  |  |  |     """Return the decoded string of the resource.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     The decoding-related arguments have the same semantics as those of | 
					
						
							|  |  |  |     bytes.decode(). | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     with open_text(package, resource, encoding, errors) as fp: | 
					
						
							|  |  |  |         return fp.read() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  | def files(package: Package) -> resources_abc.Traversable: | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     Get a Traversable resource from a package | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     return _common.from_package(_get_package(package)) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  | def path( | 
					
						
							|  |  |  |         package: Package, resource: Resource, | 
					
						
							|  |  |  |         ) -> 'ContextManager[Path]': | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  |     """A context manager providing a file path object to the resource.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     If the resource does not already exist on its own on the file system, | 
					
						
							|  |  |  |     a temporary file will be created. If the file was created, the file | 
					
						
							|  |  |  |     will be deleted upon exiting the context manager (no exception is | 
					
						
							|  |  |  |     raised if the file was deleted prior to the context manager | 
					
						
							|  |  |  |     exiting). | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  |     reader = _get_resource_reader(_get_package(package)) | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     return ( | 
					
						
							|  |  |  |         _path_from_reader(reader, resource) | 
					
						
							|  |  |  |         if reader else | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  |         _common.as_file(files(package).joinpath(_normalize_path(resource))) | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |         ) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | @contextmanager | 
					
						
							|  |  |  | def _path_from_reader(reader, resource): | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  |     norm_resource = _normalize_path(resource) | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     with suppress(FileNotFoundError): | 
					
						
							|  |  |  |         yield Path(reader.resource_path(norm_resource)) | 
					
						
							|  |  |  |         return | 
					
						
							|  |  |  |     opener_reader = reader.open_resource(norm_resource) | 
					
						
							|  |  |  |     with _common._tempfile(opener_reader.read, suffix=norm_resource) as res: | 
					
						
							|  |  |  |         yield res | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def is_resource(package: Package, name: str) -> bool: | 
					
						
							|  |  |  |     """True if 'name' is a resource inside 'package'.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     Directories are *not* resources. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  |     package = _get_package(package) | 
					
						
							|  |  |  |     _normalize_path(name) | 
					
						
							|  |  |  |     reader = _get_resource_reader(package) | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  |     if reader is not None: | 
					
						
							|  |  |  |         return reader.is_resource(name) | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     package_contents = set(contents(package)) | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  |     if name not in package_contents: | 
					
						
							|  |  |  |         return False | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     return (_common.from_package(package) / name).is_file() | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-04-30 11:31:45 -07:00
										 |  |  | def contents(package: Package) -> Iterable[str]: | 
					
						
							|  |  |  |     """Return an iterable of entries in 'package'.
 | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |     Note that not all entries are resources.  Specifically, directories are | 
					
						
							|  |  |  |     not considered resources.  Use `is_resource()` on each entry returned here | 
					
						
							|  |  |  |     to check if it is a resource or not. | 
					
						
							|  |  |  |     """
 | 
					
						
							| 
									
										
										
										
											2020-06-09 19:50:01 +02:00
										 |  |  |     package = _get_package(package) | 
					
						
							|  |  |  |     reader = _get_resource_reader(package) | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  |     if reader is not None: | 
					
						
							| 
									
										
										
										
											2018-04-30 11:31:45 -07:00
										 |  |  |         return reader.contents() | 
					
						
							| 
									
										
										
										
											2017-12-30 15:18:06 -05:00
										 |  |  |     # Is the package a namespace package?  By definition, namespace packages | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     # cannot have resources. | 
					
						
							|  |  |  |     namespace = ( | 
					
						
							|  |  |  |         package.__spec__.origin is None or | 
					
						
							|  |  |  |         package.__spec__.origin == 'namespace' | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |     if namespace or not package.__spec__.has_location: | 
					
						
							| 
									
										
										
										
											2018-04-30 11:31:45 -07:00
										 |  |  |         return () | 
					
						
							| 
									
										
										
										
											2020-05-08 19:20:26 -04:00
										 |  |  |     return list(item.name for item in _common.from_package(package).iterdir()) |