Coverage for python/lsst/resources/location.py: 34%

78 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-01 11:14 +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__ = ("Location", "LocationFactory") 

15 

16from ._resourcePath import ResourcePath 

17 

18 

19class Location: 

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

21 

22 Parameters 

23 ---------- 

24 datastoreRootUri : `ResourcePath` or `str` or `None` 

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

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

27 path : `ResourcePath` or `str` 

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

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

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

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

32 This path is required to be a file name and not a directory. 

33 """ 

34 

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

36 

37 def __init__(self, datastoreRootUri: None | ResourcePath | str, path: ResourcePath | str): 

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

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

40 

41 if isinstance(datastoreRootUri, str): 

42 datastoreRootUri = ResourcePath(datastoreRootUri, forceDirectory=True) 

43 elif datastoreRootUri is None: 

44 if not path_uri.isabs(): 

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

46 elif not isinstance(datastoreRootUri, ResourcePath): 

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

48 

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

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

51 

52 self._datastoreRootUri = datastoreRootUri 

53 

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

55 # it is required to be within the root. 

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

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

58 

59 self._path = path_uri 

60 

61 # Internal cache of the full location as a ResourcePath 

62 self._uri: ResourcePath | None = None 

63 

64 # Check that the resulting URI is inside the datastore 

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

66 if self._datastoreRootUri is not None: 

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

68 if pathInStore is None: 

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

70 

71 def __str__(self) -> str: 

72 return str(self.uri) 

73 

74 def __repr__(self) -> str: 

75 uri = self._datastoreRootUri 

76 path = self._path 

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

78 

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

80 if not isinstance(other, Location): 

81 return NotImplemented 

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

83 return self.uri == other.uri 

84 

85 @property 

86 def uri(self) -> ResourcePath: 

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

88 if self._uri is None: 

89 root = self._datastoreRootUri 

90 uri = self._path if root is None else root.join(self._path) 

91 self._uri = uri 

92 return self._uri 

93 

94 @property 

95 def path(self) -> str: 

96 """Return path corresponding to location. 

97 

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

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

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

101 with the local OS path separator. 

102 """ 

103 full = self.uri 

104 try: 

105 return full.ospath 

106 except AttributeError: 

107 return full.unquoted_path 

108 

109 @property 

110 def pathInStore(self) -> ResourcePath: 

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

112 

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

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

115 """ 

116 return self._path 

117 

118 @property 

119 def netloc(self) -> str: 

120 """Return the URI network location.""" 

121 return self.uri.netloc 

122 

123 @property 

124 def relativeToPathRoot(self) -> str: 

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

126 

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

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

129 """ 

130 return self.uri.relativeToPathRoot 

131 

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

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

134 

135 All file extensions are replaced. 

136 

137 Parameters 

138 ---------- 

139 ext : `str` 

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

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

142 """ 

143 if ext is None: 

144 return 

145 

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

147 

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

149 self._uri = None 

150 

151 def getExtension(self) -> str: 

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

153 

154 Returns 

155 ------- 

156 ext : `str` 

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

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

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

160 a value of ``.fits.gz``. 

161 """ 

162 return self.uri.getExtension() 

163 

164 

165class LocationFactory: 

166 """Factory for `Location` instances. 

167 

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

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

170 or as a URI. 

171 

172 Parameters 

173 ---------- 

174 datastoreRoot : `str` 

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

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

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

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

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

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

181 """ 

182 

183 def __init__(self, datastoreRoot: ResourcePath | str): 

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

185 

186 def __str__(self) -> str: 

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

188 

189 @property 

190 def netloc(self) -> str: 

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

192 return self._datastoreRootUri.netloc 

193 

194 def fromPath(self, path: str | ResourcePath) -> Location: 

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

196 

197 Parameters 

198 ---------- 

199 path : `str` or `ResourcePath` 

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

201 If it is a `ResourcePath` it must not be absolute. 

202 

203 Returns 

204 ------- 

205 location : `Location` 

206 The equivalent `Location`. 

207 """ 

208 path = ResourcePath(path, forceAbsolute=False) 

209 if path.isabs(): 

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

211 return Location(self._datastoreRootUri, path)