Coverage for python/lsst/daf/butler/_location.py: 34%

93 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-18 09:55 +0000

1# This file is part of daf_butler. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

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

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

7# for details of code ownership. 

8# 

9# This software is dual licensed under the GNU General Public License and also 

10# under a 3-clause BSD license. Recipients may choose which of these licenses 

11# to use; please see the files gpl-3.0.txt and/or bsd_license.txt, 

12# respectively. If you choose the GPL option then the following text applies 

13# (but note that there is still no warranty even if you opt for BSD instead): 

14# 

15# This program is free software: you can redistribute it and/or modify 

16# it under the terms of the GNU General Public License as published by 

17# the Free Software Foundation, either version 3 of the License, or 

18# (at your option) any later version. 

19# 

20# This program is distributed in the hope that it will be useful, 

21# but WITHOUT ANY WARRANTY; without even the implied warranty of 

22# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

23# GNU General Public License for more details. 

24# 

25# You should have received a copy of the GNU General Public License 

26# along with this program. If not, see <http://www.gnu.org/licenses/>. 

27 

28from __future__ import annotations 

29 

30__all__ = ("Location", "LocationFactory") 

31 

32from typing import Any, Self 

33 

34from lsst.resources import ResourcePath, ResourcePathExpression 

35 

36 

37class Location: 

38 """Identifies a location within the `Datastore`. 

39 

40 Parameters 

41 ---------- 

42 datastoreRootUri : `lsst.resources.ResourcePathExpression` or `None` 

43 Base URI for this datastore, must include an absolute path. 

44 If `None` the `path` must correspond to an absolute URI. 

45 path : `lsst.resources.ResourcePathExpression` 

46 Relative path within datastore. Assumed to be using the local 

47 path separator if a ``file`` scheme is being used for the URI, 

48 else a POSIX separator. Can be a full URI if the root URI is `None`. 

49 Can also be a schemeless URI if it refers to a relative path. 

50 trusted_path : `bool`, optional 

51 If `True`, the path is not checked to see if it is really inside 

52 the datastore. 

53 """ 

54 

55 __slots__ = ("_datastoreRootUri", "_path", "_uri") 

56 

57 def __init__( 

58 self, 

59 datastoreRootUri: None | ResourcePathExpression, 

60 path: ResourcePathExpression, 

61 *, 

62 trusted_path: bool = False, 

63 ): 

64 # Be careful not to force a relative local path to absolute path 

65 path_uri = ResourcePath(path, forceAbsolute=False, forceDirectory=False) 

66 

67 if isinstance(datastoreRootUri, str): 

68 datastoreRootUri = ResourcePath(datastoreRootUri, forceDirectory=True) 

69 elif datastoreRootUri is None: 

70 if not path_uri.isabs(): 

71 raise ValueError(f"No datastore root URI given but path '{path}' was not absolute URI.") 

72 elif not isinstance(datastoreRootUri, ResourcePath): 

73 raise ValueError("Datastore root must be a ResourcePath instance") 

74 

75 if datastoreRootUri is not None and not datastoreRootUri.isabs(): 

76 raise ValueError(f"Supplied root URI must be an absolute path (given {datastoreRootUri}).") 

77 

78 self._datastoreRootUri = datastoreRootUri 

79 

80 # if the root URI is not None the path must not be absolute since 

81 # it is required to be within the root. 

82 if datastoreRootUri is not None and path_uri.isabs(): 

83 raise ValueError(f"Path within datastore must be relative not absolute, got {path_uri}") 

84 

85 self._path = path_uri 

86 

87 # Internal cache of the full location as a ResourcePath 

88 self._uri: ResourcePath | None = None 

89 

90 # Check that the resulting URI is inside the datastore 

91 # This can go wrong if we were given ../dir as path 

92 if self._datastoreRootUri is not None and not trusted_path: 

93 pathInStore = self.uri.relative_to(self._datastoreRootUri) 

94 if pathInStore is None: 

95 raise ValueError(f"Unexpectedly {path} jumps out of {self._datastoreRootUri}") 

96 

97 def clone(self) -> Self: 

98 """Return a copy of this location as a new instance. 

99 

100 Returns 

101 ------- 

102 location : `Location` 

103 An identical location as a new instance. 

104 """ 

105 # The properties associated with this object are all immutable 

106 # so we can copy them directly to the clone. 

107 clone = object.__new__(type(self)) 

108 clone._path = self._path 

109 clone._datastoreRootUri = self._datastoreRootUri 

110 clone._uri = None 

111 return clone 

112 

113 def __copy__(self) -> Self: 

114 """Copy constructor.""" 

115 # Implement here because the __new__ method confuses things 

116 return self.clone() 

117 

118 def __deepcopy__(self, memo: Any) -> Self: 

119 """Deepcopy the object.""" 

120 return self.clone() 

121 

122 def __str__(self) -> str: 

123 return str(self.uri) 

124 

125 def __repr__(self) -> str: 

126 uri = self._datastoreRootUri 

127 path = self._path 

128 return f"{self.__class__.__name__}({uri!r}, {path.path!r})" 

