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

2 import shlex

3 import subprocess

4 from pathlib import Path

5 from typing import List

6 from typing import Optional

7 from typing import TYPE_CHECKING

8 from typing import Union

9

10 import attr

11 from iniconfig import SectionWrapper

12

13 from _pytest.cacheprovider import Cache

14 from _pytest.compat import final

15 from _pytest.compat import LEGACY_PATH

16 from _pytest.compat import legacy_path

17 from _pytest.config import Config

18 from _pytest.config import hookimpl

19 from _pytest.config import PytestPluginManager

20 from _pytest.deprecated import check_ispytest

21 from _pytest.fixtures import fixture

22 from _pytest.fixtures import FixtureRequest

23 from _pytest.main import Session

24 from _pytest.monkeypatch import MonkeyPatch

25 from _pytest.nodes import Collector

26 from _pytest.nodes import Item

27 from _pytest.nodes import Node

28 from _pytest.pytester import HookRecorder

29 from _pytest.pytester import Pytester

30 from _pytest.pytester import RunResult

31 from _pytest.terminal import TerminalReporter

32 from _pytest.tmpdir import TempPathFactory

33

34 if TYPE_CHECKING:

35 from typing_extensions import Final

36

37 import pexpect

38

39

40@final

41 class 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

257 class 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)

272 class 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

300 class 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

332 def 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

340 def 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

345 def 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

355 def 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

366 def 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

376 def 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

386 def 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

396 def 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

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

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

411 return legacy_path(self.path)

412

413

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

415 self.path = Path(value)

416

417

418@hookimpl(tryfirst=True)

419 def 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

451 def 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

474 def 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")