Coverage for /opt/homebrew/lib/python3.11/site-packages/_pytest/legacypath.py: 69%

216 statements  

« prev     ^ index     » next       coverage.py v7.2.3, created at 2023-05-04 13:14 +0700

1"""Add backward compatibility support for the legacy py path type.""" 

2import shlex 

3import subprocess 

4from pathlib import Path 

5from typing import List 

6from typing import Optional 

7from typing import TYPE_CHECKING 

8from typing import Union 

9 

10import attr 

11from iniconfig import SectionWrapper 

12 

13from _pytest.cacheprovider import Cache 

14from _pytest.compat import final 

15from _pytest.compat import LEGACY_PATH 

16from _pytest.compat import legacy_path 

17from _pytest.config import Config 

18from _pytest.config import hookimpl 

19from _pytest.config import PytestPluginManager 

20from _pytest.deprecated import check_ispytest 

21from _pytest.fixtures import fixture 

22from _pytest.fixtures import FixtureRequest 

23from _pytest.main import Session 

24from _pytest.monkeypatch import MonkeyPatch 

25from _pytest.nodes import Collector 

26from _pytest.nodes import Item 

27from _pytest.nodes import Node 

28from _pytest.pytester import HookRecorder 

29from _pytest.pytester import Pytester 

30from _pytest.pytester import RunResult 

31from _pytest.terminal import TerminalReporter 

32from _pytest.tmpdir import TempPathFactory 

33 

34if TYPE_CHECKING: 

35 from typing_extensions import Final 

36 

37 import pexpect 

38 

39 

40@final 

41class Testdir: 

42 """ 

43 Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead. 

44 

45 All methods just forward to an internal :class:`Pytester` instance, converting results 

46 to `legacy_path` objects as necessary. 

47 """ 

48 

49 __test__ = False 

50 

51 CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN 

52 TimeoutExpired: "Final" = Pytester.TimeoutExpired 

53 

54 def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: 

55 check_ispytest(_ispytest) 

56 self._pytester = pytester 

57 

58 @property 

59 def tmpdir(self) -> LEGACY_PATH: 

60 """Temporary directory where tests are executed.""" 

61 return legacy_path(self._pytester.path) 

62 

63 @property 

64 def test_tmproot(self) -> LEGACY_PATH: 

65 return legacy_path(self._pytester._test_tmproot) 

66 

67 @property 

68 def request(self): 

69 return self._pytester._request 

70 

71 @property 

72 def plugins(self): 

73 return self._pytester.plugins 

74 

75 @plugins.setter 

76 def plugins(self, plugins): 

77 self._pytester.plugins = plugins 

78 

79 @property 

80 def monkeypatch(self) -> MonkeyPatch: 

81 return self._pytester._monkeypatch 

82 

83 def make_hook_recorder(self, pluginmanager) -> HookRecorder: 

84 """See :meth:`Pytester.make_hook_recorder`.""" 

85 return self._pytester.make_hook_recorder(pluginmanager) 

86 

87 def chdir(self) -> None: 

88 """See :meth:`Pytester.chdir`.""" 

89 return self._pytester.chdir() 

90 

91 def finalize(self) -> None: 

92 """See :meth:`Pytester._finalize`.""" 

93 return self._pytester._finalize() 

94 

95 def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH: 

96 """See :meth:`Pytester.makefile`.""" 

97 if ext and not ext.startswith("."): 

98 # pytester.makefile is going to throw a ValueError in a way that 

99 # testdir.makefile did not, because 

100 # pathlib.Path is stricter suffixes than py.path 

101 # This ext arguments is likely user error, but since testdir has 

102 # allowed this, we will prepend "." as a workaround to avoid breaking 

103 # testdir usage that worked before 

104 ext = "." + ext 

105 return legacy_path(self._pytester.makefile(ext, *args, **kwargs)) 

106 

107 def makeconftest(self, source) -> LEGACY_PATH: 

108 """See :meth:`Pytester.makeconftest`.""" 

109 return legacy_path(self._pytester.makeconftest(source)) 

110 

111 def makeini(self, source) -> LEGACY_PATH: 

112 """See :meth:`Pytester.makeini`.""" 

113 return legacy_path(self._pytester.makeini(source)) 

114 

115 def getinicfg(self, source: str) -> SectionWrapper: 

116 """See :meth:`Pytester.getinicfg`.""" 

117 return self._pytester.getinicfg(source) 

118 

