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

82 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2022-09-11 01:04 -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__ = ("Location", "LocationFactory") 

15 

16from typing import Optional, Union 

17 

18from ._resourcePath import ResourcePath 

19 

20 

21class Location: 

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

23 

24 Parameters 

25 ---------- 

26 datastoreRootUri : `ResourcePath` or `str` or `None` 

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

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

29 path : `ResourcePath` or `str` 

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

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

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

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

34 """ 

35 

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

37 

38 def __init__(self, datastoreRootUri: Union[None, ResourcePath, str], path: Union[ResourcePath, str]): 

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

40 path_uri = ResourcePath(path, forceAbsolute=False) 

41 

42 if isinstance(datastoreRootUri, str): 

43 datastoreRootUri = ResourcePath(datastoreRootUri, forceDirectory=True) 

44 elif datastoreRootUri is None: 

45 if not path_uri.isabs(): 

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

47 elif not isinstance(datastoreRootUri, ResourcePath): 

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

49 

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

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

52 

53 self._datastoreRootUri = datastoreRootUri 

54 

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

56 # it is required to be within the root. 

57 if datastoreRootUri is not None: 

58 if path_uri.isabs(): 

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

60 

61 self._path = path_uri 

62 

63 # Internal cache of the full location as a ResourcePath 

64 self._uri: Optional[ResourcePath] = None 

65 

66 # Check that the resulting URI is inside the datastore 

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

68 if self._datastoreRootUri is not None: 

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

70 if pathInStore is None: 

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

72 

73 def __str__(self) -> str: 

74 return str(self.uri) 

75 

76 def __repr__(self) -> str: 

77 uri = self._datastoreRootUri 

78 path = self._path 

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

80 

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

82 if not isinstance(other, Location): 

83 return NotImplemented 

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

85 return self.uri == other.uri 

86 

87 @property 

88 def uri(self) -> ResourcePath: 

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

90 if self._uri is None: 

91 root = self._datastoreRootUri 

92 if root is None: 

93 uri = self._path 

94 else: 

95 uri = root.join(self._path) 

96 self._uri = uri 

97 return self._uri 

98 

99 @property 

100 def path(self) -> str: 

101 """Return path corresponding to location. 

102 

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

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

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

106 with the local OS path separator. 

107 """ 

108 full = self.uri 

109 try: 

110 return full.ospath 

111 except AttributeError: 

112 return full.unquoted_path 

113 

114 @property 

115 def pathInStore(self) -> ResourcePath: 

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

117 

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

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

120 """ 

121 return self._path 

122 

123 @property 

124 def netloc(self) -> str: 

125 """Return the URI network location.""" 

126 return self.uri.netloc 

127 

128 @property 

129 def relativeToPathRoot(self) -> str: 

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

131 

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

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

134 """ 

135 return self.uri.relativeToPathRoot 

136 

137 def updateExtension(self, ext: Optional[str]) -> None: 

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

139 

140 All file extensions are replaced. 

141 

142 Parameters 

143 ---------- 

144 ext : `str` 

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

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

147 """ 

148 if ext is None: 

149 return 

150 

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

152 

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

154 self._uri = None 

155 

156 def getExtension(self) -> str: 

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

158 

159 Returns 

160 ------- 

161 ext : `str` 

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

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

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

165 a value of ``.fits.gz``. 

166 """ 

167 return self.uri.getExtension() 

168 

169 

170class LocationFactory: 

171 """Factory for `Location` instances. 

172 

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

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

175 or as a URI. 

176 

177 Parameters 

178 ---------- 

179 datastoreRoot : `str` 

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

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

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

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

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

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

186 """ 

187 

188 def __init__(self, datastoreRoot: Union[ResourcePath, str]): 

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

190 

191 def __str__(self) -> str: 

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

193 

194 @property 

195 def netloc(self) -> str: 

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

197 return self._datastoreRootUri.netloc 

198 

199 def fromPath(self, path: Union[str, ResourcePath]) -> Location: 

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

201 

202 Parameters 

203 ---------- 

204 path : `str` or `ResourcePath` 

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

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

207 

208 Returns 

209 ------- 

210 location : `Location` 

211 The equivalent `Location`. 

212 """ 

213 path = ResourcePath(path, forceAbsolute=False) 

214 if path.isabs(): 

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

216 return Location(self._datastoreRootUri, path)