Coverage for python/lsst/resources/utils.py: 39%
56 statements
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-13 09:59 +0000
« prev ^ index » next coverage.py v7.4.3, created at 2024-03-13 09:59 +0000
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.
12from __future__ import annotations
14__all__ = ("os2posix", "posix2os", "NoTransaction", "TransactionProtocol")
16import contextlib
17import logging
18import os
19import posixpath
20import shutil
21import stat
22import tempfile
23from collections.abc import Callable, Iterator
24from pathlib import Path, PurePath, PurePosixPath
25from typing import Any, Protocol
27# Determine if the path separator for the OS looks like POSIX
28IS_POSIX = os.sep == posixpath.sep
30# Root path for this operating system. This can use getcwd which
31# can fail in some situations so in the default case assume that
32# posix means posix and only determine explicitly in the non-posix case.
33OS_ROOT_PATH = posixpath.sep if IS_POSIX else Path().resolve().root
35log = logging.getLogger(__name__)
38def os2posix(ospath: str) -> str:
39 """Convert a local path description to a POSIX path description.
41 Parameters
42 ----------
43 ospath : `str`
44 Path using the local path separator.
46 Returns
47 -------
48 posix : `str`
49 Path using POSIX path separator.
50 """
51 if IS_POSIX:
52 return ospath
54 posix = PurePath(ospath).as_posix()
56 # PurePath strips trailing "/" from paths such that you can no
57 # longer tell if a path is meant to be referring to a directory
58 # Try to fix this.
59 if ospath.endswith(os.sep) and not posix.endswith(posixpath.sep):
60 posix += posixpath.sep
62 return posix
65def posix2os(posix: PurePath | str) -> str:
66 """Convert a POSIX path description to a local path description.
68 Parameters
69 ----------
70 posix : `str`, `~pathlib.PurePath`
71 Path using the POSIX path separator.
73 Returns
74 -------
75 ospath : `str`
76 Path using OS path separator.
77 """
78 if IS_POSIX:
79 return str(posix)
81 posixPath = PurePosixPath(posix)
82 paths = list(posixPath.parts)
84 # Have to convert the root directory after splitting
85 if paths[0] == posixPath.root:
86 paths[0] = OS_ROOT_PATH
88 # Trailing "/" is stripped so we need to add back an empty path
89 # for consistency
90 if str(posix).endswith(posixpath.sep):
91 paths.append("")
93 return os.path.join(*paths)
96class NoTransaction:
97 """A simple emulation of the
98 `~lsst.daf.butler.core.datastore.DatastoreTransaction` class.
100 Notes
101 -----
102 Does nothing. Used as a fallback in the absence of an explicit transaction
103 class.
104 """
106 def __init__(self) -> None:
107 return
109 @contextlib.contextmanager
110 def undoWith(self, name: str, undoFunc: Callable, *args: Any, **kwargs: Any) -> Iterator[None]:
111 """No-op context manager to replace
112 `~lsst.daf.butler.core.datastore.DatastoreTransaction`.
114 Parameters
115 ----------
116 name : `str`
117 The name of this undo request.
118 undoFunc : `~collections.abc.Callable`
119 Function to call if there is an exception. Not used.
120 *args : `~typing.Any`
121 Parameters to pass to ``undoFunc``.
122 **kwargs : `~typing.Any`
123 Keyword parameters to pass to ``undoFunc``.
125 Yields
126 ------
127 `None`
128 Context manager returns nothing since transactions are disabled
129 by definition.
130 """
131 yield None
134class TransactionProtocol(Protocol):
135 """Protocol for type checking transaction interface."""
137 @contextlib.contextmanager
138 def undoWith(self, name: str, undoFunc: Callable, *args: Any, **kwargs: Any) -> Iterator[None]: ... 138 ↛ exitline 138 didn't return from function 'undoWith'
141def makeTestTempDir(default_base: str | None = None) -> str:
142 """Create a temporary directory for test usage.
144 The directory will be created within ``LSST_RESOURCES_TEST_TMP`` if that
145 environment variable is set, falling back to ``LSST_RESOURCES_TMPDIR``
146 amd then ``default_base`` if none are set.
148 Parameters
149 ----------
150 default_base : `str`, optional
151 Default parent directory. Will use system default if no environment
152 variables are set and base is set to `None`.
154 Returns
155 -------
156 dir : `str`
157 Name of the new temporary directory.
158 """
159 base = default_base
160 for envvar in ("LSST_RESOURCES_TEST_TMP", "LSST_RESOURCES_TMPDIR"):
161 if envvar in os.environ and os.environ[envvar]:
162 base = os.environ[envvar]
163 break
164 return tempfile.mkdtemp(dir=base)
167def removeTestTempDir(root: str | None) -> None:
168 """Attempt to remove a temporary test directory, but do not raise if
169 unable to.
171 Unlike `tempfile.TemporaryDirectory`, this passes ``ignore_errors=True``
172 to ``shutil.rmtree`` at close, making it safe to use on NFS.
174 Parameters
175 ----------
176 root : `str`, optional
177 Name of the directory to be removed. If `None`, nothing will be done.
178 """
179 if root is not None and os.path.exists(root):
180 shutil.rmtree(root, ignore_errors=True)
183def ensure_directory_is_writeable(directory_path: str | bytes) -> None:
184 """Given the path to a directory, ensures that we are able to write it and
185 access files in it.
187 Alters the directory permissions by adding the owner-write and
188 owner-traverse permission bits if they aren't already set
190 Parameters
191 ----------
192 directory_path : `str` or `bytes`
193 Path to the directory that will be made writeable.
194 """
195 current_mode = os.stat(directory_path).st_mode
196 desired_mode = current_mode | stat.S_IWUSR | stat.S_IXUSR
197 if current_mode != desired_mode:
198 os.chmod(directory_path, desired_mode)