Coverage for python/lsst/daf/butler/core/_butlerUri/schemeless.py : 67%

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/>.
23from __future__ import annotations
25import os
26import urllib
27import os.path
28import logging
30__all__ = ('ButlerSchemelessURI',)
32from pathlib import PurePath
34from typing import (
35 Optional,
36 Tuple,
37 Union,
38)
40from .file import ButlerFileURI
41from .utils import os2posix
42from ._butlerUri import ButlerURI
44log = logging.getLogger(__name__)
47class ButlerSchemelessURI(ButlerFileURI):
48 """Scheme-less URI referring to the local file system"""
50 _pathLib = PurePath
51 _pathModule = os.path
52 quotePaths = False
54 @property
55 def ospath(self) -> str:
56 """Path component of the URI localized to current OS."""
57 return self.path
59 def isabs(self) -> bool:
60 """Indicate that the resource is fully specified.
62 For non-schemeless URIs this is always true.
64 Returns
65 -------
66 isabs : `bool`
67 `True` if the file is absolute, `False` otherwise.
68 """
69 return os.path.isabs(self.ospath)
71 def _force_to_file(self) -> ButlerFileURI:
72 """Force a schemeless URI to a file URI and returns a new URI.
74 This will include URI quoting of the path.
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.
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
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.
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.
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`.
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.
132 Scheme-less paths are normalized.
133 """
134 # assume we are not dealing with a directory URI
135 dirLike = False
137 # Replacement values for the URI
138 replacements = {}
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)
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))
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
168 # normpath strips trailing "/" which makes it hard to keep
169 # track of directory vs file when calling replaceFile
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
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
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"]))
193 # ParseResult is a NamedTuple so _replace is standard API
194 parsed = parsed._replace(**replacements)
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())
199 return parsed, dirLike