119 def makepyprojecttoml(self, source) -> LEGACY_PATH: 

120 """See :meth:`Pytester.makepyprojecttoml`.""" 

121 return legacy_path(self._pytester.makepyprojecttoml(source)) 

122 

123 def makepyfile(self, *args, **kwargs) -> LEGACY_PATH: 

124 """See :meth:`Pytester.makepyfile`.""" 

125 return legacy_path(self._pytester.makepyfile(*args, **kwargs)) 

126 

127 def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH: 

128 """See :meth:`Pytester.maketxtfile`.""" 

129 return legacy_path(self._pytester.maketxtfile(*args, **kwargs)) 

130 

131 def syspathinsert(self, path=None) -> None: 

132 """See :meth:`Pytester.syspathinsert`.""" 

133 return self._pytester.syspathinsert(path) 

134 

135 def mkdir(self, name) -> LEGACY_PATH: 

136 """See :meth:`Pytester.mkdir`.""" 

137 return legacy_path(self._pytester.mkdir(name)) 

138 

139 def mkpydir(self, name) -> LEGACY_PATH: 

140 """See :meth:`Pytester.mkpydir`.""" 

141 return legacy_path(self._pytester.mkpydir(name)) 

142 

143 def copy_example(self, name=None) -> LEGACY_PATH: 

144 """See :meth:`Pytester.copy_example`.""" 

145 return legacy_path(self._pytester.copy_example(name)) 

146 

147 def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]: 

148 """See :meth:`Pytester.getnode`.""" 

149 return self._pytester.getnode(config, arg) 

150 

151 def getpathnode(self, path): 

152 """See :meth:`Pytester.getpathnode`.""" 

153 return self._pytester.getpathnode(path) 

154 

155 def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]: 

156 """See :meth:`Pytester.genitems`.""" 

157 return self._pytester.genitems(colitems) 

158 

159 def runitem(self, source): 

160 """See :meth:`Pytester.runitem`.""" 

161 return self._pytester.runitem(source) 

162 

163 def inline_runsource(self, source, *cmdlineargs): 

164 """See :meth:`Pytester.inline_runsource`.""" 

165 return self._pytester.inline_runsource(source, *cmdlineargs) 

166 

167 def inline_genitems(self, *args): 

168 """See :meth:`Pytester.inline_genitems`.""" 

169 return self._pytester.inline_genitems(*args) 

170 

171 def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): 

172 """See :meth:`Pytester.inline_run`.""" 

173 return self._pytester.inline_run( 

174 *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc 

175 ) 

176 

177 def runpytest_inprocess(self, *args, **kwargs) -> RunResult: 

178 """See :meth:`Pytester.runpytest_inprocess`.""" 

179 return self._pytester.runpytest_inprocess(*args, **kwargs) 

180 

181 def runpytest(self, *args, **kwargs) -> RunResult: 

182 """See :meth:`Pytester.runpytest`.""" 

183 return self._pytester.runpytest(*args, **kwargs) 

184 

185 def parseconfig(self, *args) -> Config: 

186 """See :meth:`Pytester.parseconfig`.""" 

187 return self._pytester.parseconfig(*args) 

188 

189 def parseconfigure(self, *args) -> Config: 

190 """See :meth:`Pytester.parseconfigure`.""" 

191 return self._pytester.parseconfigure(*args) 

192 

193 def getitem(self, source, funcname="test_func"): 

194 """See :meth:`Pytester.getitem`.""" 

195 return self._pytester.getitem(source, funcname) 

196 

197 def getitems(self, source): 

198 """See :meth:`Pytester.getitems`.""" 

199 return self._pytester.getitems(source) 

200 

201 def getmodulecol(self, source, configargs=(), withinit=False): 

202 """See :meth:`Pytester.getmodulecol`.""" 

203 return self._pytester.getmodulecol( 

204 source, configargs=configargs, withinit=withinit 

205 ) 

206 

207 def collect_by_name( 

208 self, modcol: Collector, name: str 

209 ) -> Optional[Union[Item, Collector]]: 

210 """See :meth:`Pytester.collect_by_name`.""" 

211 return self._pytester.collect_by_name(modcol, name) 

212 

213 def popen( 

214 self, 

215 cmdargs, 

216 stdout=subprocess.PIPE, 

217 stderr=subprocess.PIPE, 

218 stdin=CLOSE_STDIN, 

219 **kw, 

220 ): 

221 """See :meth:`Pytester.popen`.""" 

222 return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw) 

223 

