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 

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 _force_to_file(self) -> ButlerFileURI: 

72 """Force a schemeless URI to a file URI and returns a new URI. 

73 

74 This will include URI quoting of the path. 

75 

76 Returns 

77 ------- 

78 file : `ButlerFileURI` 

79 A copy of the URI using file scheme. If already a file scheme 

80 the copy will be identical. 

81 

82 Raises 

83 ------ 

84 ValueError 

85 Raised if this URI is schemeless and relative path and so can 

86 not be forced to file absolute path without context. 

87 """ 

88 if not self.isabs(): 

89 raise RuntimeError(f"Internal error: Can not force {self} to absolute file URI") 

90 uri = self._uri._replace(scheme="file", path=urllib.parse.quote(os2posix(self.path))) 

91 # mypy really wants a ButlerFileURI to be returned here 

92 return ButlerURI(uri, forceDirectory=self.dirLike) # type: ignore 

93 

94 @staticmethod 

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

96 forceAbsolute: bool = False, 

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

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

99 

100 Parameters 

101 ---------- 

102 parsed : `~urllib.parse.ParseResult` 

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

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

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

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

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

108 forceAbsolute : `bool`, optional 

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

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

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

112 absolute path. 

113 forceDirectory : `bool`, optional 

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

115 URI is interpreted as is. 

116 

117 Returns 

118 ------- 

119 modified : `~urllib.parse.ParseResult` 

120 Update result if a URI is being handled. 

121 dirLike : `bool` 

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

123 forceDirectory is True. Otherwise `False`. 

124 

125 Notes 

126 ----- 

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

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

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

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

131 

132 Scheme-less paths are normalized. 

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 # Ensure that this becomes a file URI if it is already absolute 

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

153 replacements["scheme"] = "file" 

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

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

156 elif forceAbsolute: 

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

158 # scheme. 

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

160 else: 

161 # No change needed for relative local path staying relative 

162 # except normalization 

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

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

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

166 dirLike = True 

167 

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

169 # track of directory vs file when calling replaceFile 

170 

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

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

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

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

175 # we are already being told. 

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

177 forceDirectory = True 

178 

179 # add the trailing separator only if explicitly required or 

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

181 # separator exists. 

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

183 if (forceDirectory or endsOnSep or dirLike): 

184 dirLike = True 

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

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

187 

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

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

190 # and quote 

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

192 

193 # ParseResult is a NamedTuple so _replace is standard API 

194 parsed = parsed._replace(**replacements) 

195 

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

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

198 

199 return parsed, dirLike