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

80 statements  

« prev     ^ index     » next       coverage.py v7.4.3, created at 2024-03-13 09:59 +0000

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 ↛ 26line 21 didn't jump to line 26, because the condition on line 21 was never true

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 

31 

32from ._resourceHandles._baseResourceHandle import ResourceHandleProtocol 

33from ._resourcePath import ResourcePath 

34 

35log = logging.getLogger(__name__) 

36 

37 

38class PackageResourcePath(ResourcePath): 

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

40 

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

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

43 resource name. 

44 """ 

45 

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

47 """Obtain the object representing the resource. 

48 

49 Returns 

50 ------- 

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

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

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

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

55 """ 

56 # Need the path without the leading /. 

57 path = self.path.lstrip("/") 

58 try: 

59 ref = resources.files(self.netloc).joinpath(path) 

60 except ModuleNotFoundError: 

61 return None 

62 return ref 

63 

64 def isdir(self) -> bool: 

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

66 if self.dirLike is None: 

67 ref = self._get_ref() 

68 if ref is not None: 

69 self.dirLike = ref.is_dir() 

70 else: 

71 return False 

72 return self.dirLike 

73 

74 def exists(self) -> bool: 

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

76 ref = self._get_ref() 

77 if ref is None: 

78 return False 

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

80 

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

82 ref = self._get_ref() 

83 if not ref: 

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

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

86 return fh.read(size) 

87 

88 @contextlib.contextmanager 

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

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

91 

92 Yields 

93 ------ 

94 local : `ResourcePath` 

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

96 system. 

97 

98 Notes 

99 ----- 

100 The context manager will automatically delete any local temporary 

101 file. 

102 

103 Examples 

104 -------- 

105 Should be used as a context manager: 

106 

107 .. code-block:: py 

108 

109 with uri.as_local() as local: 

110 ospath = local.ospath 

111 """ 

112 ref = self._get_ref() 

113 if ref is None: 

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

115 if ref.is_dir(): 

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

117 

118 with resources.as_file(ref) as file: 

119 yield ResourcePath(file) 

120 

121 @contextlib.contextmanager 

122 def open( 

123 self, 

124 mode: str = "r", 

125 *, 

126 encoding: str | None = None, 

127 prefer_file_temporary: bool = False, 

128 ) -> Iterator[ResourceHandleProtocol]: 

129 # Docstring inherited. 

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

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

132 ref = self._get_ref() 

133 if ref is None: 

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

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

136 yield buffer 

137 

138 def walk( 

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

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

141 # Docstring inherited. 

142 if not self.isdir(): 

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

144 

145 if isinstance(file_filter, str): 

146 file_filter = re.compile(file_filter) 

147 

148 ref = self._get_ref() 

149 if ref is None: 

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

151 

152 files: list[str] = [] 

153 dirs: list[str] = [] 

154 for item in ref.iterdir(): 

155 if item.is_dir(): 

156 dirs.append(item.name) 

157 elif item.is_file(): 157 ↛ 154line 157 didn't jump to line 154, because the condition on line 157 was never false

158 files.append(item.name) 

159 # If the item wasn't covered by one of the cases above that 

160 # means it was deleted concurrently with this walk or is 

161 # not a plain file/directory/symlink 

162 

163 if file_filter is not None: 

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

165 

166 if not dirs and not files: 

167 return 

168 else: 

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

170 

171 for dir in dirs: 

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

173 yield from new_uri.walk(file_filter)