129 

130 def __eq__(self, other: object) -> bool: 

131 if not isinstance(other, Location): 

132 return NotImplemented 

133 # Compare the combined URI rather than how it is apportioned 

134 return self.uri == other.uri 

135 

136 @property 

137 def uri(self) -> ResourcePath: 

138 """Return URI corresponding to fully-specified datastore location.""" 

139 if self._uri is None: 

140 root = self._datastoreRootUri 

141 if root is None: 

142 uri = self._path 

143 else: 

144 uri = root.join(self._path, forceDirectory=False) 

145 self._uri = uri 

146 return self._uri 

147 

148 @property 

149 def path(self) -> str: 

150 """Return path corresponding to location. 

151 

152 This path includes the root of the `Datastore`, but does not include 

153 non-path components of the root URI. Paths will not include URI 

154 quoting. If a file URI scheme is being used the path will be returned 

155 with the local OS path separator. 

156 """ 

157 full = self.uri 

158 try: 

159 return full.ospath 

160 except AttributeError: 

161 return full.unquoted_path 

162 

163 @property 

164 def pathInStore(self) -> ResourcePath: 

165 """Return path corresponding to location relative to `Datastore` root. 

166 

167 Uses the same path separator as supplied to the object constructor. 

168 Can be an absolute URI if that is how the location was configured. 

169 """ 

170 return self._path 

171 

172 @property 

173 def netloc(self) -> str: 

174 """Return the URI network location.""" 

175 return self.uri.netloc 

176 

177 @property 

178 def relativeToPathRoot(self) -> str: 

179 """Return the path component relative to the network location. 

180 

181 Effectively, this is the path property with POSIX separator stripped 

182 from the left hand side of the path. Will be unquoted. 

183 """ 

184 return self.uri.relativeToPathRoot 

185 

186 def updateExtension(self, ext: str | None) -> None: 

187 """Update the file extension associated with this `Location`. 

188 

189 All file extensions are replaced. 

190 

191 Parameters 

192 ---------- 

193 ext : `str` 

194 New extension. If an empty string is given any extension will 

195 be removed. If `None` is given there will be no change. 

196 """ 

197 if ext is None: 

198 return 

199 

200 self._path = self._path.updatedExtension(ext) 

201 

202 # Clear the URI cache so it can be recreated with the new path 

203 self._uri = None 

204 

205 def getExtension(self) -> str: 

206 """Return the file extension(s) associated with this location. 

207 

208 Returns 

209 ------- 

210 ext : `str` 

211 The file extension (including the ``.``). Can be empty string 

212 if there is no file extension. Will return all file extensions 

213 as a single extension such that ``file.fits.gz`` will return 

214 a value of ``.fits.gz``. 

215 """ 

216 return self.uri.getExtension() 

217 

218 

219class LocationFactory: 

220 """Factory for `Location` instances. 

221 

222 The factory is constructed from the root location of the datastore. 

223 This location can be a path on the file system (absolute or relative) 

224 or as a URI. 

225 

226 Parameters 

227 ---------- 

228 datastoreRoot : `str` 

229 Root location of the `Datastore` either as a path in the local 

230 filesystem or as a URI. File scheme URIs can be used. If a local 

231 filesystem path is used without URI scheme, it will be converted 

232 to an absolute path and any home directory indicators expanded. 

233 If a file scheme is used with a relative path, the path will 

234 be treated as a posixpath but then converted to an absolute path. 

235 """ 

236 

237 def __init__(self, datastoreRoot: ResourcePathExpression): 

238 self._datastoreRootUri = ResourcePath(datastoreRoot, forceAbsolute=True, forceDirectory=True) 

239 

240 def __str__(self) -> str: 

241 return f"{self.__class__.__name__}@{self._datastoreRootUri}" 

242 

243 @property 

244 def netloc(self) -> str: 

245 """Return the network location of root location of the `Datastore`.""" 

246 return self._datastoreRootUri.netloc 

247 

248 def fromPath(self, path: ResourcePathExpression, *, trusted_path: bool = False) -> Location: 

249 """Create a `Location` from a POSIX path. 

250 

251 Parameters 

252 ---------- 

253 path : `str` or `lsst.resources.ResourcePath` 

254 A standard POSIX path, relative to the `Datastore` root. 

255 If it is a `lsst.resources.ResourcePath` it must not be absolute. 

256 Is assumed to refer to a file and not a directory in the datastore. 

257 trusted_path : `bool`, optional 

258 If `True`, the path is not checked to see if it is really inside 

259 the datastore. 

260 

261 Returns 

262 ------- 

263 location : `Location` 

264 The equivalent `Location`. 

265 """ 

266 path = ResourcePath(path, forceAbsolute=False, forceDirectory=False) 

267 return self.from_uri(path, trusted_path=trusted_path) 

268 

269 def from_uri(self, uri: ResourcePath, *, trusted_path: bool = False) -> Location: 

270 if uri.isabs(): 

271 raise ValueError("LocationFactory path must be relative to datastore, not absolute.") 

272 return Location(self._datastoreRootUri, uri, trusted_path=trusted_path)