| | |
| | """Add backward compatibility support for the legacy py path type.""" |
| |
|
| | from __future__ import annotations |
| |
|
| | import dataclasses |
| | from pathlib import Path |
| | import shlex |
| | import subprocess |
| | from typing import Final |
| | from typing import final |
| | from typing import TYPE_CHECKING |
| |
|
| | from iniconfig import SectionWrapper |
| |
|
| | from _pytest.cacheprovider import Cache |
| | from _pytest.compat import LEGACY_PATH |
| | from _pytest.compat import legacy_path |
| | from _pytest.config import Config |
| | from _pytest.config import hookimpl |
| | from _pytest.config import PytestPluginManager |
| | from _pytest.deprecated import check_ispytest |
| | from _pytest.fixtures import fixture |
| | from _pytest.fixtures import FixtureRequest |
| | from _pytest.main import Session |
| | from _pytest.monkeypatch import MonkeyPatch |
| | from _pytest.nodes import Collector |
| | from _pytest.nodes import Item |
| | from _pytest.nodes import Node |
| | from _pytest.pytester import HookRecorder |
| | from _pytest.pytester import Pytester |
| | from _pytest.pytester import RunResult |
| | from _pytest.terminal import TerminalReporter |
| | from _pytest.tmpdir import TempPathFactory |
| |
|
| |
|
| | if TYPE_CHECKING: |
| | import pexpect |
| |
|
| |
|
| | @final |
| | class Testdir: |
| | """ |
| | Similar to :class:`Pytester`, but this class works with legacy legacy_path objects instead. |
| | |
| | All methods just forward to an internal :class:`Pytester` instance, converting results |
| | to `legacy_path` objects as necessary. |
| | """ |
| |
|
| | __test__ = False |
| |
|
| | CLOSE_STDIN: Final = Pytester.CLOSE_STDIN |
| | TimeoutExpired: Final = Pytester.TimeoutExpired |
| |
|
| | def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None: |
| | check_ispytest(_ispytest) |
| | self._pytester = pytester |
| |
|
| | @property |
| | def tmpdir(self) -> LEGACY_PATH: |
| | """Temporary directory where tests are executed.""" |
| | return legacy_path(self._pytester.path) |
| |
|
| | @property |
| | def test_tmproot(self) -> LEGACY_PATH: |
| | return legacy_path(self._pytester._test_tmproot) |
| |
|
| | @property |
| | def request(self): |
| | return self._pytester._request |
| |
|
| | @property |
| | def plugins(self): |
| | return self._pytester.plugins |
| |
|
| | @plugins.setter |
| | def plugins(self, plugins): |
| | self._pytester.plugins = plugins |
| |
|
| | @property |
| | def monkeypatch(self) -> MonkeyPatch: |
| | return self._pytester._monkeypatch |
| |
|
| | def make_hook_recorder(self, pluginmanager) -> HookRecorder: |
| | """See :meth:`Pytester.make_hook_recorder`.""" |
| | return self._pytester.make_hook_recorder(pluginmanager) |
| |
|
| | def chdir(self) -> None: |
| | """See :meth:`Pytester.chdir`.""" |
| | return self._pytester.chdir() |
| |
|
| | def finalize(self) -> None: |
| | return self._pytester._finalize() |
| |
|
| | def makefile(self, ext, *args, **kwargs) -> LEGACY_PATH: |
| | """See :meth:`Pytester.makefile`.""" |
| | if ext and not ext.startswith("."): |
| | |
| | |
| | |
| | |
| | |
| | |
| | ext = "." + ext |
| | return legacy_path(self._pytester.makefile(ext, *args, **kwargs)) |
| |
|
| | def makeconftest(self, source) -> LEGACY_PATH: |
| | """See :meth:`Pytester.makeconftest`.""" |
| | return legacy_path(self._pytester.makeconftest(source)) |
| |
|
| | def makeini(self, source) -> LEGACY_PATH: |
| | """See :meth:`Pytester.makeini`.""" |
| | return legacy_path(self._pytester.makeini(source)) |
| |
|
| | def getinicfg(self, source: str) -> SectionWrapper: |
| | """See :meth:`Pytester.getinicfg`.""" |
| | return self._pytester.getinicfg(source) |
| |
|
| | def makepyprojecttoml(self, source) -> LEGACY_PATH: |
| | """See :meth:`Pytester.makepyprojecttoml`.""" |
| | return legacy_path(self._pytester.makepyprojecttoml(source)) |
| |
|
| | def makepyfile(self, *args, **kwargs) -> LEGACY_PATH: |
| | """See :meth:`Pytester.makepyfile`.""" |
| | return legacy_path(self._pytester.makepyfile(*args, **kwargs)) |
| |
|
| | def maketxtfile(self, *args, **kwargs) -> LEGACY_PATH: |
| | """See :meth:`Pytester.maketxtfile`.""" |
| | return legacy_path(self._pytester.maketxtfile(*args, **kwargs)) |
| |
|
| | def syspathinsert(self, path=None) -> None: |
| | """See :meth:`Pytester.syspathinsert`.""" |
| | return self._pytester.syspathinsert(path) |
| |
|
| | def mkdir(self, name) -> LEGACY_PATH: |
| | """See :meth:`Pytester.mkdir`.""" |
| | return legacy_path(self._pytester.mkdir(name)) |
| |
|
| | def mkpydir(self, name) -> LEGACY_PATH: |
| | """See :meth:`Pytester.mkpydir`.""" |
| | return legacy_path(self._pytester.mkpydir(name)) |
| |
|
| | def copy_example(self, name=None) -> LEGACY_PATH: |
| | """See :meth:`Pytester.copy_example`.""" |
| | return legacy_path(self._pytester.copy_example(name)) |
| |
|
| | def getnode(self, config: Config, arg) -> Item | Collector | None: |
| | """See :meth:`Pytester.getnode`.""" |
| | return self._pytester.getnode(config, arg) |
| |
|
| | def getpathnode(self, path): |
| | """See :meth:`Pytester.getpathnode`.""" |
| | return self._pytester.getpathnode(path) |
| |
|
| | def genitems(self, colitems: list[Item | Collector]) -> list[Item]: |
| | """See :meth:`Pytester.genitems`.""" |
| | return self._pytester.genitems(colitems) |
| |
|
| | def runitem(self, source): |
| | """See :meth:`Pytester.runitem`.""" |
| | return self._pytester.runitem(source) |
| |
|
| | def inline_runsource(self, source, *cmdlineargs): |
| | """See :meth:`Pytester.inline_runsource`.""" |
| | return self._pytester.inline_runsource(source, *cmdlineargs) |
| |
|
| | def inline_genitems(self, *args): |
| | """See :meth:`Pytester.inline_genitems`.""" |
| | return self._pytester.inline_genitems(*args) |
| |
|
| | def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): |
| | """See :meth:`Pytester.inline_run`.""" |
| | return self._pytester.inline_run( |
| | *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc |
| | ) |
| |
|
| | def runpytest_inprocess(self, *args, **kwargs) -> RunResult: |
| | """See :meth:`Pytester.runpytest_inprocess`.""" |
| | return self._pytester.runpytest_inprocess(*args, **kwargs) |
| |
|
| | def runpytest(self, *args, **kwargs) -> RunResult: |
| | """See :meth:`Pytester.runpytest`.""" |
| | return self._pytester.runpytest(*args, **kwargs) |
| |
|
| | def parseconfig(self, *args) -> Config: |
| | """See :meth:`Pytester.parseconfig`.""" |
| | return self._pytester.parseconfig(*args) |
| |
|
| | def parseconfigure(self, *args) -> Config: |
| | """See :meth:`Pytester.parseconfigure`.""" |
| | return self._pytester.parseconfigure(*args) |
| |
|
| | def getitem(self, source, funcname="test_func"): |
| | """See :meth:`Pytester.getitem`.""" |
| | return self._pytester.getitem(source, funcname) |
| |
|
| | def getitems(self, source): |
| | """See :meth:`Pytester.getitems`.""" |
| | return self._pytester.getitems(source) |
| |
|
| | def getmodulecol(self, source, configargs=(), withinit=False): |
| | """See :meth:`Pytester.getmodulecol`.""" |
| | return self._pytester.getmodulecol( |
| | source, configargs=configargs, withinit=withinit |
| | ) |
| |
|
| | def collect_by_name(self, modcol: Collector, name: str) -> Item | Collector | None: |
| | """See :meth:`Pytester.collect_by_name`.""" |
| | return self._pytester.collect_by_name(modcol, name) |
| |
|
| | def popen( |
| | self, |
| | cmdargs, |
| | stdout=subprocess.PIPE, |
| | stderr=subprocess.PIPE, |
| | stdin=CLOSE_STDIN, |
| | **kw, |
| | ): |
| | """See :meth:`Pytester.popen`.""" |
| | return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw) |
| |
|
| | def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: |
| | """See :meth:`Pytester.run`.""" |
| | return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin) |
| |
|
| | def runpython(self, script) -> RunResult: |
| | """See :meth:`Pytester.runpython`.""" |
| | return self._pytester.runpython(script) |
| |
|
| | def runpython_c(self, command): |
| | """See :meth:`Pytester.runpython_c`.""" |
| | return self._pytester.runpython_c(command) |
| |
|
| | def runpytest_subprocess(self, *args, timeout=None) -> RunResult: |
| | """See :meth:`Pytester.runpytest_subprocess`.""" |
| | return self._pytester.runpytest_subprocess(*args, timeout=timeout) |
| |
|
| | def spawn_pytest(self, string: str, expect_timeout: float = 10.0) -> pexpect.spawn: |
| | """See :meth:`Pytester.spawn_pytest`.""" |
| | return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) |
| |
|
| | def spawn(self, cmd: str, expect_timeout: float = 10.0) -> pexpect.spawn: |
| | """See :meth:`Pytester.spawn`.""" |
| | return self._pytester.spawn(cmd, expect_timeout=expect_timeout) |
| |
|
| | def __repr__(self) -> str: |
| | return f"<Testdir {self.tmpdir!r}>" |
| |
|
| | def __str__(self) -> str: |
| | return str(self.tmpdir) |
| |
|
| |
|
| | class LegacyTestdirPlugin: |
| | @staticmethod |
| | @fixture |
| | def testdir(pytester: Pytester) -> Testdir: |
| | """ |
| | Identical to :fixture:`pytester`, and provides an instance whose methods return |
| | legacy ``LEGACY_PATH`` objects instead when applicable. |
| | |
| | New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. |
| | """ |
| | return Testdir(pytester, _ispytest=True) |
| |
|
| |
|
| | @final |
| | @dataclasses.dataclass |
| | class TempdirFactory: |
| | """Backward compatibility wrapper that implements ``py.path.local`` |
| | for :class:`TempPathFactory`. |
| | |
| | .. note:: |
| | These days, it is preferred to use ``tmp_path_factory``. |
| | |
| | :ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`. |
| | |
| | """ |
| |
|
| | _tmppath_factory: TempPathFactory |
| |
|
| | def __init__( |
| | self, tmppath_factory: TempPathFactory, *, _ispytest: bool = False |
| | ) -> None: |
| | check_ispytest(_ispytest) |
| | self._tmppath_factory = tmppath_factory |
| |
|
| | def mktemp(self, basename: str, numbered: bool = True) -> LEGACY_PATH: |
| | """Same as :meth:`TempPathFactory.mktemp`, but returns a ``py.path.local`` object.""" |
| | return legacy_path(self._tmppath_factory.mktemp(basename, numbered).resolve()) |
| |
|
| | def getbasetemp(self) -> LEGACY_PATH: |
| | """Same as :meth:`TempPathFactory.getbasetemp`, but returns a ``py.path.local`` object.""" |
| | return legacy_path(self._tmppath_factory.getbasetemp().resolve()) |
| |
|
| |
|
| | class LegacyTmpdirPlugin: |
| | @staticmethod |
| | @fixture(scope="session") |
| | def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: |
| | """Return a :class:`pytest.TempdirFactory` instance for the test session.""" |
| | |
| | return request.config._tmpdirhandler |
| |
|
| | @staticmethod |
| | @fixture |
| | def tmpdir(tmp_path: Path) -> LEGACY_PATH: |
| | """Return a temporary directory (as `legacy_path`_ object) |
| | which is unique to each test function invocation. |
| | The temporary directory is created as a subdirectory |
| | of the base temporary directory, with configurable retention, |
| | as discussed in :ref:`temporary directory location and retention`. |
| | |
| | .. note:: |
| | These days, it is preferred to use ``tmp_path``. |
| | |
| | :ref:`About the tmpdir and tmpdir_factory fixtures<tmpdir and tmpdir_factory>`. |
| | |
| | .. _legacy_path: https://py.readthedocs.io/en/latest/path.html |
| | """ |
| | return legacy_path(tmp_path) |
| |
|
| |
|
| | def Cache_makedir(self: Cache, name: str) -> LEGACY_PATH: |
| | """Return a directory path object with the given name. |
| | |
| | Same as :func:`mkdir`, but returns a legacy py path instance. |
| | """ |
| | return legacy_path(self.mkdir(name)) |
| |
|
| |
|
| | def FixtureRequest_fspath(self: FixtureRequest) -> LEGACY_PATH: |
| | """(deprecated) The file system path of the test module which collected this test.""" |
| | return legacy_path(self.path) |
| |
|
| |
|
| | def TerminalReporter_startdir(self: TerminalReporter) -> LEGACY_PATH: |
| | """The directory from which pytest was invoked. |
| | |
| | Prefer to use ``startpath`` which is a :class:`pathlib.Path`. |
| | |
| | :type: LEGACY_PATH |
| | """ |
| | return legacy_path(self.startpath) |
| |
|
| |
|
| | def Config_invocation_dir(self: Config) -> LEGACY_PATH: |
| | """The directory from which pytest was invoked. |
| | |
| | Prefer to use :attr:`invocation_params.dir <InvocationParams.dir>`, |
| | which is a :class:`pathlib.Path`. |
| | |
| | :type: LEGACY_PATH |
| | """ |
| | return legacy_path(str(self.invocation_params.dir)) |
| |
|
| |
|
| | def Config_rootdir(self: Config) -> LEGACY_PATH: |
| | """The path to the :ref:`rootdir <rootdir>`. |
| | |
| | Prefer to use :attr:`rootpath`, which is a :class:`pathlib.Path`. |
| | |
| | :type: LEGACY_PATH |
| | """ |
| | return legacy_path(str(self.rootpath)) |
| |
|
| |
|
| | def Config_inifile(self: Config) -> LEGACY_PATH | None: |
| | """The path to the :ref:`configfile <configfiles>`. |
| | |
| | Prefer to use :attr:`inipath`, which is a :class:`pathlib.Path`. |
| | |
| | :type: Optional[LEGACY_PATH] |
| | """ |
| | return legacy_path(str(self.inipath)) if self.inipath else None |
| |
|
| |
|
| | def Session_startdir(self: Session) -> LEGACY_PATH: |
| | """The path from which pytest was invoked. |
| | |
| | Prefer to use ``startpath`` which is a :class:`pathlib.Path`. |
| | |
| | :type: LEGACY_PATH |
| | """ |
| | return legacy_path(self.startpath) |
| |
|
| |
|
| | def Config__getini_unknown_type(self, name: str, type: str, value: str | list[str]): |
| | if type == "pathlist": |
| | |
| | assert self.inipath is not None |
| | dp = self.inipath.parent |
| | input_values = shlex.split(value) if isinstance(value, str) else value |
| | return [legacy_path(str(dp / x)) for x in input_values] |
| | else: |
| | raise ValueError(f"unknown configuration type: {type}", value) |
| |
|
| |
|
| | def Node_fspath(self: Node) -> LEGACY_PATH: |
| | """(deprecated) returns a legacy_path copy of self.path""" |
| | return legacy_path(self.path) |
| |
|
| |
|
| | def Node_fspath_set(self: Node, value: LEGACY_PATH) -> None: |
| | self.path = Path(value) |
| |
|
| |
|
| | @hookimpl(tryfirst=True) |
| | def pytest_load_initial_conftests(early_config: Config) -> None: |
| | """Monkeypatch legacy path attributes in several classes, as early as possible.""" |
| | mp = MonkeyPatch() |
| | early_config.add_cleanup(mp.undo) |
| |
|
| | |
| | mp.setattr(Cache, "makedir", Cache_makedir, raising=False) |
| |
|
| | |
| | mp.setattr(FixtureRequest, "fspath", property(FixtureRequest_fspath), raising=False) |
| |
|
| | |
| | mp.setattr( |
| | TerminalReporter, "startdir", property(TerminalReporter_startdir), raising=False |
| | ) |
| |
|
| | |
| | mp.setattr(Config, "invocation_dir", property(Config_invocation_dir), raising=False) |
| | mp.setattr(Config, "rootdir", property(Config_rootdir), raising=False) |
| | mp.setattr(Config, "inifile", property(Config_inifile), raising=False) |
| |
|
| | |
| | mp.setattr(Session, "startdir", property(Session_startdir), raising=False) |
| |
|
| | |
| | mp.setattr(Config, "_getini_unknown_type", Config__getini_unknown_type) |
| |
|
| | |
| | mp.setattr(Node, "fspath", property(Node_fspath, Node_fspath_set), raising=False) |
| |
|
| |
|
| | @hookimpl |
| | def pytest_configure(config: Config) -> None: |
| | """Installs the LegacyTmpdirPlugin if the ``tmpdir`` plugin is also installed.""" |
| | if config.pluginmanager.has_plugin("tmpdir"): |
| | mp = MonkeyPatch() |
| | config.add_cleanup(mp.undo) |
| | |
| | |
| | |
| | |
| | |
| | try: |
| | tmp_path_factory = config._tmp_path_factory |
| | except AttributeError: |
| | |
| | pass |
| | else: |
| | _tmpdirhandler = TempdirFactory(tmp_path_factory, _ispytest=True) |
| | mp.setattr(config, "_tmpdirhandler", _tmpdirhandler, raising=False) |
| |
|
| | config.pluginmanager.register(LegacyTmpdirPlugin, "legacypath-tmpdir") |
| |
|
| |
|
| | @hookimpl |
| | def pytest_plugin_registered(plugin: object, manager: PytestPluginManager) -> None: |
| | |
| | |
| | is_pytester = plugin is manager.get_plugin("pytester") |
| | if is_pytester and not manager.is_registered(LegacyTestdirPlugin): |
| | manager.register(LegacyTestdirPlugin, "legacypath-pytester") |
| |
|