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

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 

8 

9import attr 

10 

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 

21 

22 

23@final 

24@attr.s(init=False) 

25class TempPathFactory: 

26 """Factory for temporary directories under the common base temp directory. 

27 

28 The base directory can be configured using the ``--basetemp`` option. 

29 """ 

30 

31 _given_basetemp = attr.ib(type=Optional[Path]) 

32 _trace = attr.ib() 

33 _basetemp = attr.ib(type=Optional[Path]) 

34 

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 

53 

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. 

62 

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 ) 

71 

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 

77 

78 def mktemp(self, basename: str, numbered: bool = True) -> Path: 

79 """Create a new temporary directory managed by the factory. 

80 

81 :param basename: 

82 Directory base name, must be a relative path. 

83 

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. 

89 

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 

101 

102 def getbasetemp(self) -> Path: 

103 """Return the base temporary directory, creating it if needed. 

104 

105 :returns: 

106 The base temporary directory. 

107 """ 

108 if self._basetemp is not None: 

109 return self._basetemp 

110 

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 

160 

161 

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 

168 

169 return getpass.getuser() 

170 except (ImportError, KeyError): 

171 return None 

172 

173 

174def pytest_configure(config: Config) -> None: 

175 """Create a TempPathFactory and attach it to the config object. 

176 

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) 

185 

186 

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 

192 

193 

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) 

200 

201 

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. 

207 

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`. 

212 

213 The returned object is a :class:`pathlib.Path` object. 

214 """ 

215 

216 return _mk_tmp(request, tmp_path_factory)