2021-12-31 03:00:48 +01:00
|
|
|
import abc
|
2022-04-17 17:10:36 +02:00
|
|
|
import io
|
2022-10-16 21:00:39 +02:00
|
|
|
import itertools
|
2022-04-17 17:10:36 +02:00
|
|
|
import os
|
2022-10-16 21:00:39 +02:00
|
|
|
import pathlib
|
2022-04-17 17:10:36 +02:00
|
|
|
from typing import Any, BinaryIO, Iterable, Iterator, NoReturn, Text, Optional
|
2021-12-31 03:00:48 +01:00
|
|
|
from typing import runtime_checkable, Protocol
|
2022-04-17 17:10:36 +02:00
|
|
|
from typing import Union
|
|
|
|
|
|
|
|
|
|
|
|
StrPath = Union[str, os.PathLike[str]]
|
|
|
|
|
|
|
|
__all__ = ["ResourceReader", "Traversable", "TraversableResources"]
|
2021-12-31 03:00:48 +01: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 21:00:39 +02:00
|
|
|
class TraversalError(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2021-12-31 03:00:48 +01: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 17:10:36 +02:00
|
|
|
|
|
|
|
Any exceptions that occur when accessing the backing resource
|
|
|
|
may propagate unaltered.
|
2021-12-31 03:00:48 +01:00
|
|
|
"""
|
|
|
|
|
|
|
|
@abc.abstractmethod
|
2022-04-17 17:10:36 +02:00
|
|
|
def iterdir(self) -> Iterator["Traversable"]:
|
2021-12-31 03:00:48 +01:00
|
|
|
"""
|
|
|
|
Yield Traversable objects in self
|
|
|
|
"""
|
|
|
|
|
2022-04-17 17:10:36 +02:00
|
|
|
def read_bytes(self) -> bytes:
|
2021-12-31 03:00:48 +01:00
|
|
|
"""
|
|
|
|
Read contents of self as bytes
|
|
|
|
"""
|
|
|
|
with self.open('rb') as strm:
|
|
|
|
return strm.read()
|
|
|
|
|
2022-04-17 17:10:36 +02:00
|
|
|
def read_text(self, encoding: Optional[str] = None) -> str:
|
2021-12-31 03:00:48 +01: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 17:10:36 +02:00
|
|
|
def joinpath(self, *descendants: StrPath) -> "Traversable":
|
2021-12-31 03:00:48 +01:00
|
|
|
"""
|
2022-04-17 17:10:36 +02: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-31 03:00:48 +01:00
|
|
|
"""
|
2022-10-16 21:00:39 +02: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-31 03:00:48 +01:00
|
|
|
|
2022-04-17 17:10:36 +02:00
|
|
|
def __truediv__(self, child: StrPath) -> "Traversable":
|
2021-12-31 03:00:48 +01: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.
|
|
|
|
"""
|
|
|
|
|
2023-01-01 17:07:32 +01:00
|
|
|
@property
|
|
|
|
@abc.abstractmethod
|
2021-12-31 03:00:48 +01:00
|
|
|
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 17:10:36 +02:00
|
|
|
def files(self) -> "Traversable":
|
2021-12-31 03:00:48 +01:00
|
|
|
"""Return a Traversable object for the loaded package."""
|
|
|
|
|
2022-04-17 17:10:36 +02:00
|
|
|
def open_resource(self, resource: StrPath) -> io.BufferedReader:
|
2021-12-31 03:00:48 +01:00
|
|
|
return self.files().joinpath(resource).open('rb')
|
|
|
|
|
2022-04-17 17:10:36 +02:00
|
|
|
def resource_path(self, resource: Any) -> NoReturn:
|
2021-12-31 03:00:48 +01:00
|
|
|
raise FileNotFoundError(resource)
|
|
|
|
|
2022-04-17 17:10:36 +02:00
|
|
|
def is_resource(self, path: StrPath) -> bool:
|
2021-12-31 03:00:48 +01:00
|
|
|
return self.files().joinpath(path).is_file()
|
|
|
|
|
2022-04-17 17:10:36 +02:00
|
|
|
def contents(self) -> Iterator[str]:
|
2021-12-31 03:00:48 +01:00
|
|
|
return (item.name for item in self.files().iterdir())
|