Coverage for python/lsst/resources/packageresource.py: 98%

89 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-15 02:25 -0700

1# This file is part of lsst-resources. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# Use of this source code is governed by a 3-clause BSD-style 

10# license that can be found in the LICENSE file. 

11 

12from __future__ import annotations 

13 

14__all__ = ("PackageResourcePath",) 

15 

16import contextlib 

17import logging 

18import re 

19import sys 

20 

21if sys.version_info < (3, 11, 0): 21 ↛ 28line 21 didn't jump to line 28, because the condition on line 21 was never false

22 # Mypy will try to use the first import it encounters and ignores 

23 # the sys.version_info. This means that the first import has to be 

24 # the backwards compatibility import since we are currently using 3.10 

25 # for mypy. Once we switch to 3.11 for mypy the order will have to change. 

26 import importlib_resources as resources 

27else: 

28 from importlib import resources # type: ignore[no-redef] 

29 

30from collections.abc import Iterator 

31from typing import TYPE_CHECKING 

32 

33from ._resourceHandles._baseResourceHandle import ResourceHandleProtocol 

34from ._resourcePath import ResourcePath 

35 

36if TYPE_CHECKING: 

37 import urllib.parse 

38 

39log = logging.getLogger(__name__) 

40 

41 

42class PackageResourcePath(ResourcePath): 

43 """URI referring to a Python package resource. 

44 

45 These URIs look like: ``resource://lsst.daf.butler/configs/file.yaml`` 

46 where the network location is the Python package and the path is the 

47 resource name. 

48 """ 

49 

50 @classmethod 

51 def _fixDirectorySep( 

52 cls, parsed: urllib.parse.ParseResult, forceDirectory: bool = False 

53 ) -> tuple[urllib.parse.ParseResult, bool]: 

54 """Ensure that a path separator is present on directory paths.""" 

55 parsed, dirLike = super()._fixDirectorySep(parsed, forceDirectory=forceDirectory) 

56 if not dirLike: 

57 try: 

58 # If the resource location does not exist this can 

59 # fail immediately. It is possible we are doing path 

60 # manipulation and not wanting to read the resource now, 

61 # so catch the error and move on. 

62 ref = resources.files(parsed.netloc).joinpath(parsed.path.lstrip("/")) 

63 except ModuleNotFoundError: 

64 pass 

65 else: 

66 dirLike = ref.is_dir() 

67 return parsed, dirLike 

68 

69 def _get_ref(self) -> resources.abc.Traversable | None: 

70 """Obtain the object representing the resource. 

71 

72 Returns 

73 ------- 

74 path : `resources.abc.Traversable` or `None` 

75 The reference to the resource path, or `None` if the module 

76 associated with the resources is not accessible. This can happen 

77 if Python can't import the Python package defining the resource. 

78 """ 

79 try: 

80 ref = resources.files(self.netloc).joinpath(self.relativeToPathRoot) 

81 except ModuleNotFoundError: 

82 return None 

83 return ref 

84 

85 def isdir(self) -> bool: 

86 """Return True if this URI is a directory, else False.""" 

87 if self.dirLike: # Always bypass if we guessed the resource is a directory. 

88 return True 

89 ref = self._get_ref() 

90 if ref is None: 

91 return False # Does not seem to exist so assume not a directory. 

92 return ref.is_dir() 

93 

94 def exists(self) -> bool: 

95 """Check that the python resource exists.""" 

96 ref = self._get_ref() 

97 if ref is None: 

98 return False 

99 return ref.is_file() or ref.is_dir() 

100 

101 def read(self, size: int = -1) -> bytes: 

102 """Read the contents of the resource.""" 

103 ref = self._get_ref() 

104 if not ref: 

105 raise FileNotFoundError(f"Unable to locate resource {self}.") 

106 with ref.open("rb") as fh: 

107 return fh.read(size) 

108 

109 @contextlib.contextmanager 

110 def as_local(self) -> Iterator[ResourcePath]: 

111 """Return the location of the Python resource as local file. 

112 

113 Yields 

114 ------ 

115 local : `ResourcePath` 

116 This might be the original resource or a copy on the local file 

117 system. 

118 

119 Notes 

120 ----- 

121 The context manager will automatically delete any local temporary 

122 file. 

123 

124 Examples 

125 -------- 

126 Should be used as a context manager: 

127 

128 .. code-block:: py 

129 

130 with uri.as_local() as local: 

131 ospath = local.ospath 

132 """ 

133 ref = self._get_ref() 

134 if ref is None: 

135 raise FileNotFoundError(f"Resource {self} could not be located.") 

136 if ref.is_dir(): 

137 raise IsADirectoryError(f"Directory-like URI {self} cannot be fetched as local.") 

138 

139 with resources.as_file(ref) as file: 

140 yield ResourcePath(file) 

141 

142 @contextlib.contextmanager 

143 def open( 

144 self, 

145 mode: str = "r", 

146 *, 

147 encoding: str | None = None, 

148 prefer_file_temporary: bool = False, 

149 ) -> Iterator[ResourceHandleProtocol]: 

150 # Docstring inherited. 

151 if "r" not in mode or "+" in mode: 

152 raise RuntimeError(f"Package resource URI {self} is read-only.") 

153 ref = self._get_ref() 

154 if ref is None: 

155 raise FileNotFoundError(f"Could not open resource {self}.") 

156 with ref.open(mode, encoding=encoding) as buffer: 

157 yield buffer 

158 

159 def walk( 

160 self, file_filter: str | re.Pattern | None = None 

161 ) -> Iterator[list | tuple[ResourcePath, list[str], list[str]]]: 

162 # Docstring inherited. 

163 if not self.isdir(): 

164 raise ValueError(f"Can not walk a non-directory URI: {self}") 

165 

166 if isinstance(file_filter, str): 

167 file_filter = re.compile(file_filter) 

168 

169 ref = self._get_ref() 

170 if ref is None: 

171 raise ValueError(f"Unable to find resource {self}.") 

172 

173 files: list[str] = [] 

174 dirs: list[str] = [] 

175 for item in ref.iterdir(): 

176 if item.is_file(): 

177 files.append(item.name) 

178 else: 

179 # This is a directory. 

180 dirs.append(item.name) 

181 

182 if file_filter is not None: 

183 files = [f for f in files if file_filter.search(f)] 

184 

185 if not dirs and not files: 

186 return 

187 else: 

188 yield type(self)(self, forceAbsolute=False, forceDirectory=True), dirs, files 

189 

190 for dir in dirs: 

191 new_uri = self.join(dir, forceDirectory=True) 

192 yield from new_uri.walk(file_filter)