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