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

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.parse
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 abspath(self) -> ButlerURI:
72 """Force a schemeless URI to a file URI.
74 This will include URI quoting of the path.
76 Returns
77 -------
78 file : `ButlerFileURI`
79 A new URI using file scheme.
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)
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.
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.
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`.
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.
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
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 # We might also be receiving a path containing environment variables
152 # so expand those here
153 expandedPath = os.path.expandvars(expandedPath)
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
172 # normpath strips trailing "/" which makes it hard to keep
173 # track of directory vs file when calling replaceFile
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
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
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"]))
197 # ParseResult is a NamedTuple so _replace is standard API
198 parsed = parsed._replace(**replacements)
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())
205 return parsed, dirLike