Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/tmpdir.py: 47%
101 statements
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
« prev ^ index » next coverage.py v7.2.3, created at 2023-05-04 13:14 +0700
1"""Support for providing temporary directories to test functions."""
2import os
3import re
4import sys
5import tempfile
6from pathlib import Path
7from typing import Optional
9import attr
11from .pathlib import LOCK_TIMEOUT
12from .pathlib import make_numbered_dir
13from .pathlib import make_numbered_dir_with_cleanup
14from .pathlib import rm_rf
15from _pytest.compat import final
16from _pytest.config import Config
17from _pytest.deprecated import check_ispytest
18from _pytest.fixtures import fixture
19from _pytest.fixtures import FixtureRequest
20from _pytest.monkeypatch import MonkeyPatch
23@final
24@attr.s(init=False)
25class TempPathFactory:
26 """Factory for temporary directories under the common base temp directory.
28 The base directory can be configured using the ``--basetemp`` option.
29 """
31 _given_basetemp = attr.ib(type=Optional[Path])
32 _trace = attr.ib()
33 _basetemp = attr.ib(type=Optional[Path])
35 def __init__(
36 self,
37 given_basetemp: Optional[Path],
38 trace,
39 basetemp: Optional[Path] = None,
40 *,
41 _ispytest: bool = False,
42 ) -> None:
43 check_ispytest(_ispytest)
44 if given_basetemp is None:
45 self._given_basetemp = None
46 else:
47 # Use os.path.abspath() to get absolute path instead of resolve() as it
48 # does not work the same in all platforms (see #4427).
49 # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012).
50 self._given_basetemp = Path(os.path.abspath(str(given_basetemp)))
51 self._trace = trace
52 self._basetemp = basetemp
54 @classmethod
55 def from_config(
56 cls,
57 config: Config,
58 *,
59 _ispytest: bool = False,
60 ) -> "TempPathFactory":
61 """Create a factory according to pytest configuration.
63 :meta private:
64 """
65 check_ispytest(_ispytest)
66 return cls(
67 given_basetemp=config.option.basetemp,
68 trace=config.trace.get("tmpdir"),
69 _ispytest=True,
70 )
72 def _ensure_relative_to_basetemp(self, basename: str) -> str:
73 basename = os.path.normpath(basename)
74 if (self.getbasetemp() / basename).resolve().parent != self.getbasetemp():
75 raise ValueError(f"{basename} is not a normalized and relative path")
76 return basename
78 def mktemp(self, basename: str, numbered: bool = True) -> Path:
79 """Create a new temporary directory managed by the factory.
81 :param basename:
82 Directory base name, must be a relative path.
84 :param numbered:
85 If ``True``, ensure the directory is unique by adding a numbered
86 suffix greater than any existing one: ``basename="foo-"`` and ``numbered=True``
87 means that this function will create directories named ``"foo-0"``,
88 ``"foo-1"``, ``"foo-2"`` and so on.
90 :returns:
91 The path to the new directory.
92 """
93 basename = self._ensure_relative_to_basetemp(basename)
94 if not numbered:
95 p = self.getbasetemp().joinpath(basename)
96 p.mkdir(mode=0o700)
97 else:
98 p = make_numbered_dir(root=self.getbasetemp(), prefix=basename, mode=0o700)
99 self._trace("mktemp", p)
100 return p
102 def getbasetemp(self) -> Path:
103 """Return the base temporary directory, creating it if needed.
105 :returns:
106 The base temporary directory.
107 """
108 if self._basetemp is not None:
109 return self._basetemp
111 if self._given_basetemp is not None:
112 basetemp = self._given_basetemp
113 if basetemp.exists():
114 rm_rf(basetemp)
115 basetemp.mkdir(mode=0o700)
116 basetemp = basetemp.resolve()
117 else:
118 from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT")
119 temproot = Path(from_env or tempfile.gettempdir()).resolve()
120 user = get_user() or "unknown"
121 # use a sub-directory in the temproot to speed-up
122 # make_numbered_dir() call
123 rootdir = temproot.joinpath(f"pytest-of-{user}")
124 try:
125 rootdir.mkdir(mode=0o700, exist_ok=True)
126 except OSError:
127 # getuser() likely returned illegal characters for the platform, use unknown back off mechanism
128 rootdir = temproot.joinpath("pytest-of-unknown")
129 rootdir.mkdir(mode=0o700, exist_ok=True)
130 # Because we use exist_ok=True with a predictable name, make sure
131 # we are the owners, to prevent any funny business (on unix, where
132 # temproot is usually shared).
133 # Also, to keep things private, fixup any world-readable temp
134 # rootdir's permissions. Historically 0o755 was used, so we can't
135 # just error out on this, at least for a while.
136 if sys.platform != "win32":
137 uid = os.getuid()
138 rootdir_stat = rootdir.stat()
139 # getuid shouldn't fail, but cpython defines such a case.
140 # Let's hope for the best.
141 if uid != -1:
142 if rootdir_stat.st_uid != uid:
143 raise OSError(
144 f"The temporary directory {rootdir} is not owned by the current user. "
145 "Fix this and try again."
146 )
147 if (rootdir_stat.st_mode & 0o077) != 0:
148 os.chmod(rootdir, rootdir_stat.st_mode & ~0o077)
149 basetemp = make_numbered_dir_with_cleanup(
150 prefix="pytest-",
151 root=rootdir,
152 keep=3,
153 lock_timeout=LOCK_TIMEOUT,
154 mode=0o700,
155 )
156 assert basetemp is not None, basetemp
157 self._basetemp = basetemp
158 self._trace("new basetemp", basetemp)
159 return basetemp
162def get_user() -> Optional[str]:
163 """Return the current user name, or None if getuser() does not work
164 in the current environment (see #1010)."""
165 try:
166 # In some exotic environments, getpass may not be importable.
167 import getpass
169 return getpass.getuser()
170 except (ImportError, KeyError):
171 return None
174def pytest_configure(config: Config) -> None:
175 """Create a TempPathFactory and attach it to the config object.
177 This is to comply with existing plugins which expect the handler to be
178 available at pytest_configure time, but ideally should be moved entirely
179 to the tmp_path_factory session fixture.
180 """
181 mp = MonkeyPatch()
182 config.add_cleanup(mp.undo)
183 _tmp_path_factory = TempPathFactory.from_config(config, _ispytest=True)
184 mp.setattr(config, "_tmp_path_factory", _tmp_path_factory, raising=False)
187@fixture(scope="session")
188def tmp_path_factory(request: FixtureRequest) -> TempPathFactory:
189 """Return a :class:`pytest.TempPathFactory` instance for the test session."""
190 # Set dynamically by pytest_configure() above.
191 return request.config._tmp_path_factory # type: ignore
194def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path:
195 name = request.node.name
196 name = re.sub(r"[\W]", "_", name)
197 MAXVAL = 30
198 name = name[:MAXVAL]
199 return factory.mktemp(name, numbered=True)
202@fixture
203def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path:
204 """Return a temporary directory path object which is unique to each test
205 function invocation, created as a sub directory of the base temporary
206 directory.
208 By default, a new base temporary directory is created each test session,
209 and old bases are removed after 3 sessions, to aid in debugging. If
210 ``--basetemp`` is used then it is cleared each session. See :ref:`base
211 temporary directory`.
213 The returned object is a :class:`pathlib.Path` object.
214 """
216 return _mk_tmp(request, tmp_path_factory)