224 def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: 

225 """See :meth:`Pytester.run`.""" 

226 return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin) 

227 

228 def runpython(self, script) -> RunResult: 

229 """See :meth:`Pytester.runpython`.""" 

230 return self._pytester.runpython(script) 

231 

232 def runpython_c(self, command): 

233 """See :meth:`Pytester.runpython_c`.""" 

234 return self._pytester.runpython_c(command) 

235 

236 def runpytest_subprocess(self, *args, timeout=None) -> RunResult: 

237 """See :meth:`Pytester.runpytest_subprocess`.""" 

238 return self._pytester.runpytest_subprocess(*args, timeout=timeout) 

239 

240 def spawn_pytest( 

241 self, string: str, expect_timeout: float = 10.0 

242 ) -> "pexpect.spawn": 

243 """See :meth:`Pytester.spawn_pytest`.""" 

244 return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) 

245 

246 def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": 

247 """See :meth:`Pytester.spawn`.""" 

248 return self._pytester.spawn(cmd, expect_timeout=expect_timeout) 

249 

250 def __repr__(self) -> str: 

251 return f"<Testdir {self.tmpdir!r}>" 

252 

253 def __str__(self) -> str: 

254 return str(self.tmpdir) 

255 

256 

257class LegacyTestdirPlugin: 

258 @staticmethod 

259 @fixture 

260 def testdir(pytester: Pytester) -> Testdir: 

261 """ 

262 Identical to :fixture:`pytester`, and provides an instance whose methods return 

263 legacy ``LEGACY_PATH`` objects instead when applicable. 

264 

265 New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. 

266 """ 

267 return Testdir(pytester, _ispytest=True) 

268 

269 

270@final 

271@attr.s(init=False, auto_attribs=True) 

272class TempdirFactory: 

273 """Backward compatibility wrapper that implements :class:`py.path.local` 

274 for :class:`TempPathFactory`. 

275 

276 .. note:: 

277 These days, it is preferred to use ``tmp_path_factory``. 

278 

279 :ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`. 

280 

281 """ 

282 

283 _tmppath_factory: TempPathFactory 

284 

285 def __init__( 

286 self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False 

287 ) -> None: 

288 check_ispytest(_ispytest) 

289 self._tmppath_factory = tmppath_factory 

290 

291 def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH: 

292 """Same as :meth:`TempPathFactory.mktemp`, but returns a :class:`py.path.local` object.""" 

293 return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve()) 

294 

295 def getbasetemp(self) -> LEGACY_PATH: 

296 """Same as :meth:`TempPathFactory.getbasetemp`, but returns a :class:`py.path.local` object.""" 

297 return legacy_path(self._tmppath_factory.getbasetemp().resolve()) 

298 

299 

300class LegacyTmpdirPlugin: 

301 @staticmethod 

302 @fixture(scope="session") 

303 def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: 

304 """Return a :class:`pytest.TempdirFactory` instance for the test session.""" 

305 # Set dynamically by pytest_configure(). 

306 return request.config._tmpdirhandler # type: ignore 

307 

308 @staticmethod 

309 @fixture 

310 def tmpdir(tmp_path: Path) -> LEGACY_PATH: 

311 """Return a temporary directory path object which is unique to each test 

312 function invocation, created as a sub directory of the base temporary 

313 directory. 

314 

315 By default, a new base temporary directory is created each test session, 

316 and old bases are removed after 3 sessions, to aid in debugging. If 

317 ``--basetemp`` is used then it is cleared each session. See :ref:`base 

318 temporary directory`. 

319 

320 The returned object is a `legacy_path`_ object. 

321 

322 .. note:: 

323 These days, it is preferred to use ``tmp_path``. 

324 

325 :ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`. 

326 

327 .. _legacy_path: https://py.readthedocs.io/en/latest/path.html 

328 """ 

329 return legacy_path(tmp_path) 

330 

331 

332def Cache_makedir(self: Cache, name: str) -> LEGACY_PATH: 

333 """Return a directory path object with the given name. 

334 

335 Same as :func:`mkdir`, but returns a legacy py path instance. 

336 """ 

337 return legacy_path(self.mkdir(name)) 

338 

339 

340def FixtureRequest_fspath(self: FixtureRequest) -> LEGACY_PATH: 

341 """(deprecated) The file system path of the test module which collected this test.""" 

342 return legacy_path(self.path) 

343 

344 

345def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH: 

