Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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 program is free software: you can redistribute it and/or modify 

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

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

12# (at your option) any later version. 

13# 

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

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

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

17# GNU General Public License for more details. 

18# 

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

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

21 

22 

23from __future__ import annotations 

24 

25import os 

26import urllib.parse 

27import os.path 

28import logging 

29 

30__all__ = ('ButlerSchemelessURI',) 

31 

32from pathlib import PurePath 

33 

34from typing import ( 

35 Optional, 

36 Tuple, 

37 Union, 

38) 

39 

40from .file import ButlerFileURI 

41from .utils import os2posix 

42from ._butlerUri import ButlerURI 

43 

44log = logging.getLogger(__name__) 

45 

46 

47class ButlerSchemelessURI(ButlerFileURI): 

48 """Scheme-less URI referring to the local file system.""" 

49 

50 _pathLib = PurePath 

51 _pathModule = os.path 

52 quotePaths = False 

53 

54 @property 

55 def ospath(self) -> str: 

56 """Path component of the URI localized to current OS.""" 

57 return self.path 

58 

59 def isabs(self) -> bool: 

60 """Indicate that the resource is fully specified. 

61 

62 For non-schemeless URIs this is always true. 

63 

64 Returns 

65 ------- 

66 isabs : `bool` 

67 `True` if the file is absolute, `False` otherwise. 

68 """ 

69 return os.path.isabs(self.ospath) 

70 

71 def abspath(self) -> ButlerURI: 

72 """Force a schemeless URI to a file URI. 

73 

74 This will include URI quoting of the path. 

75 

76 Returns 

77 ------- 

78 file : `ButlerFileURI` 

79 A new URI using file scheme. 

80 

81 Notes 

82 ----- 

83 The current working directory will be used to convert this scheme-less 

84 URI to an absolute path. 

85 """ 

86 # Convert this URI to a string so that any fragments will be 

87 # processed correctly by the ButlerURI constructor. We provide 

88 # the options that will force the code below in _fixupPathUri to 

89 # return a file URI from a scheme-less one. 

90 return ButlerURI(str(self), forceAbsolute=True, forceDirectory=self.isdir(), 

91 isTemporary=self.isTemporary) 

92 

93 @classmethod 

94 def _fixupPathUri(cls, parsed: urllib.parse.ParseResult, root: Optional[Union[str, ButlerURI]] = None, 

95 forceAbsolute: bool = False, 

96 forceDirectory: bool = False) -> Tuple[urllib.parse.ParseResult, bool]: 

97 """Fix up relative paths for local file system. 

98 

99 Parameters 

100 ---------- 

101 parsed : `~urllib.parse.ParseResult` 

102 The result from parsing a URI using `urllib.parse`. 

103 root : `str` or `ButlerURI`, optional 

104 Path to use as root when converting relative to absolute. 

105 If `None`, it will be the current working directory. This 

106 is a local file system path, or a file URI. 

107 forceAbsolute : `bool`, optional 

108 If `True`, scheme-less relative URI will be converted to an 

109 absolute path using a ``file`` scheme. If `False` scheme-less URI 

110 will remain scheme-less and will not be updated to ``file`` or 

111 absolute path. 

112 forceDirectory : `bool`, optional 

113 If `True` forces the URI to end with a separator, otherwise given 

114 URI is interpreted as is. 

115 

116 Returns 

117 ------- 

118 modified : `~urllib.parse.ParseResult` 

119 Update result if a URI is being handled. 

120 dirLike : `bool` 

121 `True` if given parsed URI has a trailing separator or 

122 forceDirectory is True. Otherwise `False`. 

123 

124 Notes 

125 ----- 

126 Relative paths are explicitly not supported by RFC8089 but `urllib` 

127 does accept URIs of the form ``file:relative/path.ext``. They need 

128 to be turned into absolute paths before they can be used. This is 

129 always done regardless of the ``forceAbsolute`` parameter. 

130 

131 Scheme-less paths are normalized and environment variables are 

132 expanded. 

133 """ 

134 # assume we are not dealing with a directory URI 

135 dirLike = False 

136 

137 # Replacement values for the URI 

138 replacements = {} 

139 

140 if root is None: 140 ↛ 142line 140 didn't jump to line 142, because the condition on line 140 was never false

141 root = os.path.abspath(os.path.curdir) 

142 elif isinstance(root, ButlerURI): 

143 if root.scheme and root.scheme != "file": 

144 raise RuntimeError(f"The override root must be a file URI not {root.scheme}") 

145 root = os.path.abspath(root.ospath) 

146 

147 # this is a local OS file path which can support tilde expansion. 

148 # we quoted it in the constructor so unquote here 

149 expandedPath = os.path.expanduser(urllib.parse.unquote(parsed.path)) 

150 

151 # We might also be receiving a path containing environment variables 

152 # so expand those here 

153 expandedPath = os.path.expandvars(expandedPath) 

154 

155 # Ensure that this becomes a file URI if it is already absolute 

156 if os.path.isabs(expandedPath): 156 ↛ 157line 156 didn't jump to line 157, because the condition on line 156 was never true

157 replacements["scheme"] = "file" 

158 # Keep in OS form for now to simplify later logic 

159 replacements["path"] = os.path.normpath(expandedPath) 

160 elif forceAbsolute: 

161 # This can stay in OS path form, do not change to file 

162 # scheme. 

163 replacements["path"] = os.path.normpath(os.path.join(root, expandedPath)) 

164 else: 

165 # No change needed for relative local path staying relative 

166 # except normalization 

167 replacements["path"] = os.path.normpath(expandedPath) 

168 # normalization of empty path returns "." so we are dirLike 

169 if expandedPath == "": 169 ↛ 170line 169 didn't jump to line 170, because the condition on line 169 was never true

170 dirLike = True 

171 

172 # normpath strips trailing "/" which makes it hard to keep 

173 # track of directory vs file when calling replaceFile 

174 

175 # For local file system we can explicitly check to see if this 

176 # really is a directory. The URI might point to a location that 

177 # does not exists yet but all that matters is if it is a directory 

178 # then we make sure use that fact. No need to do the check if 

179 # we are already being told. 

180 if not forceDirectory and os.path.isdir(replacements["path"]): 180 ↛ 181line 180 didn't jump to line 181, because the condition on line 180 was never true

181 forceDirectory = True 

182 

183 # add the trailing separator only if explicitly required or 

184 # if it was stripped by normpath. Acknowledge that trailing 

185 # separator exists. 

186 endsOnSep = expandedPath.endswith(os.sep) and not replacements["path"].endswith(os.sep) 

187 if (forceDirectory or endsOnSep or dirLike): 

188 dirLike = True 

189 if not replacements["path"].endswith(os.sep): 189 ↛ 192line 189 didn't jump to line 192, because the condition on line 189 was never false

190 replacements["path"] += os.sep 

191 

192 if "scheme" in replacements: 192 ↛ 195line 192 didn't jump to line 195, because the condition on line 192 was never true

193 # This is now meant to be a URI path so force to posix 

194 # and quote 

195 replacements["path"] = urllib.parse.quote(os2posix(replacements["path"])) 

196 

197 # ParseResult is a NamedTuple so _replace is standard API 

198 parsed = parsed._replace(**replacements) 

199 

200 # We do allow fragment but do not expect params or query to be 

201 # specified for schemeless 

202 if parsed.params or parsed.query: 202 ↛ 203line 202 didn't jump to line 203, because the condition on line 202 was never true

203 log.warning("Additional items unexpectedly encountered in schemeless URI: %s", parsed.geturl()) 

204 

205 return parsed, dirLike