legacypath.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  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. import attr
  10. from iniconfig import SectionWrapper
  11. from _pytest.cacheprovider import Cache
  12. from _pytest.compat import final
  13. from _pytest.compat import LEGACY_PATH
  14. from _pytest.compat import legacy_path
  15. from _pytest.config import Config
  16. from _pytest.config import hookimpl
  17. from _pytest.config import PytestPluginManager
  18. from _pytest.deprecated import check_ispytest
  19. from _pytest.fixtures import fixture
  20. from _pytest.fixtures import FixtureRequest
  21. from _pytest.main import Session
  22. from _pytest.monkeypatch import MonkeyPatch
  23. from _pytest.nodes import Collector
  24. from _pytest.nodes import Item
  25. from _pytest.nodes import Node
  26. from _pytest.pytester import HookRecorder
  27. from _pytest.pytester import Pytester
  28. from _pytest.pytester import RunResult
  29. from _pytest.terminal import TerminalReporter
  30. from _pytest.tmpdir import TempPathFactory
  31. if TYPE_CHECKING:
  32. from typing_extensions import Final
  33. import pexpect
  34. @final
  35. class Testdir:
  36. """
  37. Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead.
  38. All methods just forward to an internal :class:`Pytester` instance, converting results
  39. to `legacy_path` objects as necessary.
  40. """
  41. __test__ = False
  42. CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN
  43. TimeoutExpired: "Final" = Pytester.TimeoutExpired
  44. def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
  45. check_ispytest(_ispytest)
  46. self._pytester = pytester
  47. @property
  48. def tmpdir(self) -> LEGACY_PATH:
  49. """Temporary directory where tests are executed."""
  50. return legacy_path(self._pytester.path)
  51. @property
  52. def test_tmproot(self) -> LEGACY_PATH:
  53. return legacy_path(self._pytester._test_tmproot)
  54. @property
  55. def request(self):
  56. return self._pytester._request
  57. @property
  58. def plugins(self):
  59. return self._pytester.plugins
  60. @plugins.setter
  61. def plugins(self, plugins):
  62. self._pytester.plugins = plugins
  63. @property
  64. def monkeypatch(self) -> MonkeyPatch:
  65. return self._pytester._monkeypatch
  66. def make_hook_recorder(self, pluginmanager) -> HookRecorder:
  67. """See :meth:`Pytester.make_hook_recorder`."""
  68. return self._pytester.make_hook_recorder(pluginmanager)
  69. def chdir(self) -> None:
  70. """See :meth:`Pytester.chdir`."""
  71. return self._pytester.chdir()
  72. def finalize(self) -> None:
  73. """See :meth:`Pytester._finalize`."""
  74. return self._pytester._finalize()
  75. def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH:
  76. """See :meth:`Pytester.makefile`."""
  77. if ext and not ext.startswith("."):
  78. # pytester.makefile is going to throw a ValueError in a way that
  79. # testdir.makefile did not, because
  80. # pathlib.Path is stricter suffixes than py.path
  81. # This ext arguments is likely user error, but since testdir has
  82. # allowed this, we will prepend "." as a workaround to avoid breaking
  83. # testdir usage that worked before
  84. ext = "." + ext
  85. return legacy_path(self._pytester.makefile(ext, *args, **kwargs))
  86. def makeconftest(self, source) -> LEGACY_PATH:
  87. """See :meth:`Pytester.makeconftest`."""
  88. return legacy_path(self._pytester.makeconftest(source))
  89. def makeini(self, source) -> LEGACY_PATH:
  90. """See :meth:`Pytester.makeini`."""
  91. return legacy_path(self._pytester.makeini(source))
  92. def getinicfg(self, source: str) -> SectionWrapper:
  93. """See :meth:`Pytester.getinicfg`."""
  94. return self._pytester.getinicfg(source)
  95. def makepyprojecttoml(self, source) -> LEGACY_PATH:
  96. """See :meth:`Pytester.makepyprojecttoml`."""
  97. return legacy_path(self._pytester.makepyprojecttoml(source))
  98. def makepyfile(self, *args, **kwargs) -> LEGACY_PATH:
  99. """See :meth:`Pytester.makepyfile`."""
  100. return legacy_path(self._pytester.makepyfile(*args, **kwargs))
  101. def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH:
  102. """See :meth:`Pytester.maketxtfile`."""
  103. return legacy_path(self._pytester.maketxtfile(*args, **kwargs))
  104. def syspathinsert(self, path=None) -> None:
  105. """See :meth:`Pytester.syspathinsert`."""
  106. return self._pytester.syspathinsert(path)
  107. def mkdir(self, name) -> LEGACY_PATH:
  108. """See :meth:`Pytester.mkdir`."""
  109. return legacy_path(self._pytester.mkdir(name))
  110. def mkpydir(self, name) -> LEGACY_PATH:
  111. """See :meth:`Pytester.mkpydir`."""
  112. return legacy_path(self._pytester.mkpydir(name))
  113. def copy_example(self, name=None) -> LEGACY_PATH:
  114. """See :meth:`Pytester.copy_example`."""
  115. return legacy_path(self._pytester.copy_example(name))
  116. def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]:
  117. """See :meth:`Pytester.getnode`."""
  118. return self._pytester.getnode(config, arg)
  119. def getpathnode(self, path):
  120. """See :meth:`Pytester.getpathnode`."""
  121. return self._pytester.getpathnode(path)
  122. def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]:
  123. """See :meth:`Pytester.genitems`."""
  124. return self._pytester.genitems(colitems)
  125. def runitem(self, source):
  126. """See :meth:`Pytester.runitem`."""
  127. return self._pytester.runitem(source)
  128. def inline_runsource(self, source, *cmdlineargs):
  129. """See :meth:`Pytester.inline_runsource`."""
  130. return self._pytester.inline_runsource(source, *cmdlineargs)
  131. def inline_genitems(self, *args):
  132. """See :meth:`Pytester.inline_genitems`."""
  133. return self._pytester.inline_genitems(*args)
  134. def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False):
  135. """See :meth:`Pytester.inline_run`."""
  136. return self._pytester.inline_run(
  137. *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc
  138. )
  139. def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
  140. """See :meth:`Pytester.runpytest_inprocess`."""
  141. return self._pytester.runpytest_inprocess(*args, **kwargs)
  142. def runpytest(self, *args, **kwargs) -> RunResult:
  143. """See :meth:`Pytester.runpytest`."""
  144. return self._pytester.runpytest(*args, **kwargs)
  145. def parseconfig(self, *args) -> Config:
  146. """See :meth:`Pytester.parseconfig`."""
  147. return self._pytester.parseconfig(*args)
  148. def parseconfigure(self, *args) -> Config:
  149. """See :meth:`Pytester.parseconfigure`."""
  150. return self._pytester.parseconfigure(*args)
  151. def getitem(self, source, funcname="test_func"):
  152. """See :meth:`Pytester.getitem`."""
  153. return self._pytester.getitem(source, funcname)
  154. def getitems(self, source):
  155. """See :meth:`Pytester.getitems`."""
  156. return self._pytester.getitems(source)
  157. def getmodulecol(self, source, configargs=(), withinit=False):
  158. """See :meth:`Pytester.getmodulecol`."""
  159. return self._pytester.getmodulecol(
  160. source, configargs=configargs, withinit=withinit
  161. )
  162. def collect_by_name(
  163. self, modcol: Collector, name: str
  164. ) -> Optional[Union[Item, Collector]]:
  165. """See :meth:`Pytester.collect_by_name`."""
  166. return self._pytester.collect_by_name(modcol, name)
  167. def popen(
  168. self,
  169. cmdargs,
  170. stdout=subprocess.PIPE,
  171. stderr=subprocess.PIPE,
  172. stdin=CLOSE_STDIN,
  173. **kw,
  174. ):
  175. """See :meth:`Pytester.popen`."""
  176. return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw)
  177. def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult:
  178. """See :meth:`Pytester.run`."""
  179. return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin)
  180. def runpython(self, script) -> RunResult:
  181. """See :meth:`Pytester.runpython`."""
  182. return self._pytester.runpython(script)
  183. def runpython_c(self, command):
  184. """See :meth:`Pytester.runpython_c`."""
  185. return self._pytester.runpython_c(command)
  186. def runpytest_subprocess(self, *args, timeout=None) -> RunResult:
  187. """See :meth:`Pytester.runpytest_subprocess`."""
  188. return self._pytester.runpytest_subprocess(*args, timeout=timeout)
  189. def spawn_pytest(
  190. self, string: str, expect_timeout: float = 10.0
  191. ) -> "pexpect.spawn":
  192. """See :meth:`Pytester.spawn_pytest`."""
  193. return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout)
  194. def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
  195. """See :meth:`Pytester.spawn`."""
  196. return self._pytester.spawn(cmd, expect_timeout=expect_timeout)
  197. def __repr__(self) -> str:
  198. return f"<Testdir {self.tmpdir!r}>"
  199. def __str__(self) -> str:
  200. return str(self.tmpdir)
  201. class LegacyTestdirPlugin:
  202. @staticmethod
  203. @fixture
  204. def testdir(pytester: Pytester) -> Testdir:
  205. """
  206. Identical to :fixture:`pytester`, and provides an instance whose methods return
  207. legacy ``LEGACY_PATH`` objects instead when applicable.
  208. New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
  209. """
  210. return Testdir(pytester, _ispytest=True)
  211. @final
  212. @attr.s(init=False, auto_attribs=True)
  213. class TempdirFactory:
  214. """Backward compatibility wrapper that implements :class:``_pytest.compat.LEGACY_PATH``
  215. for :class:``TempPathFactory``."""
  216. _tmppath_factory: TempPathFactory
  217. def __init__(
  218. self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False
  219. ) -> None:
  220. check_ispytest(_ispytest)
  221. self._tmppath_factory = tmppath_factory
  222. def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH:
  223. """Same as :meth:`TempPathFactory.mktemp`, but returns a ``_pytest.compat.LEGACY_PATH`` object."""
  224. return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve())
  225. def getbasetemp(self) -> LEGACY_PATH:
  226. """Backward compat wrapper for ``_tmppath_factory.getbasetemp``."""
  227. return legacy_path(self._tmppath_factory.getbasetemp().resolve())
  228. class LegacyTmpdirPlugin:
  229. @staticmethod
  230. @fixture(scope="session")
  231. def tmpdir_factory(request: FixtureRequest) -> TempdirFactory:
  232. """Return a :class:`pytest.TempdirFactory` instance for the test session."""
  233. # Set dynamically by pytest_configure().
  234. return request.config._tmpdirhandler # type: ignore
  235. @staticmethod
  236. @fixture
  237. def tmpdir(tmp_path: Path) -> LEGACY_PATH:
  238. """Return a temporary directory path object which is unique to each test
  239. function invocation, created as a sub directory of the base temporary
  240. directory.
  241. By default, a new base temporary directory is created each test session,
  242. and old bases are removed after 3 sessions, to aid in debugging. If
  243. ``--basetemp`` is used then it is cleared each session. See :ref:`base
  244. temporary directory`.
  245. The returned object is a `legacy_path`_ object.
  246. .. _legacy_path: https://py.readthedocs.io/en/latest/path.html
  247. """
  248. return legacy_path(tmp_path)
  249. def Cache_makedir(self: Cache, name: str) -> LEGACY_PATH:
  250. """Return a directory path object with the given name.
  251. Same as :func:`mkdir`, but returns a legacy py path instance.
  252. """
  253. return legacy_path(self.mkdir(name))
  254. def FixtureRequest_fspath(self: FixtureRequest) -> LEGACY_PATH:
  255. """(deprecated) The file system path of the test module which collected this test."""
  256. return legacy_path(self.path)
  257. def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH:
  258. """The directory from which pytest was invoked.
  259. Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
  260. :type: LEGACY_PATH
  261. """
  262. return legacy_path(self.startpath)
  263. def Config_invocation_dir(self: Config) -> LEGACY_PATH:
  264. """The directory from which pytest was invoked.
  265. Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`,
  266. which is a :class:`pathlib.Path`.
  267. :type: LEGACY_PATH
  268. """
  269. return legacy_path(str(self.invocation_params.dir))
  270. def Config_rootdir(self: Config) -> LEGACY_PATH:
  271. """The path to the :ref:`rootdir <rootdir>`.
  272. Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`.
  273. :type: LEGACY_PATH
  274. """
  275. return legacy_path(str(self.rootpath))
  276. def Config_inifile(self: Config) -> Optional[LEGACY_PATH]:
  277. """The path to the :ref:`configfile <configfiles>`.
  278. Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`.
  279. :type: Optional[LEGACY_PATH]
  280. """
  281. return legacy_path(str(self.inipath)) if self.inipath else None
  282. def Session_stardir(self: Session) -> LEGACY_PATH:
  283. """The path from which pytest was invoked.
  284. Prefer to use ``startpath`` which is a :class:`pathlib.Path`.
  285. :type: LEGACY_PATH
  286. """
  287. return legacy_path(self.startpath)
  288. def Config__getini_unknown_type(
  289. self, name: str, type: str, value: Union[str, List[str]]
  290. ):
  291. if type == "pathlist":
  292. # TODO: This assert is probably not valid in all cases.
  293. assert self.inipath is not None
  294. dp = self.inipath.parent
  295. input_values = shlex.split(value) if isinstance(value, str) else value
  296. return [legacy_path(str(dp / x)) for x in input_values]
  297. else:
  298. raise ValueError(f"unknown configuration type: {type}", value)
  299. def Node_fspath(self: Node) -> LEGACY_PATH:
  300. """(deprecated) returns a legacy_path copy of self.path"""
  301. return legacy_path(self.path)
  302. def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None:
  303. self.path = Path(value)
  304. @hookimpl(tryfirst=True)
  305. def pytest_load_initial_conftests(early_config: Config) -> None:
  306. """Monkeypatch legacy path attributes in several classes, as early as possible."""
  307. mp = MonkeyPatch()
  308. early_config.add_cleanup(mp.undo)
  309. # Add Cache.makedir().
  310. mp.setattr(Cache, "makedir", Cache_makedir, raising=False)
  311. # Add FixtureRequest.fspath property.
  312. mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False)
  313. # Add TerminalReporter.startdir property.
  314. mp.setattr(
  315. TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False
  316. )
  317. # Add Config.{invocation_dir,rootdir,inifile} properties.
  318. mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False)
  319. mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False)
  320. mp.setattr(Config, "inifile", property(Config_inifile), raising=False)
  321. # Add Session.startdir property.
  322. mp.setattr(Session, "startdir", property(Session_stardir), raising=False)
  323. # Add pathlist configuration type.
  324. mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type)
  325. # Add Node.fspath property.
  326. mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False)
  327. @hookimpl
  328. def pytest_configure(config: Config) -> None:
  329. """Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed."""
  330. if config.pluginmanager.has_plugin("tmpdir"):
  331. mp = MonkeyPatch()
  332. config.add_cleanup(mp.undo)
  333. # Create TmpdirFactory and attach it to the config object.
  334. #
  335. # This is to comply with existing plugins which expect the handler to be
  336. # available at pytest_configure time, but ideally should be moved entirely
  337. # to the tmpdir_factory session fixture.
  338. try:
  339. tmp_path_factory = config._tmp_path_factory # type: ignore[attr-defined]
  340. except AttributeError:
  341. # tmpdir plugin is blocked.
  342. pass
  343. else:
  344. _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True)
  345. mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False)
  346. config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir")
  347. @hookimpl
  348. def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None:
  349. # pytester is not loaded by default and is commonly loaded from a conftest,
  350. # so checking for it in `pytest_configure` is not enough.
  351. is_pytester = plugin is manager.get_plugin("pytester")
  352. if is_pytester and not manager.is_registered(LegacyTestdirPlugin):
  353. manager.register(LegacyTestdirPlugin, "legacypath-pytester")