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

78 statements  

« prev     ^ index     » next       coverage.py v7.3.1, created at 2023-09-13 09:44 +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 """ 

33 

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

35 

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

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

38 path_uri = ResourcePath(path, forceAbsolute=False) 

39 

40 if isinstance(datastoreRootUri, str): 

41 datastoreRootUri = ResourcePath(datastoreRootUri, forceDirectory=True) 

42 elif datastoreRootUri is None: 

43 if not path_uri.isabs(): 

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

45 elif not isinstance(datastoreRootUri, ResourcePath): 

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

47 

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

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

50 

51 self._datastoreRootUri = datastoreRootUri 

52 

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

54 # it is required to be within the root. 

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

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

57 

58 self._path = path_uri 

59 

60 # Internal cache of the full location as a ResourcePath 

61 self._uri: ResourcePath | None = None 

62 

63 # Check that the resulting URI is inside the datastore 

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

65 if self._datastoreRootUri is not None: 

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

67 if pathInStore is None: 

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

69 

70 def __str__(self) -> str: 

71 return str(self.uri) 

72 

73 def __repr__(self) -> str: 

74 uri = self._datastoreRootUri 

75 path = self._path 

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

77 

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

79 if not isinstance(other, Location): 

80 return NotImplemented 

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

82 return self.uri == other.uri 

83 

84 @property 

85 def uri(self) -> ResourcePath: 

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

87 if self._uri is None: 

88 root = self._datastoreRootUri 

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

90 self._uri = uri 

91 return self._uri 

92 

93 @property 

94 def path(self) -> str: 

95 """Return path corresponding to location. 

96 

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

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

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

100 with the local OS path separator. 

101 """ 

102 full = self.uri 

103 try: 

104 return full.ospath 

105 except AttributeError: 

106 return full.unquoted_path 

107 

108 @property 

109 def pathInStore(self) -> ResourcePath: 

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

111 

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

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

114 """ 

115 return self._path 

116 

117 @property 

118 def netloc(self) -> str: 

119 """Return the URI network location.""" 

120 return self.uri.netloc 

121 

122 @property 

123 def relativeToPathRoot(self) -> str: 

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

125 

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

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

128 """ 

129 return self.uri.relativeToPathRoot 

130 

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

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

133 

134 All file extensions are replaced. 

135 

136 Parameters 

137 ---------- 

138 ext : `str` 

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

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

141 """ 

142 if ext is None: 

143 return 

144 

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

146 

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

148 self._uri = None 

149 

150 def getExtension(self) -> str: 

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

152 

153 Returns 

154 ------- 

155 ext : `str` 

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

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

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

159 a value of ``.fits.gz``. 

160 """ 

161 return self.uri.getExtension() 

162 

163 

164class LocationFactory: 

165 """Factory for `Location` instances. 

166 

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

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

169 or as a URI. 

170 

171 Parameters 

172 ---------- 

173 datastoreRoot : `str` 

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

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

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

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

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

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

180 """ 

181 

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

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

184 

185 def __str__(self) -> str: 

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

187 

188 @property 

189 def netloc(self) -> str: 

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

191 return self._datastoreRootUri.netloc 

192 

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

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

195 

196 Parameters 

197 ---------- 

198 path : `str` or `ResourcePath` 

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

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

201 

202 Returns 

203 ------- 

204 location : `Location` 

205 The equivalent `Location`. 

206 """ 

207 path = ResourcePath(path, forceAbsolute=False) 

208 if path.isabs(): 

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

210 return Location(self._datastoreRootUri, path)