| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  | import abc | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  | import io | 
					
						
							| 
									
										
										
										
											2022-10-16 15:00:39 -04:00
										 |  |  | import itertools | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  | import os | 
					
						
							| 
									
										
										
										
											2022-10-16 15:00:39 -04:00
										 |  |  | import pathlib | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  | from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  | from typing import runtime_checkable, Protocol | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  | from typing import Union | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | StrPath = Union[str, os.PathLike[str]] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | __all__ = ["ResourceReader", "Traversable", "TraversableResources"] | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ResourceReader(metaclass=abc.ABCMeta): | 
					
						
							|  |  |  |     """Abstract base class for loaders to provide resource reading support.""" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							|  |  |  |     def open_resource(self, resource: Text) -> BinaryIO: | 
					
						
							|  |  |  |         """Return an opened, file-like object for binary reading.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         The 'resource' argument is expected to represent only a file name. | 
					
						
							|  |  |  |         If the resource cannot be found, FileNotFoundError is raised. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         # This deliberately raises FileNotFoundError instead of | 
					
						
							|  |  |  |         # NotImplementedError so that if this method is accidentally called, | 
					
						
							|  |  |  |         # it'll still do the right thing. | 
					
						
							|  |  |  |         raise FileNotFoundError | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							|  |  |  |     def resource_path(self, resource: Text) -> Text: | 
					
						
							|  |  |  |         """Return the file system path to the specified resource.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         The 'resource' argument is expected to represent only a file name. | 
					
						
							|  |  |  |         If the resource does not exist on the file system, raise | 
					
						
							|  |  |  |         FileNotFoundError. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         # This deliberately raises FileNotFoundError instead of | 
					
						
							|  |  |  |         # NotImplementedError so that if this method is accidentally called, | 
					
						
							|  |  |  |         # it'll still do the right thing. | 
					
						
							|  |  |  |         raise FileNotFoundError | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							|  |  |  |     def is_resource(self, path: Text) -> bool: | 
					
						
							|  |  |  |         """Return True if the named 'path' is a resource.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Files are resources, directories are not. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         raise FileNotFoundError | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							|  |  |  |     def contents(self) -> Iterable[str]: | 
					
						
							|  |  |  |         """Return an iterable of entries in `package`.""" | 
					
						
							|  |  |  |         raise FileNotFoundError | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-10-16 15:00:39 -04:00
										 |  |  | class TraversalError(Exception): | 
					
						
							|  |  |  |     pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  | @runtime_checkable | 
					
						
							|  |  |  | class Traversable(Protocol): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     An object with a subset of pathlib.Path methods suitable for | 
					
						
							|  |  |  |     traversing directories and opening files. | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     Any exceptions that occur when accessing the backing resource | 
					
						
							|  |  |  |     may propagate unaltered. | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def iterdir(self) -> Iterator["Traversable"]: | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Yield Traversable objects in self | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def read_bytes(self) -> bytes: | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Read contents of self as bytes | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         with self.open('rb') as strm: | 
					
						
							|  |  |  |             return strm.read() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def read_text(self, encoding: Optional[str] = None) -> str: | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Read contents of self as text | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         with self.open(encoding=encoding) as strm: | 
					
						
							|  |  |  |             return strm.read() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							|  |  |  |     def is_dir(self) -> bool: | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Return True if self is a directory | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							|  |  |  |     def is_file(self) -> bool: | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         Return True if self is a file | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def joinpath(self, *descendants: StrPath) -> "Traversable": | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |         Return Traversable resolved with any descendants applied. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         Each descendant should be a path segment relative to self | 
					
						
							|  |  |  |         and each may contain multiple levels separated by | 
					
						
							|  |  |  |         ``posixpath.sep`` (``/``). | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         """
 | 
					
						
							| 
									
										
										
										
											2022-10-16 15:00:39 -04:00
										 |  |  |         if not descendants: | 
					
						
							|  |  |  |             return self | 
					
						
							|  |  |  |         names = itertools.chain.from_iterable( | 
					
						
							|  |  |  |             path.parts for path in map(pathlib.PurePosixPath, descendants) | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         target = next(names) | 
					
						
							|  |  |  |         matches = ( | 
					
						
							|  |  |  |             traversable for traversable in self.iterdir() if traversable.name == target | 
					
						
							|  |  |  |         ) | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             match = next(matches) | 
					
						
							|  |  |  |         except StopIteration: | 
					
						
							|  |  |  |             raise TraversalError( | 
					
						
							|  |  |  |                 "Target not found during traversal.", target, list(names) | 
					
						
							|  |  |  |             ) | 
					
						
							|  |  |  |         return match.joinpath(*names) | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def __truediv__(self, child: StrPath) -> "Traversable": | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         """
 | 
					
						
							|  |  |  |         Return Traversable child in self | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         return self.joinpath(child) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							|  |  |  |     def open(self, mode='r', *args, **kwargs): | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         mode may be 'r' or 'rb' to open as text or binary. Return a handle | 
					
						
							|  |  |  |         suitable for reading (same as pathlib.Path.open). | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         When opening as text, accepts encoding parameters such as those | 
					
						
							|  |  |  |         accepted by io.TextIOWrapper. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractproperty | 
					
						
							|  |  |  |     def name(self) -> str: | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  |         The base name of this object without any parent references. | 
					
						
							|  |  |  |         """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class TraversableResources(ResourceReader): | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  |     The required interface for providing traversable | 
					
						
							|  |  |  |     resources. | 
					
						
							|  |  |  |     """
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     @abc.abstractmethod | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def files(self) -> "Traversable": | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         """Return a Traversable object for the loaded package.""" | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def open_resource(self, resource: StrPath) -> io.BufferedReader: | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         return self.files().joinpath(resource).open('rb') | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def resource_path(self, resource: Any) -> NoReturn: | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         raise FileNotFoundError(resource) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def is_resource(self, path: StrPath) -> bool: | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         return self.files().joinpath(path).is_file() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-04-17 11:10:36 -04:00
										 |  |  |     def contents(self) -> Iterator[str]: | 
					
						
							| 
									
										
										
										
											2021-12-30 21:00:48 -05:00
										 |  |  |         return (item.name for item in self.files().iterdir()) |