346 """The directory from which pytest was invoked. 

347 

348 Prefer to use ``startpath`` which is a :class:`pathlib.Path`. 

349 

350 :type: LEGACY_PATH 

351 """ 

352 return legacy_path(self.startpath) 

353 

354 

355def Config_invocation_dir(self: Config) -> LEGACY_PATH: 

356 """The directory from which pytest was invoked. 

357 

358 Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`, 

359 which is a :class:`pathlib.Path`. 

360 

361 :type: LEGACY_PATH 

362 """ 

363 return legacy_path(str(self.invocation_params.dir)) 

364 

365 

366def Config_rootdir(self: Config) -> LEGACY_PATH: 

367 """The path to the :ref:`rootdir <rootdir>`. 

368 

369 Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. 

370 

371 :type: LEGACY_PATH 

372 """ 

373 return legacy_path(str(self.rootpath)) 

374 

375 

376def Config_inifile(self: Config) -> Optional[LEGACY_PATH]: 

377 """The path to the :ref:`configfile <configfiles>`. 

378 

379 Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. 

380 

381 :type: Optional[LEGACY_PATH] 

382 """ 

383 return legacy_path(str(self.inipath)) if self.inipath else None 

384 

385 

386def Session_stardir(self: Session) -> LEGACY_PATH: 

387 """The path from which pytest was invoked. 

388 

389 Prefer to use ``startpath`` which is a :class:`pathlib.Path`. 

390 

391 :type: LEGACY_PATH 

392 """ 

393 return legacy_path(self.startpath) 

394 

395 

396def Config__getini_unknown_type( 

397 self, name: str, type: str, value: Union[str, List[str]] 

398): 

399 if type == "pathlist": 

400 # TODO: This assert is probably not valid in all cases. 

401 assert self.inipath is not None 

402 dp = self.inipath.parent 

403 input_values = shlex.split(value) if isinstance(value, str) else value 

404 return [legacy_path(str(dp / x)) for x in input_values] 

405 else: 

406 raise ValueError(f"unknown configuration type: {type}", value) 

407 

408 

409def Node_fspath(self: Node) -> LEGACY_PATH: 

410 """(deprecated) returns a legacy_path copy of self.path""" 

411 return legacy_path(self.path) 

412 

413 

414def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None: 

415 self.path = Path(value) 

416 

417 

418@hookimpl(tryfirst=True) 

419def pytest_load_initial_conftests(early_config: Config) -> None: 

420 """Monkeypatch legacy path attributes in several classes, as early as possible.""" 

421 mp = MonkeyPatch() 

422 early_config.add_cleanup(mp.undo) 

423 

424 # Add Cache.makedir(). 

425 mp.setattr(Cache, "makedir", Cache_makedir, raising=False) 

426 

427 # Add FixtureRequest.fspath property. 

428 mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False) 

429 

430 # Add TerminalReporter.startdir property. 

431 mp.setattr( 

432 TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False 

433 ) 

434 

435 # Add Config.{invocation_dir,rootdir,inifile} properties. 

436 mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False) 

437 mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False) 

438 mp.setattr(Config, "inifile", property(Config_inifile), raising=False) 

439 

440 # Add Session.startdir property. 

441 mp.setattr(Session, "startdir", property(Session_stardir), raising=False) 

442 

443 # Add pathlist configuration type. 

444 mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type) 

445 

446 # Add Node.fspath property. 

447 mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False) 

448 

449 

450@hookimpl 

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

452 """Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed.""" 

453 if config.pluginmanager.has_plugin("tmpdir"): 

454 mp = MonkeyPatch() 

455 config.add_cleanup(mp.undo) 

456 # Create TmpdirFactory and attach it to the config object. 

457 # 

458 # This is to comply with existing plugins which expect the handler to be 

459 # available at pytest_configure time, but ideally should be moved entirely 

460 # to the tmpdir_factory session fixture. 

461 try: 

462 tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined] 

463 except AttributeError: 

464 # tmpdir plugin is blocked. 

465 pass 

466 else: 

467 _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) 

468 mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) 

469 

470 config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir") 

471 

472 

473@hookimpl 

474def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None: 

475 # pytester is not loaded by default and is commonly loaded from a conftest, 

476 # so checking for it in `pytest_configure` is not enough. 

477 is_pytester = plugin is manager.get_plugin("pytester") 

478 if is_pytester and not manager.is_registered(LegacyTestdirPlugin): 

479 manager.register(LegacyTestdirPlugin, "legacypath-pytester")