abc.py 5.0 KB

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