1import abc 2import io 3import os 4from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional 5from typing import runtime_checkable, Protocol 6from typing import Union 7 8 9StrPath = Union[str, os.PathLike[str]] 10 11__all__ = ["ResourceReader", "Traversable", "TraversableResources"] 12 13 14class ResourceReader(metaclass=abc.ABCMeta): 15 """Abstract base class for loaders to provide resource reading support.""" 16 17 @abc.abstractmethod 18 def open_resource(self, resource: Text) -> BinaryIO: 19 """Return an opened, file-like object for binary reading. 20 21 The 'resource' argument is expected to represent only a file name. 22 If the resource cannot be found, FileNotFoundError is raised. 23 """ 24 # This deliberately raises FileNotFoundError instead of 25 # NotImplementedError so that if this method is accidentally called, 26 # it'll still do the right thing. 27 raise FileNotFoundError 28 29 @abc.abstractmethod 30 def resource_path(self, resource: Text) -> Text: 31 """Return the file system path to the specified resource. 32 33 The 'resource' argument is expected to represent only a file name. 34 If the resource does not exist on the file system, raise 35 FileNotFoundError. 36 """ 37 # This deliberately raises FileNotFoundError instead of 38 # NotImplementedError so that if this method is accidentally called, 39 # it'll still do the right thing. 40 raise FileNotFoundError 41 42 @abc.abstractmethod 43 def is_resource(self, path: Text) -> bool: 44 """Return True if the named 'path' is a resource. 45 46 Files are resources, directories are not. 47 """ 48 raise FileNotFoundError 49 50 @abc.abstractmethod 51 def contents(self) -> Iterable[str]: 52 """Return an iterable of entries in `package`.""" 53 raise FileNotFoundError 54 55 56@runtime_checkable 57class Traversable(Protocol): 58 """ 59 An object with a subset of pathlib.Path methods suitable for 60 traversing directories and opening files. 61 62 Any exceptions that occur when accessing the backing resource 63 may propagate unaltered. 64 """ 65 66 @abc.abstractmethod 67 def iterdir(self) -> Iterator["Traversable"]: 68 """ 69 Yield Traversable objects in self 70 """ 71 72 def read_bytes(self) -> bytes: 73 """ 74 Read contents of self as bytes 75 """ 76 with self.open('rb') as strm: 77 return strm.read() 78 79 def read_text(self, encoding: Optional[str] = None) -> str: 80 """ 81 Read contents of self as text 82 """ 83 with self.open(encoding=encoding) as strm: 84 return strm.read() 85 86 @abc.abstractmethod 87 def is_dir(self) -> bool: 88 """ 89 Return True if self is a directory 90 """ 91 92 @abc.abstractmethod 93 def is_file(self) -> bool: 94 """ 95 Return True if self is a file 96 """ 97 98 @abc.abstractmethod 99 def joinpath(self, *descendants: StrPath) -> "Traversable": 100 """ 101 Return Traversable resolved with any descendants applied. 102 103 Each descendant should be a path segment relative to self 104 and each may contain multiple levels separated by 105 ``posixpath.sep`` (``/``). 106 """ 107 108 def __truediv__(self, child: StrPath) -> "Traversable": 109 """ 110 Return Traversable child in self 111 """ 112 return self.joinpath(child) 113 114 @abc.abstractmethod 115 def open(self, mode='r', *args, **kwargs): 116 """ 117 mode may be 'r' or 'rb' to open as text or binary. Return a handle 118 suitable for reading (same as pathlib.Path.open). 119 120 When opening as text, accepts encoding parameters such as those 121 accepted by io.TextIOWrapper. 122 """ 123 124 @abc.abstractproperty 125 def name(self) -> str: 126 """ 127 The base name of this object without any parent references. 128 """ 129 130 131class TraversableResources(ResourceReader): 132 """ 133 The required interface for providing traversable 134 resources. 135 """ 136 137 @abc.abstractmethod 138 def files(self) -> "Traversable": 139 """Return a Traversable object for the loaded package.""" 140 141 def open_resource(self, resource: StrPath) -> io.BufferedReader: 142 return self.files().joinpath(resource).open('rb') 143 144 def resource_path(self, resource: Any) -> NoReturn: 145 raise FileNotFoundError(resource) 146 147 def is_resource(self, path: StrPath) -> bool: 148 return self.files().joinpath(path).is_file() 149 150 def contents(self) -> Iterator[str]: 151 return (item.name for item in self.files().iterdir()) 152