diff --git a/changelog/7425.feature.rst b/changelog/7425.feature.rst new file mode 100644 index 000000000..55881d207 --- /dev/null +++ b/changelog/7425.feature.rst @@ -0,0 +1,5 @@ +New :fixture:`pytester` fixture, which is identical to :fixture:`testdir` but its methods return :class:`pathlib.Path` when appropriate instead of ``py.path.local``. + +This is part of the movement to use :class:`pathlib.Path` objects internally, in order to remove the dependency to ``py`` in the future. + +Internally, the old :class:`Testdir` is now a thin wrapper around :class:`Pytester`, preserving the old interface. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 15d825084..6795b721c 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -499,17 +499,21 @@ monkeypatch :members: -.. fixture:: testdir +.. fixture:: pytester -testdir -~~~~~~~ +pytester +~~~~~~~~ + +.. versionadded:: 6.2 .. currentmodule:: _pytest.pytester -This fixture provides a :class:`Testdir` instance useful for black-box testing of test files, making it ideal to -test plugins. +Provides a :class:`Pytester` instance that can be used to run and test pytest itself. -To use it, include in your top-most ``conftest.py`` file: +It provides an empty directory where pytest can be executed in isolation, and contains facilities +to write tests, configuration files, and match against expected output. + +To use it, include in your topmost ``conftest.py`` file: .. code-block:: python @@ -517,7 +521,7 @@ To use it, include in your top-most ``conftest.py`` file: -.. autoclass:: Testdir() +.. autoclass:: Pytester() :members: .. autoclass:: RunResult() @@ -526,6 +530,15 @@ To use it, include in your top-most ``conftest.py`` file: .. autoclass:: LineMatcher() :members: +.. fixture:: testdir + +testdir +~~~~~~~ + +Identical to :fixture:`pytester`, but provides an instance whose methods return +legacy ``py.path.local`` objects instead when applicable. + +New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. .. fixture:: recwarn diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index e66e718f1..b7a79b902 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1,16 +1,19 @@ """(Disabled by default) support for testing pytest and pytest plugins.""" import collections.abc +import contextlib import gc import importlib import os import platform import re +import shutil import subprocess import sys import traceback from fnmatch import fnmatch from io import StringIO from pathlib import Path +from typing import Any from typing import Callable from typing import Dict from typing import Generator @@ -19,12 +22,14 @@ from typing import List from typing import Optional from typing import overload from typing import Sequence +from typing import TextIO from typing import Tuple from typing import Type from typing import TYPE_CHECKING from typing import Union from weakref import WeakKeyDictionary +import attr import py from iniconfig import IniConfig @@ -47,7 +52,7 @@ from _pytest.pathlib import make_numbered_dir from _pytest.python import Module from _pytest.reports import CollectReport from _pytest.reports import TestReport -from _pytest.tmpdir import TempdirFactory +from _pytest.tmpdir import TempPathFactory if TYPE_CHECKING: from typing_extensions import Literal @@ -176,11 +181,11 @@ def _pytest(request: FixtureRequest) -> "PytestArg": class PytestArg: def __init__(self, request: FixtureRequest) -> None: - self.request = request + self._request = request def gethookrecorder(self, hook) -> "HookRecorder": hookrecorder = HookRecorder(hook._pm) - self.request.addfinalizer(hookrecorder.finish_recording) + self._request.addfinalizer(hookrecorder.finish_recording) return hookrecorder @@ -430,13 +435,29 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: @pytest.fixture -def testdir(request: FixtureRequest, tmpdir_factory: TempdirFactory) -> "Testdir": - """A :class: `TestDir` instance, that can be used to run and test pytest itself. - - It is particularly useful for testing plugins. It is similar to the `tmpdir` fixture - but provides methods which aid in testing pytest itself. +def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester": """ - return Testdir(request, tmpdir_factory) + Facilities to write tests/configuration files, execute pytest in isolation, and match + against expected output, perfect for black-box testing of pytest plugins. + + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to ``path`` and environment variables during initialization. + + It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path` + fixture but provides methods which aid in testing pytest itself. + """ + return Pytester(request, tmp_path_factory) + + +@pytest.fixture +def testdir(pytester: "Pytester") -> "Testdir": + """ + Identical to :fixture:`pytester`, and provides an instance whose methods return + legacy ``py.path.local`` objects instead when applicable. + + New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. + """ + return Testdir(pytester) @pytest.fixture @@ -599,16 +620,17 @@ class SysPathsSnapshot: @final -class Testdir: - """Temporary test directory with tools to test/run pytest itself. +class Pytester: + """ + Facilities to write tests/configuration files, execute pytest in isolation, and match + against expected output, perfect for black-box testing of pytest plugins. - This is based on the :fixture:`tmpdir` fixture but provides a number of methods - which aid with testing pytest itself. Unless :py:meth:`chdir` is used all - methods will use :py:attr:`tmpdir` as their current working directory. + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to ``path`` and environment variables during initialization. Attributes: - :ivar tmpdir: The :py:class:`py.path.local` instance of the temporary directory. + :ivar Path path: temporary directory path used to create files/run tests from, etc. :ivar plugins: A list of plugins to use with :py:meth:`parseconfig` and @@ -624,8 +646,10 @@ class Testdir: class TimeoutExpired(Exception): pass - def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> None: - self.request = request + def __init__( + self, request: FixtureRequest, tmp_path_factory: TempPathFactory + ) -> None: + self._request = request self._mod_collections: WeakKeyDictionary[ Module, List[Union[Item, Collector]] ] = (WeakKeyDictionary()) @@ -634,46 +658,49 @@ class Testdir: else: name = request.node.name self._name = name - self.tmpdir = tmpdir_factory.mktemp(name, numbered=True) - self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True) + self._path: Path = tmp_path_factory.mktemp(name, numbered=True) self.plugins: List[Union[str, _PluggyPlugin]] = [] self._cwd_snapshot = CwdSnapshot() self._sys_path_snapshot = SysPathsSnapshot() self._sys_modules_snapshot = self.__take_sys_modules_snapshot() self.chdir() - self.request.addfinalizer(self.finalize) - self._method = self.request.config.getoption("--runpytest") + self._request.addfinalizer(self._finalize) + self._method = self._request.config.getoption("--runpytest") + self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) - mp = self.monkeypatch = MonkeyPatch() - mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self.test_tmproot)) + self._monkeypatch = mp = MonkeyPatch() + mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot)) # Ensure no unexpected caching via tox. mp.delenv("TOX_ENV_DIR", raising=False) # Discard outer pytest options. mp.delenv("PYTEST_ADDOPTS", raising=False) # Ensure no user config is used. - tmphome = str(self.tmpdir) + tmphome = str(self.path) mp.setenv("HOME", tmphome) mp.setenv("USERPROFILE", tmphome) # Do not use colors for inner runs by default. mp.setenv("PY_COLORS", "0") + @property + def path(self) -> Path: + """Temporary directory where files are created and pytest is executed.""" + return self._path + def __repr__(self) -> str: - return f"" + return f"" - def __str__(self) -> str: - return str(self.tmpdir) - - def finalize(self) -> None: - """Clean up global state artifacts. + def _finalize(self) -> None: + """ + Clean up global state artifacts. Some methods modify the global interpreter state and this tries to - clean this up. It does not remove the temporary directory however so + clean this up. It does not remove the temporary directory however so it can be looked at after the test run has finished. """ self._sys_modules_snapshot.restore() self._sys_path_snapshot.restore() self._cwd_snapshot.restore() - self.monkeypatch.undo() + self._monkeypatch.undo() def __take_sys_modules_snapshot(self) -> SysModulesSnapshot: # Some zope modules used by twisted-related tests keep internal state @@ -687,7 +714,7 @@ class Testdir: def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder: """Create a new :py:class:`HookRecorder` for a PluginManager.""" pluginmanager.reprec = reprec = HookRecorder(pluginmanager) - self.request.addfinalizer(reprec.finish_recording) + self._request.addfinalizer(reprec.finish_recording) return reprec def chdir(self) -> None: @@ -695,12 +722,18 @@ class Testdir: This is done automatically upon instantiation. """ - self.tmpdir.chdir() + os.chdir(self.path) - def _makefile(self, ext: str, lines, files, encoding: str = "utf-8"): + def _makefile( + self, + ext: str, + lines: Sequence[Union[Any, bytes]], + files: Dict[str, str], + encoding: str = "utf-8", + ) -> Path: items = list(files.items()) - def to_text(s): + def to_text(s: Union[Any, bytes]) -> str: return s.decode(encoding) if isinstance(s, bytes) else str(s) if lines: @@ -710,17 +743,18 @@ class Testdir: ret = None for basename, value in items: - p = self.tmpdir.join(basename).new(ext=ext) - p.dirpath().ensure_dir() + p = self.path.joinpath(basename).with_suffix(ext) + p.parent.mkdir(parents=True, exist_ok=True) source_ = Source(value) source = "\n".join(to_text(line) for line in source_.lines) - p.write(source.strip().encode(encoding), "wb") + p.write_text(source.strip(), encoding=encoding) if ret is None: ret = p + assert ret is not None return ret - def makefile(self, ext: str, *args: str, **kwargs): - r"""Create new file(s) in the testdir. + def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: + r"""Create new file(s) in the test directory. :param str ext: The extension the file(s) should use, including the dot, e.g. `.py`. @@ -743,27 +777,27 @@ class Testdir: """ return self._makefile(ext, args, kwargs) - def makeconftest(self, source): + def makeconftest(self, source: str) -> Path: """Write a contest.py file with 'source' as contents.""" return self.makepyfile(conftest=source) - def makeini(self, source): + def makeini(self, source: str) -> Path: """Write a tox.ini file with 'source' as contents.""" return self.makefile(".ini", tox=source) - def getinicfg(self, source) -> IniConfig: + def getinicfg(self, source: str) -> IniConfig: """Return the pytest section from the tox.ini config file.""" p = self.makeini(source) return IniConfig(p)["pytest"] - def makepyprojecttoml(self, source): + def makepyprojecttoml(self, source: str) -> Path: """Write a pyproject.toml file with 'source' as contents. .. versionadded:: 6.0 """ return self.makefile(".toml", pyproject=source) - def makepyfile(self, *args, **kwargs): + def makepyfile(self, *args, **kwargs) -> Path: r"""Shortcut for .makefile() with a .py extension. Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting @@ -783,7 +817,7 @@ class Testdir: """ return self._makefile(".py", args, kwargs) - def maketxtfile(self, *args, **kwargs): + def maketxtfile(self, *args, **kwargs) -> Path: r"""Shortcut for .makefile() with a .txt extension. Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting @@ -803,74 +837,77 @@ class Testdir: """ return self._makefile(".txt", args, kwargs) - def syspathinsert(self, path=None) -> None: + def syspathinsert( + self, path: Optional[Union[str, "os.PathLike[str]"]] = None + ) -> None: """Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`. This is undone automatically when this object dies at the end of each test. """ if path is None: - path = self.tmpdir + path = self.path - self.monkeypatch.syspath_prepend(str(path)) + self._monkeypatch.syspath_prepend(str(path)) - def mkdir(self, name) -> py.path.local: + def mkdir(self, name: str) -> Path: """Create a new (sub)directory.""" - return self.tmpdir.mkdir(name) + p = self.path / name + p.mkdir() + return p - def mkpydir(self, name) -> py.path.local: - """Create a new Python package. + def mkpydir(self, name: str) -> Path: + """Create a new python package. This creates a (sub)directory with an empty ``__init__.py`` file so it gets recognised as a Python package. """ - p = self.mkdir(name) - p.ensure("__init__.py") + p = self.path / name + p.mkdir() + p.joinpath("__init__.py").touch() return p - def copy_example(self, name=None) -> py.path.local: + def copy_example(self, name: Optional[str] = None) -> Path: """Copy file from project's directory into the testdir. :param str name: The name of the file to copy. - :returns: Path to the copied directory (inside ``self.tmpdir``). - """ - import warnings - from _pytest.warning_types import PYTESTER_COPY_EXAMPLE + :return: path to the copied directory (inside ``self.path``). - warnings.warn(PYTESTER_COPY_EXAMPLE, stacklevel=2) - example_dir = self.request.config.getini("pytester_example_dir") + """ + example_dir = self._request.config.getini("pytester_example_dir") if example_dir is None: raise ValueError("pytester_example_dir is unset, can't copy examples") - example_dir = self.request.config.rootdir.join(example_dir) + example_dir = Path(str(self._request.config.rootdir)) / example_dir - for extra_element in self.request.node.iter_markers("pytester_example_path"): + for extra_element in self._request.node.iter_markers("pytester_example_path"): assert extra_element.args - example_dir = example_dir.join(*extra_element.args) + example_dir = example_dir.joinpath(*extra_element.args) if name is None: func_name = self._name maybe_dir = example_dir / func_name maybe_file = example_dir / (func_name + ".py") - if maybe_dir.isdir(): + if maybe_dir.is_dir(): example_path = maybe_dir - elif maybe_file.isfile(): + elif maybe_file.is_file(): example_path = maybe_file else: raise LookupError( - "{} cant be found as module or package in {}".format( - func_name, example_dir.bestrelpath(self.request.config.rootdir) - ) + f"{func_name} can't be found as module or package in {example_dir}" ) else: - example_path = example_dir.join(name) + example_path = example_dir.joinpath(name) - if example_path.isdir() and not example_path.join("__init__.py").isfile(): - example_path.copy(self.tmpdir) - return self.tmpdir - elif example_path.isfile(): - result = self.tmpdir.join(example_path.basename) - example_path.copy(result) + if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file(): + # TODO: py.path.local.copy can copy files to existing directories, + # while with shutil.copytree the destination directory cannot exist, + # we will need to roll our own in order to drop py.path.local completely + py.path.local(example_path).copy(py.path.local(self.path)) + return self.path + elif example_path.is_file(): + result = self.path.joinpath(example_path.name) + shutil.copy(example_path, result) return result else: raise LookupError( @@ -879,7 +916,9 @@ class Testdir: Session = Session - def getnode(self, config: Config, arg): + def getnode( + self, config: Config, arg: Union[str, "os.PathLike[str]"] + ) -> Optional[Union[Collector, Item]]: """Return the collection node of a file. :param _pytest.config.Config config: @@ -896,7 +935,7 @@ class Testdir: config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res - def getpathnode(self, path): + def getpathnode(self, path: Union[str, "os.PathLike[str]"]): """Return the collection node of a file. This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to @@ -904,6 +943,7 @@ class Testdir: :param py.path.local path: Path to the file. """ + path = py.path.local(path) config = self.parseconfigure(path) session = Session.from_config(config) x = session.fspath.bestrelpath(path) @@ -924,7 +964,7 @@ class Testdir: result.extend(session.genitems(colitem)) return result - def runitem(self, source): + def runitem(self, source: str) -> Any: """Run the "test_func" Item. The calling test instance (class containing the test method) must @@ -935,11 +975,11 @@ class Testdir: # used from runner functional tests item = self.getitem(source) # the test class where we are called from wants to provide the runner - testclassinstance = self.request.instance + testclassinstance = self._request.instance runner = testclassinstance.getrunner() return runner(item) - def inline_runsource(self, source, *cmdlineargs) -> HookRecorder: + def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder: """Run a test module in process using ``pytest.main()``. This run writes "source" into a temporary file and runs @@ -968,7 +1008,10 @@ class Testdir: return items, rec def inline_run( - self, *args, plugins=(), no_reraise_ctrlc: bool = False + self, + *args: Union[str, "os.PathLike[str]"], + plugins=(), + no_reraise_ctrlc: bool = False, ) -> HookRecorder: """Run ``pytest.main()`` in-process, returning a HookRecorder. @@ -1016,7 +1059,7 @@ class Testdir: rec.append(self.make_hook_recorder(config.pluginmanager)) plugins.append(Collect()) - ret = pytest.main(list(args), plugins=plugins) + ret = pytest.main([str(x) for x in args], plugins=plugins) if len(rec) == 1: reprec = rec.pop() else: @@ -1024,7 +1067,7 @@ class Testdir: class reprec: # type: ignore pass - reprec.ret = ret + reprec.ret = ret # type: ignore # Typically we reraise keyboard interrupts from the child run # because it's our user requesting interruption of the testing. @@ -1037,7 +1080,9 @@ class Testdir: for finalizer in finalizers: finalizer() - def runpytest_inprocess(self, *args, **kwargs) -> RunResult: + def runpytest_inprocess( + self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any + ) -> RunResult: """Return result of running pytest in-process, providing a similar interface to what self.runpytest() provides.""" syspathinsert = kwargs.pop("syspathinsert", False) @@ -1079,26 +1124,30 @@ class Testdir: res.reprec = reprec # type: ignore return res - def runpytest(self, *args, **kwargs) -> RunResult: + def runpytest( + self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any + ) -> RunResult: """Run pytest inline or in a subprocess, depending on the command line option "--runpytest" and return a :py:class:`RunResult`.""" - args = self._ensure_basetemp(args) + new_args = self._ensure_basetemp(args) if self._method == "inprocess": - return self.runpytest_inprocess(*args, **kwargs) + return self.runpytest_inprocess(*new_args, **kwargs) elif self._method == "subprocess": - return self.runpytest_subprocess(*args, **kwargs) + return self.runpytest_subprocess(*new_args, **kwargs) raise RuntimeError(f"Unrecognized runpytest option: {self._method}") - def _ensure_basetemp(self, args): - args = list(args) - for x in args: + def _ensure_basetemp( + self, args: Sequence[Union[str, "os.PathLike[str]"]] + ) -> List[Union[str, "os.PathLike[str]"]]: + new_args = list(args) + for x in new_args: if str(x).startswith("--basetemp"): break else: - args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp")) - return args + new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp")) + return new_args - def parseconfig(self, *args) -> Config: + def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config: """Return a new pytest Config instance from given commandline args. This invokes the pytest bootstrapping code in _pytest.config to create @@ -1109,18 +1158,19 @@ class Testdir: If :py:attr:`plugins` has been populated they should be plugin modules to be registered with the PluginManager. """ - args = self._ensure_basetemp(args) - import _pytest.config - config = _pytest.config._prepareconfig(args, self.plugins) # type: ignore[arg-type] + new_args = self._ensure_basetemp(args) + new_args = [str(x) for x in new_args] + + config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type] # we don't know what the test will do with this half-setup config # object and thus we make sure it gets unconfigured properly in any # case (otherwise capturing could still be active, for example) - self.request.addfinalizer(config._ensure_unconfigure) + self._request.addfinalizer(config._ensure_unconfigure) return config - def parseconfigure(self, *args) -> Config: + def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config: """Return a new pytest configured Config instance. Returns a new :py:class:`_pytest.config.Config` instance like @@ -1130,7 +1180,7 @@ class Testdir: config._do_configure() return config - def getitem(self, source, funcname: str = "test_func") -> Item: + def getitem(self, source: str, funcname: str = "test_func") -> Item: """Return the test item for a test function. Writes the source to a python file and runs pytest's collection on @@ -1150,7 +1200,7 @@ class Testdir: funcname, source, items ) - def getitems(self, source) -> List[Item]: + def getitems(self, source: str) -> List[Item]: """Return all test items collected from the module. Writes the source to a Python file and runs pytest's collection on @@ -1159,7 +1209,9 @@ class Testdir: modcol = self.getmodulecol(source) return self.genitems([modcol]) - def getmodulecol(self, source, configargs=(), withinit: bool = False): + def getmodulecol( + self, source: Union[str, Path], configargs=(), *, withinit: bool = False + ): """Return the module collection node for ``source``. Writes ``source`` to a file using :py:meth:`makepyfile` and then @@ -1177,10 +1229,10 @@ class Testdir: directory to ensure it is a package. """ if isinstance(source, Path): - path = self.tmpdir.join(str(source)) + path = self.path.joinpath(source) assert not withinit, "not supported for paths" else: - kw = {self._name: Source(source).strip()} + kw = {self._name: str(source)} path = self.makepyfile(**kw) if withinit: self.makepyfile(__init__="#") @@ -1208,8 +1260,8 @@ class Testdir: def popen( self, cmdargs, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, + stdout: Union[int, TextIO] = subprocess.PIPE, + stderr: Union[int, TextIO] = subprocess.PIPE, stdin=CLOSE_STDIN, **kw, ): @@ -1244,14 +1296,18 @@ class Testdir: return popen def run( - self, *cmdargs, timeout: Optional[float] = None, stdin=CLOSE_STDIN + self, + *cmdargs: Union[str, "os.PathLike[str]"], + timeout: Optional[float] = None, + stdin=CLOSE_STDIN, ) -> RunResult: """Run a command with arguments. Run a process using subprocess.Popen saving the stdout and stderr. - :param args: - The sequence of arguments to pass to `subprocess.Popen()`. + :param cmdargs: + The sequence of arguments to pass to `subprocess.Popen()`, with path-like objects + being converted to ``str`` automatically. :param timeout: The period in seconds after which to timeout and raise :py:class:`Testdir.TimeoutExpired`. @@ -1266,15 +1322,14 @@ class Testdir: __tracebackhide__ = True cmdargs = tuple( - str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs + os.fspath(arg) if isinstance(arg, os.PathLike) else arg for arg in cmdargs ) - p1 = self.tmpdir.join("stdout") - p2 = self.tmpdir.join("stderr") + p1 = self.path.joinpath("stdout") + p2 = self.path.joinpath("stderr") print("running:", *cmdargs) - print(" in:", py.path.local()) - f1 = open(str(p1), "w", encoding="utf8") - f2 = open(str(p2), "w", encoding="utf8") - try: + print(" in:", Path.cwd()) + + with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2: now = timing.time() popen = self.popen( cmdargs, @@ -1305,23 +1360,16 @@ class Testdir: ret = popen.wait(timeout) except subprocess.TimeoutExpired: handle_timeout() - finally: - f1.close() - f2.close() - f1 = open(str(p1), encoding="utf8") - f2 = open(str(p2), encoding="utf8") - try: + + with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2: out = f1.read().splitlines() err = f2.read().splitlines() - finally: - f1.close() - f2.close() + self._dump_lines(out, sys.stdout) self._dump_lines(err, sys.stderr) - try: + + with contextlib.suppress(ValueError): ret = ExitCode(ret) - except ValueError: - pass return RunResult(ret, out, err, timing.time() - now) def _dump_lines(self, lines, fp): @@ -1366,7 +1414,7 @@ class Testdir: :rtype: RunResult """ __tracebackhide__ = True - p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-") + p = make_numbered_dir(root=self.path, prefix="runpytest-") args = ("--basetemp=%s" % p,) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: @@ -1384,7 +1432,8 @@ class Testdir: The pexpect child is returned. """ - basetemp = self.tmpdir.mkdir("temp-pexpect") + basetemp = self.path / "temp-pexpect" + basetemp.mkdir() invoke = " ".join(map(str, self._getpytestargs())) cmd = f"{invoke} --basetemp={basetemp} {string}" return self.spawn(cmd, expect_timeout=expect_timeout) @@ -1399,10 +1448,10 @@ class Testdir: pytest.skip("pypy-64 bit not supported") if not hasattr(pexpect, "spawn"): pytest.skip("pexpect.spawn not available") - logfile = self.tmpdir.join("spawn.out").open("wb") + logfile = self.path.joinpath("spawn.out").open("wb") child = pexpect.spawn(cmd, logfile=logfile) - self.request.addfinalizer(logfile.close) + self._request.addfinalizer(logfile.close) child.timeout = expect_timeout return child @@ -1425,6 +1474,178 @@ class LineComp: LineMatcher(lines1).fnmatch_lines(lines2) +@final +@attr.s(repr=False, str=False) +class Testdir: + """ + Similar to :class:`Pytester`, but this class works with legacy py.path.local objects instead. + + All methods just forward to an internal :class:`Pytester` instance, converting results + to `py.path.local` objects as necessary. + """ + + __test__ = False + + CLOSE_STDIN = Pytester.CLOSE_STDIN + TimeoutExpired = Pytester.TimeoutExpired + Session = Pytester.Session + + _pytester: Pytester = attr.ib() + + @property + def tmpdir(self) -> py.path.local: + return py.path.local(self._pytester.path) + + @property + def test_tmproot(self) -> py.path.local: + return py.path.local(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: + return self._pytester.make_hook_recorder(pluginmanager) + + def chdir(self) -> None: + return self._pytester.chdir() + + def finalize(self) -> None: + return self._pytester._finalize() + + def makefile(self, ext, *args, **kwargs) -> py.path.local: + return py.path.local(str(self._pytester.makefile(ext, *args, **kwargs))) + + def makeconftest(self, source) -> py.path.local: + return py.path.local(str(self._pytester.makeconftest(source))) + + def makeini(self, source) -> py.path.local: + return py.path.local(str(self._pytester.makeini(source))) + + def getinicfg(self, source) -> py.path.local: + return py.path.local(str(self._pytester.getinicfg(source))) + + def makepyprojecttoml(self, source) -> py.path.local: + return py.path.local(str(self._pytester.makepyprojecttoml(source))) + + def makepyfile(self, *args, **kwargs) -> py.path.local: + return py.path.local(str(self._pytester.makepyfile(*args, **kwargs))) + + def maketxtfile(self, *args, **kwargs) -> py.path.local: + return py.path.local(str(self._pytester.maketxtfile(*args, **kwargs))) + + def syspathinsert(self, path=None) -> None: + return self._pytester.syspathinsert(path) + + def mkdir(self, name) -> py.path.local: + return py.path.local(str(self._pytester.mkdir(name))) + + def mkpydir(self, name) -> py.path.local: + return py.path.local(str(self._pytester.mkpydir(name))) + + def copy_example(self, name=None) -> py.path.local: + return py.path.local(str(self._pytester.copy_example(name))) + + def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]: + return self._pytester.getnode(config, arg) + + def getpathnode(self, path): + return self._pytester.getpathnode(path) + + def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]: + return self._pytester.genitems(colitems) + + def runitem(self, source): + return self._pytester.runitem(source) + + def inline_runsource(self, source, *cmdlineargs): + return self._pytester.inline_runsource(source, *cmdlineargs) + + def inline_genitems(self, *args): + return self._pytester.inline_genitems(*args) + + def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): + return self._pytester.inline_run( + *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc + ) + + def runpytest_inprocess(self, *args, **kwargs) -> RunResult: + return self._pytester.runpytest_inprocess(*args, **kwargs) + + def runpytest(self, *args, **kwargs) -> RunResult: + return self._pytester.runpytest(*args, **kwargs) + + def parseconfig(self, *args) -> Config: + return self._pytester.parseconfig(*args) + + def parseconfigure(self, *args) -> Config: + return self._pytester.parseconfigure(*args) + + def getitem(self, source, funcname="test_func"): + return self._pytester.getitem(source, funcname) + + def getitems(self, source): + return self._pytester.getitems(source) + + def getmodulecol(self, source, configargs=(), withinit=False): + return self._pytester.getmodulecol( + source, configargs=configargs, withinit=withinit + ) + + def collect_by_name( + self, modcol: Module, name: str + ) -> Optional[Union[Item, Collector]]: + return self._pytester.collect_by_name(modcol, name) + + def popen( + self, + cmdargs, + stdout: Union[int, TextIO] = subprocess.PIPE, + stderr: Union[int, TextIO] = subprocess.PIPE, + stdin=CLOSE_STDIN, + **kw, + ): + return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw) + + def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: + return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin) + + def runpython(self, script) -> RunResult: + return self._pytester.runpython(script) + + def runpython_c(self, command): + return self._pytester.runpython_c(command) + + def runpytest_subprocess(self, *args, timeout=None) -> RunResult: + return self._pytester.runpytest_subprocess(*args, timeout=timeout) + + def spawn_pytest( + self, string: str, expect_timeout: float = 10.0 + ) -> "pexpect.spawn": + return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout) + + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": + return self._pytester.spawn(cmd, expect_timeout=expect_timeout) + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return str(self.tmpdir) + + class LineMatcher: """Flexible matching of text. diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index bd3a1d0b7..2fd4d4f6e 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -108,6 +108,3 @@ class UnformattedWarning(Generic[_W]): def format(self, **kwargs: Any) -> _W: """Return an instance of the warning category, formatted with given kwargs.""" return self.category(self.template.format(**kwargs)) - - -PYTESTER_COPY_EXAMPLE = PytestExperimentalApiWarning.simple("testdir.copy_example") diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index f4b7d6135..c937ce9dc 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -9,7 +9,7 @@ import pytest from _pytest.compat import importlib_metadata from _pytest.config import ExitCode from _pytest.pathlib import symlink_or_skip -from _pytest.pytester import Testdir +from _pytest.pytester import Pytester def prepend_pythonpath(*dirs): @@ -1276,14 +1276,14 @@ def test_tee_stdio_captures_and_live_prints(testdir): sys.platform == "win32", reason="Windows raises `OSError: [Errno 22] Invalid argument` instead", ) -def test_no_brokenpipeerror_message(testdir: Testdir) -> None: +def test_no_brokenpipeerror_message(pytester: Pytester) -> None: """Ensure that the broken pipe error message is supressed. In some Python versions, it reaches sys.unraisablehook, in others a BrokenPipeError exception is propagated, but either way it prints to stderr on shutdown, so checking nothing is printed is enough. """ - popen = testdir.popen((*testdir._getpytestargs(), "--help")) + popen = pytester.popen((*pytester._getpytestargs(), "--help")) popen.stdout.close() ret = popen.wait() assert popen.stderr.read() == b"" diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 0e8bba980..67e93b76a 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -3,6 +3,8 @@ import textwrap from typing import Callable from typing import Optional +import py + import pytest from _pytest.doctest import _get_checker from _pytest.doctest import _is_mocked @@ -11,12 +13,13 @@ from _pytest.doctest import _patch_unwrap_mock_aware from _pytest.doctest import DoctestItem from _pytest.doctest import DoctestModule from _pytest.doctest import DoctestTextfile +from _pytest.pytester import Pytester class TestDoctests: - def test_collect_testtextfile(self, testdir): - w = testdir.maketxtfile(whatever="") - checkfile = testdir.maketxtfile( + def test_collect_testtextfile(self, pytester: Pytester): + w = pytester.maketxtfile(whatever="") + checkfile = pytester.maketxtfile( test_something=""" alskdjalsdk >>> i = 5 @@ -25,48 +28,48 @@ class TestDoctests: """ ) - for x in (testdir.tmpdir, checkfile): + for x in (pytester.path, checkfile): # print "checking that %s returns custom items" % (x,) - items, reprec = testdir.inline_genitems(x) + items, reprec = pytester.inline_genitems(x) assert len(items) == 1 assert isinstance(items[0], DoctestItem) assert isinstance(items[0].parent, DoctestTextfile) # Empty file has no items. - items, reprec = testdir.inline_genitems(w) + items, reprec = pytester.inline_genitems(w) assert len(items) == 0 - def test_collect_module_empty(self, testdir): - path = testdir.makepyfile(whatever="#") - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + def test_collect_module_empty(self, pytester: Pytester): + path = pytester.makepyfile(whatever="#") + for p in (path, pytester.path): + items, reprec = pytester.inline_genitems(p, "--doctest-modules") assert len(items) == 0 - def test_collect_module_single_modulelevel_doctest(self, testdir): - path = testdir.makepyfile(whatever='""">>> pass"""') - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + def test_collect_module_single_modulelevel_doctest(self, pytester: Pytester): + path = pytester.makepyfile(whatever='""">>> pass"""') + for p in (path, pytester.path): + items, reprec = pytester.inline_genitems(p, "--doctest-modules") assert len(items) == 1 assert isinstance(items[0], DoctestItem) assert isinstance(items[0].parent, DoctestModule) - def test_collect_module_two_doctest_one_modulelevel(self, testdir): - path = testdir.makepyfile( + def test_collect_module_two_doctest_one_modulelevel(self, pytester: Pytester): + path = pytester.makepyfile( whatever=""" '>>> x = None' def my_func(): ">>> magic = 42 " """ ) - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + for p in (path, pytester.path): + items, reprec = pytester.inline_genitems(p, "--doctest-modules") assert len(items) == 2 assert isinstance(items[0], DoctestItem) assert isinstance(items[1], DoctestItem) assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - def test_collect_module_two_doctest_no_modulelevel(self, testdir): - path = testdir.makepyfile( + def test_collect_module_two_doctest_no_modulelevel(self, pytester: Pytester): + path = pytester.makepyfile( whatever=""" '# Empty' def my_func(): @@ -83,72 +86,72 @@ class TestDoctests: ''' """ ) - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + for p in (path, pytester.path): + items, reprec = pytester.inline_genitems(p, "--doctest-modules") assert len(items) == 2 assert isinstance(items[0], DoctestItem) assert isinstance(items[1], DoctestItem) assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - def test_simple_doctestfile(self, testdir): - p = testdir.maketxtfile( + def test_simple_doctestfile(self, pytester: Pytester): + p = pytester.maketxtfile( test_doc=""" >>> x = 1 >>> x == 1 False """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(failed=1) - def test_new_pattern(self, testdir): - p = testdir.maketxtfile( + def test_new_pattern(self, pytester: Pytester): + p = pytester.maketxtfile( xdoc=""" >>> x = 1 >>> x == 1 False """ ) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec = pytester.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(failed=1) - def test_multiple_patterns(self, testdir): + def test_multiple_patterns(self, pytester: Pytester): """Test support for multiple --doctest-glob arguments (#1255).""" - testdir.maketxtfile( + pytester.maketxtfile( xdoc=""" >>> 1 1 """ ) - testdir.makefile( + pytester.makefile( ".foo", test=""" >>> 1 1 """, ) - testdir.maketxtfile( + pytester.maketxtfile( test_normal=""" >>> 1 1 """ ) expected = {"xdoc.txt", "test.foo", "test_normal.txt"} - assert {x.basename for x in testdir.tmpdir.listdir()} == expected + assert {x.name for x in pytester.path.iterdir()} == expected args = ["--doctest-glob=xdoc*.txt", "--doctest-glob=*.foo"] - result = testdir.runpytest(*args) + result = pytester.runpytest(*args) result.stdout.fnmatch_lines(["*test.foo *", "*xdoc.txt *", "*2 passed*"]) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*test_normal.txt *", "*1 passed*"]) @pytest.mark.parametrize( " test_string, encoding", [("foo", "ascii"), ("öäü", "latin1"), ("öäü", "utf-8")], ) - def test_encoding(self, testdir, test_string, encoding): + def test_encoding(self, pytester, test_string, encoding): """Test support for doctest_encoding ini option.""" - testdir.makeini( + pytester.makeini( """ [pytest] doctest_encoding={} @@ -162,21 +165,22 @@ class TestDoctests: """.format( test_string, repr(test_string) ) - testdir._makefile(".txt", [doctest], {}, encoding=encoding) + fn = pytester.path / "test_encoding.txt" + fn.write_text(doctest, encoding=encoding) - result = testdir.runpytest() + result = pytester.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - def test_doctest_unexpected_exception(self, testdir): - testdir.maketxtfile( + def test_doctest_unexpected_exception(self, pytester: Pytester): + pytester.maketxtfile( """ >>> i = 0 >>> 0 / i 2 """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "test_doctest_unexpected_exception.txt F *", @@ -196,8 +200,8 @@ class TestDoctests: consecutive=True, ) - def test_doctest_outcomes(self, testdir): - testdir.maketxtfile( + def test_doctest_outcomes(self, pytester: Pytester): + pytester.maketxtfile( test_skip=""" >>> 1 1 @@ -219,7 +223,7 @@ class TestDoctests: bar """, ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "collected 3 items", @@ -232,11 +236,11 @@ class TestDoctests: ] ) - def test_docstring_partial_context_around_error(self, testdir): + def test_docstring_partial_context_around_error(self, pytester: Pytester): """Test that we show some context before the actual line of a failing doctest. """ - testdir.makepyfile( + pytester.makepyfile( ''' def foo(): """ @@ -258,7 +262,7 @@ class TestDoctests: """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*docstring_partial_context_around_error*", @@ -276,11 +280,11 @@ class TestDoctests: result.stdout.no_fnmatch_line("*text-line-2*") result.stdout.no_fnmatch_line("*text-line-after*") - def test_docstring_full_context_around_error(self, testdir): + def test_docstring_full_context_around_error(self, pytester: Pytester): """Test that we show the whole context before the actual line of a failing doctest, provided that the context is up to 10 lines long. """ - testdir.makepyfile( + pytester.makepyfile( ''' def foo(): """ @@ -292,7 +296,7 @@ class TestDoctests: """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*docstring_full_context_around_error*", @@ -306,8 +310,8 @@ class TestDoctests: ] ) - def test_doctest_linedata_missing(self, testdir): - testdir.tmpdir.join("hello.py").write( + def test_doctest_linedata_missing(self, pytester: Pytester): + pytester.path.joinpath("hello.py").write_text( textwrap.dedent( """\ class Fun(object): @@ -320,13 +324,13 @@ class TestDoctests: """ ) ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( ["*hello*", "006*>>> 1/0*", "*UNEXPECTED*ZeroDivision*", "*1 failed*"] ) - def test_doctest_linedata_on_property(self, testdir): - testdir.makepyfile( + def test_doctest_linedata_on_property(self, pytester: Pytester): + pytester.makepyfile( """ class Sample(object): @property @@ -338,7 +342,7 @@ class TestDoctests: return 'something' """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*= FAILURES =*", @@ -355,8 +359,8 @@ class TestDoctests: ] ) - def test_doctest_no_linedata_on_overriden_property(self, testdir): - testdir.makepyfile( + def test_doctest_no_linedata_on_overriden_property(self, pytester: Pytester): + pytester.makepyfile( """ class Sample(object): @property @@ -369,7 +373,7 @@ class TestDoctests: some_property = property(some_property.__get__, None, None, some_property.__doc__) """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*= FAILURES =*", @@ -386,14 +390,14 @@ class TestDoctests: ] ) - def test_doctest_unex_importerror_only_txt(self, testdir): - testdir.maketxtfile( + def test_doctest_unex_importerror_only_txt(self, pytester: Pytester): + pytester.maketxtfile( """ >>> import asdalsdkjaslkdjasd >>> """ ) - result = testdir.runpytest() + result = pytester.runpytest() # doctest is never executed because of error during hello.py collection result.stdout.fnmatch_lines( [ @@ -403,21 +407,21 @@ class TestDoctests: ] ) - def test_doctest_unex_importerror_with_module(self, testdir): - testdir.tmpdir.join("hello.py").write( + def test_doctest_unex_importerror_with_module(self, pytester: Pytester): + pytester.path.joinpath("hello.py").write_text( textwrap.dedent( """\ import asdalsdkjaslkdjasd """ ) ) - testdir.maketxtfile( + pytester.maketxtfile( """ >>> import hello >>> """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") # doctest is never executed because of error during hello.py collection result.stdout.fnmatch_lines( [ @@ -427,8 +431,8 @@ class TestDoctests: ] ) - def test_doctestmodule(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule(self, pytester: Pytester): + p = pytester.makepyfile( """ ''' >>> x = 1 @@ -438,12 +442,12 @@ class TestDoctests: ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1) - def test_doctestmodule_external_and_issue116(self, testdir): - p = testdir.mkpydir("hello") - p.join("__init__.py").write( + def test_doctestmodule_external_and_issue116(self, pytester: Pytester): + p = pytester.mkpydir("hello") + p.joinpath("__init__.py").write_text( textwrap.dedent( """\ def somefunc(): @@ -455,7 +459,7 @@ class TestDoctests: """ ) ) - result = testdir.runpytest(p, "--doctest-modules") + result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines( [ "003 *>>> i = 0", @@ -468,15 +472,15 @@ class TestDoctests: ] ) - def test_txtfile_failing(self, testdir): - p = testdir.maketxtfile( + def test_txtfile_failing(self, pytester: Pytester): + p = pytester.maketxtfile( """ >>> i = 0 >>> i + 1 2 """ ) - result = testdir.runpytest(p, "-s") + result = pytester.runpytest(p, "-s") result.stdout.fnmatch_lines( [ "001 >>> i = 0", @@ -489,25 +493,25 @@ class TestDoctests: ] ) - def test_txtfile_with_fixtures(self, testdir): - p = testdir.maketxtfile( + def test_txtfile_with_fixtures(self, pytester: Pytester): + p = pytester.maketxtfile( """ - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(passed=1) - def test_txtfile_with_usefixtures_in_ini(self, testdir): - testdir.makeini( + def test_txtfile_with_usefixtures_in_ini(self, pytester: Pytester): + pytester.makeini( """ [pytest] usefixtures = myfixture """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest @pytest.fixture @@ -516,36 +520,36 @@ class TestDoctests: """ ) - p = testdir.maketxtfile( + p = pytester.maketxtfile( """ >>> import os >>> os.environ["HELLO"] 'WORLD' """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(passed=1) - def test_doctestmodule_with_fixtures(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule_with_fixtures(self, pytester: Pytester): + p = pytester.makepyfile( """ ''' - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) - def test_doctestmodule_three_tests(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule_three_tests(self, pytester: Pytester): + p = pytester.makepyfile( """ ''' - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True ''' def my_func(): ''' @@ -563,11 +567,11 @@ class TestDoctests: ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=3) - def test_doctestmodule_two_tests_one_fail(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule_two_tests_one_fail(self, pytester: Pytester): + p = pytester.makepyfile( """ class MyClass(object): def bad_meth(self): @@ -584,17 +588,17 @@ class TestDoctests: ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1, passed=1) - def test_ignored_whitespace(self, testdir): - testdir.makeini( + def test_ignored_whitespace(self, pytester: Pytester): + pytester.makeini( """ [pytest] doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ class MyClass(object): ''' @@ -605,17 +609,17 @@ class TestDoctests: pass """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) - def test_non_ignored_whitespace(self, testdir): - testdir.makeini( + def test_non_ignored_whitespace(self, pytester: Pytester): + pytester.makeini( """ [pytest] doctest_optionflags = ELLIPSIS """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ class MyClass(object): ''' @@ -626,46 +630,46 @@ class TestDoctests: pass """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1, passed=0) - def test_ignored_whitespace_glob(self, testdir): - testdir.makeini( + def test_ignored_whitespace_glob(self, pytester: Pytester): + pytester.makeini( """ [pytest] doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE """ ) - p = testdir.maketxtfile( + p = pytester.maketxtfile( xdoc=""" >>> a = "foo " >>> print(a) foo """ ) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec = pytester.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(passed=1) - def test_non_ignored_whitespace_glob(self, testdir): - testdir.makeini( + def test_non_ignored_whitespace_glob(self, pytester: Pytester): + pytester.makeini( """ [pytest] doctest_optionflags = ELLIPSIS """ ) - p = testdir.maketxtfile( + p = pytester.maketxtfile( xdoc=""" >>> a = "foo " >>> print(a) foo """ ) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec = pytester.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(failed=1, passed=0) - def test_contains_unicode(self, testdir): + def test_contains_unicode(self, pytester: Pytester): """Fix internal error with docstrings containing non-ascii characters.""" - testdir.makepyfile( + pytester.makepyfile( '''\ def foo(): """ @@ -674,11 +678,11 @@ class TestDoctests: """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["Got nothing", "* 1 failed in*"]) - def test_ignore_import_errors_on_doctest(self, testdir): - p = testdir.makepyfile( + def test_ignore_import_errors_on_doctest(self, pytester: Pytester): + p = pytester.makepyfile( """ import asdf @@ -691,14 +695,14 @@ class TestDoctests: """ ) - reprec = testdir.inline_run( + reprec = pytester.inline_run( p, "--doctest-modules", "--doctest-ignore-import-errors" ) reprec.assertoutcome(skipped=1, failed=1, passed=0) - def test_junit_report_for_doctest(self, testdir): + def test_junit_report_for_doctest(self, pytester: Pytester): """#713: Fix --junit-xml option when used with --doctest-modules.""" - p = testdir.makepyfile( + p = pytester.makepyfile( """ def foo(): ''' @@ -708,15 +712,15 @@ class TestDoctests: pass """ ) - reprec = testdir.inline_run(p, "--doctest-modules", "--junit-xml=junit.xml") + reprec = pytester.inline_run(p, "--doctest-modules", "--junit-xml=junit.xml") reprec.assertoutcome(failed=1) - def test_unicode_doctest(self, testdir): + def test_unicode_doctest(self, pytester: Pytester): """ Test case for issue 2434: DecodeError on Python 2 when doctest contains non-ascii characters. """ - p = testdir.maketxtfile( + p = pytester.maketxtfile( test_unicode_doctest=""" .. doctest:: @@ -729,17 +733,17 @@ class TestDoctests: 1 """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines( ["*UNEXPECTED EXCEPTION: ZeroDivisionError*", "*1 failed*"] ) - def test_unicode_doctest_module(self, testdir): + def test_unicode_doctest_module(self, pytester: Pytester): """ Test case for issue 2434: DecodeError on Python 2 when doctest docstring contains non-ascii characters. """ - p = testdir.makepyfile( + p = pytester.makepyfile( test_unicode_doctest_module=""" def fix_bad_unicode(text): ''' @@ -749,15 +753,15 @@ class TestDoctests: return "único" """ ) - result = testdir.runpytest(p, "--doctest-modules") + result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) - def test_print_unicode_value(self, testdir): + def test_print_unicode_value(self, pytester: Pytester): """ Test case for issue 3583: Printing Unicode in doctest under Python 2.7 doesn't work """ - p = testdir.maketxtfile( + p = pytester.maketxtfile( test_print_unicode_value=r""" Here is a doctest:: @@ -765,12 +769,12 @@ class TestDoctests: åéîøü """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["* 1 passed *"]) - def test_reportinfo(self, testdir): + def test_reportinfo(self, pytester: Pytester): """Make sure that DoctestItem.reportinfo() returns lineno.""" - p = testdir.makepyfile( + p = pytester.makepyfile( test_reportinfo=""" def foo(x): ''' @@ -780,16 +784,16 @@ class TestDoctests: return 'c' """ ) - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + items, reprec = pytester.inline_genitems(p, "--doctest-modules") reportinfo = items[0].reportinfo() assert reportinfo[1] == 1 - def test_valid_setup_py(self, testdir): + def test_valid_setup_py(self, pytester: Pytester): """ Test to make sure that pytest ignores valid setup.py files when ran with --doctest-modules """ - p = testdir.makepyfile( + p = pytester.makepyfile( setup=""" from setuptools import setup, find_packages setup(name='sample', @@ -799,33 +803,33 @@ class TestDoctests: ) """ ) - result = testdir.runpytest(p, "--doctest-modules") + result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["*collected 0 items*"]) - def test_invalid_setup_py(self, testdir): + def test_invalid_setup_py(self, pytester: Pytester): """ Test to make sure that pytest reads setup.py files that are not used for python packages when ran with --doctest-modules """ - p = testdir.makepyfile( + p = pytester.makepyfile( setup=""" def test_foo(): return 'bar' """ ) - result = testdir.runpytest(p, "--doctest-modules") + result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["*collected 1 item*"]) class TestLiterals: @pytest.mark.parametrize("config_mode", ["ini", "comment"]) - def test_allow_unicode(self, testdir, config_mode): + def test_allow_unicode(self, pytester, config_mode): """Test that doctests which output unicode work in all python versions tested by pytest when the ALLOW_UNICODE option is used (either in the ini file or by an inline comment). """ if config_mode == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] doctest_optionflags = ALLOW_UNICODE @@ -835,7 +839,7 @@ class TestLiterals: else: comment = "#doctest: +ALLOW_UNICODE" - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'12'.decode('ascii') {comment} '12' @@ -843,7 +847,7 @@ class TestLiterals: comment=comment ) ) - testdir.makepyfile( + pytester.makepyfile( foo=""" def foo(): ''' @@ -854,17 +858,17 @@ class TestLiterals: comment=comment ) ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=2) @pytest.mark.parametrize("config_mode", ["ini", "comment"]) - def test_allow_bytes(self, testdir, config_mode): + def test_allow_bytes(self, pytester, config_mode): """Test that doctests which output bytes work in all python versions tested by pytest when the ALLOW_BYTES option is used (either in the ini file or by an inline comment)(#1287). """ if config_mode == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] doctest_optionflags = ALLOW_BYTES @@ -874,7 +878,7 @@ class TestLiterals: else: comment = "#doctest: +ALLOW_BYTES" - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'foo' {comment} 'foo' @@ -882,7 +886,7 @@ class TestLiterals: comment=comment ) ) - testdir.makepyfile( + pytester.makepyfile( foo=""" def foo(): ''' @@ -893,34 +897,34 @@ class TestLiterals: comment=comment ) ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=2) - def test_unicode_string(self, testdir): + def test_unicode_string(self, pytester: Pytester): """Test that doctests which output unicode fail in Python 2 when the ALLOW_UNICODE option is not used. The same test should pass in Python 3. """ - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'12'.decode('ascii') '12' """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_bytes_literal(self, testdir): + def test_bytes_literal(self, pytester: Pytester): """Test that doctests which output bytes fail in Python 3 when the ALLOW_BYTES option is not used. (#1287). """ - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'foo' 'foo' """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(failed=1) def test_number_re(self) -> None: @@ -954,10 +958,10 @@ class TestLiterals: assert _number_re.match(s) is None @pytest.mark.parametrize("config_mode", ["ini", "comment"]) - def test_number_precision(self, testdir, config_mode): + def test_number_precision(self, pytester, config_mode): """Test the NUMBER option.""" if config_mode == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] doctest_optionflags = NUMBER @@ -967,7 +971,7 @@ class TestLiterals: else: comment = "#doctest: +NUMBER" - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" Scalars: @@ -1024,7 +1028,7 @@ class TestLiterals: comment=comment ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @pytest.mark.parametrize( @@ -1048,8 +1052,8 @@ class TestLiterals: pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), # type: ignore ], ) - def test_number_non_matches(self, testdir, expression, output): - testdir.maketxtfile( + def test_number_non_matches(self, pytester, expression, output): + pytester.maketxtfile( test_doc=""" >>> {expression} #doctest: +NUMBER {output} @@ -1057,11 +1061,11 @@ class TestLiterals: expression=expression, output=output ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=0, failed=1) - def test_number_and_allow_unicode(self, testdir): - testdir.maketxtfile( + def test_number_and_allow_unicode(self, pytester: Pytester): + pytester.maketxtfile( test_doc=""" >>> from collections import namedtuple >>> T = namedtuple('T', 'a b c') @@ -1069,7 +1073,7 @@ class TestLiterals: T(a=0.233, b=u'str', c='bytes') """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -1080,18 +1084,18 @@ class TestDoctestSkips: """ @pytest.fixture(params=["text", "module"]) - def makedoctest(self, testdir, request): + def makedoctest(self, pytester, request): def makeit(doctest): mode = request.param if mode == "text": - testdir.maketxtfile(doctest) + pytester.maketxtfile(doctest) else: assert mode == "module" - testdir.makepyfile('"""\n%s"""' % doctest) + pytester.makepyfile('"""\n%s"""' % doctest) return makeit - def test_one_skipped(self, testdir, makedoctest): + def test_one_skipped(self, pytester, makedoctest): makedoctest( """ >>> 1 + 1 # doctest: +SKIP @@ -1100,10 +1104,10 @@ class TestDoctestSkips: 4 """ ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=1) - def test_one_skipped_failed(self, testdir, makedoctest): + def test_one_skipped_failed(self, pytester, makedoctest): makedoctest( """ >>> 1 + 1 # doctest: +SKIP @@ -1112,10 +1116,10 @@ class TestDoctestSkips: 200 """ ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(failed=1) - def test_all_skipped(self, testdir, makedoctest): + def test_all_skipped(self, pytester, makedoctest): makedoctest( """ >>> 1 + 1 # doctest: +SKIP @@ -1124,16 +1128,16 @@ class TestDoctestSkips: 200 """ ) - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(skipped=1) - def test_vacuous_all_skipped(self, testdir, makedoctest): + def test_vacuous_all_skipped(self, pytester, makedoctest): makedoctest("") - reprec = testdir.inline_run("--doctest-modules") + reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=0, skipped=0) - def test_continue_on_failure(self, testdir): - testdir.maketxtfile( + def test_continue_on_failure(self, pytester: Pytester): + pytester.maketxtfile( test_something=""" >>> i = 5 >>> def foo(): @@ -1145,7 +1149,9 @@ class TestDoctestSkips: >>> i + 1 """ ) - result = testdir.runpytest("--doctest-modules", "--doctest-continue-on-failure") + result = pytester.runpytest( + "--doctest-modules", "--doctest-continue-on-failure" + ) result.assert_outcomes(passed=0, failed=1) # The lines that contains the failure are 4, 5, and 8. The first one # is a stack trace and the other two are mismatches. @@ -1158,11 +1164,11 @@ class TestDoctestAutoUseFixtures: SCOPES = ["module", "session", "class", "function"] - def test_doctest_module_session_fixture(self, testdir): + def test_doctest_module_session_fixture(self, pytester: Pytester): """Test that session fixtures are initialized for doctest modules (#768).""" # session fixture which changes some global data, which will # be accessed by doctests in a module - testdir.makeconftest( + pytester.makeconftest( """ import pytest import sys @@ -1175,7 +1181,7 @@ class TestDoctestAutoUseFixtures: del sys.pytest_session_data """ ) - testdir.makepyfile( + pytester.makepyfile( foo=""" import sys @@ -1190,16 +1196,16 @@ class TestDoctestAutoUseFixtures: ''' """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["*2 passed*"]) @pytest.mark.parametrize("scope", SCOPES) @pytest.mark.parametrize("enable_doctest", [True, False]) - def test_fixture_scopes(self, testdir, scope, enable_doctest): + def test_fixture_scopes(self, pytester, scope, enable_doctest): """Test that auto-use fixtures work properly with doctest modules. See #1057 and #1100. """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -1210,7 +1216,7 @@ class TestDoctestAutoUseFixtures: scope=scope ) ) - testdir.makepyfile( + pytester.makepyfile( test_1=''' def test_foo(): """ @@ -1223,19 +1229,19 @@ class TestDoctestAutoUseFixtures: ) params = ("--doctest-modules",) if enable_doctest else () passes = 3 if enable_doctest else 2 - result = testdir.runpytest(*params) + result = pytester.runpytest(*params) result.stdout.fnmatch_lines(["*=== %d passed in *" % passes]) @pytest.mark.parametrize("scope", SCOPES) @pytest.mark.parametrize("autouse", [True, False]) @pytest.mark.parametrize("use_fixture_in_doctest", [True, False]) def test_fixture_module_doctest_scopes( - self, testdir, scope, autouse, use_fixture_in_doctest + self, pytester, scope, autouse, use_fixture_in_doctest ): """Test that auto-use fixtures work properly with doctest files. See #1057 and #1100. """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -1247,29 +1253,29 @@ class TestDoctestAutoUseFixtures: ) ) if use_fixture_in_doctest: - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> getfixture('auto') 99 """ ) else: - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> 1 + 1 2 """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.no_fnmatch_line("*FAILURES*") result.stdout.fnmatch_lines(["*=== 1 passed in *"]) @pytest.mark.parametrize("scope", SCOPES) - def test_auto_use_request_attributes(self, testdir, scope): + def test_auto_use_request_attributes(self, pytester, scope): """Check that all attributes of a request in an autouse fixture behave as expected when requested for a doctest item. """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest @@ -1286,13 +1292,13 @@ class TestDoctestAutoUseFixtures: scope=scope ) ) - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> 1 + 1 2 """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") str(result.stdout.no_fnmatch_line("*FAILURES*")) result.stdout.fnmatch_lines(["*=== 1 passed in *"]) @@ -1302,12 +1308,12 @@ class TestDoctestNamespaceFixture: SCOPES = ["module", "session", "class", "function"] @pytest.mark.parametrize("scope", SCOPES) - def test_namespace_doctestfile(self, testdir, scope): + def test_namespace_doctestfile(self, pytester, scope): """ Check that inserting something into the namespace works in a simple text file doctest """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest import contextlib @@ -1319,22 +1325,22 @@ class TestDoctestNamespaceFixture: scope=scope ) ) - p = testdir.maketxtfile( + p = pytester.maketxtfile( """ >>> print(cl.__name__) contextlib """ ) - reprec = testdir.inline_run(p) + reprec = pytester.inline_run(p) reprec.assertoutcome(passed=1) @pytest.mark.parametrize("scope", SCOPES) - def test_namespace_pyfile(self, testdir, scope): + def test_namespace_pyfile(self, pytester, scope): """ Check that inserting something into the namespace works in a simple Python file docstring doctest """ - testdir.makeconftest( + pytester.makeconftest( """ import pytest import contextlib @@ -1346,7 +1352,7 @@ class TestDoctestNamespaceFixture: scope=scope ) ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ def foo(): ''' @@ -1355,13 +1361,13 @@ class TestDoctestNamespaceFixture: ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) class TestDoctestReportingOption: - def _run_doctest_report(self, testdir, format): - testdir.makepyfile( + def _run_doctest_report(self, pytester, format): + pytester.makepyfile( """ def foo(): ''' @@ -1377,17 +1383,17 @@ class TestDoctestReportingOption: '2 3 6') """ ) - return testdir.runpytest("--doctest-modules", "--doctest-report", format) + return pytester.runpytest("--doctest-modules", "--doctest-report", format) @pytest.mark.parametrize("format", ["udiff", "UDIFF", "uDiFf"]) - def test_doctest_report_udiff(self, testdir, format): - result = self._run_doctest_report(testdir, format) + def test_doctest_report_udiff(self, pytester, format): + result = self._run_doctest_report(pytester, format) result.stdout.fnmatch_lines( [" 0 1 4", " -1 2 4", " +1 2 5", " 2 3 6"] ) - def test_doctest_report_cdiff(self, testdir): - result = self._run_doctest_report(testdir, "cdiff") + def test_doctest_report_cdiff(self, pytester: Pytester): + result = self._run_doctest_report(pytester, "cdiff") result.stdout.fnmatch_lines( [ " a b", @@ -1402,8 +1408,8 @@ class TestDoctestReportingOption: ] ) - def test_doctest_report_ndiff(self, testdir): - result = self._run_doctest_report(testdir, "ndiff") + def test_doctest_report_ndiff(self, pytester: Pytester): + result = self._run_doctest_report(pytester, "ndiff") result.stdout.fnmatch_lines( [ " a b", @@ -1417,8 +1423,8 @@ class TestDoctestReportingOption: ) @pytest.mark.parametrize("format", ["none", "only_first_failure"]) - def test_doctest_report_none_or_only_first_failure(self, testdir, format): - result = self._run_doctest_report(testdir, format) + def test_doctest_report_none_or_only_first_failure(self, pytester, format): + result = self._run_doctest_report(pytester, format) result.stdout.fnmatch_lines( [ "Expected:", @@ -1434,8 +1440,8 @@ class TestDoctestReportingOption: ] ) - def test_doctest_report_invalid(self, testdir): - result = self._run_doctest_report(testdir, "obviously_invalid_format") + def test_doctest_report_invalid(self, pytester: Pytester): + result = self._run_doctest_report(pytester, "obviously_invalid_format") result.stderr.fnmatch_lines( [ "*error: argument --doctest-report: invalid choice: 'obviously_invalid_format' (choose from*" @@ -1444,9 +1450,9 @@ class TestDoctestReportingOption: @pytest.mark.parametrize("mock_module", ["mock", "unittest.mock"]) -def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, testdir): +def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, pytester: Pytester): pytest.importorskip(mock_module) - testdir.makepyfile( + pytester.makepyfile( """ from {mock_module} import call class Example(object): @@ -1458,7 +1464,7 @@ def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, testdir): mock_module=mock_module ) ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) @@ -1485,10 +1491,10 @@ def test_warning_on_unwrap_of_broken_object( assert inspect.unwrap.__module__ == "inspect" -def test_is_setup_py_not_named_setup_py(tmpdir): - not_setup_py = tmpdir.join("not_setup.py") - not_setup_py.write('from setuptools import setup; setup(name="foo")') - assert not _is_setup_py(not_setup_py) +def test_is_setup_py_not_named_setup_py(tmp_path): + not_setup_py = tmp_path.joinpath("not_setup.py") + not_setup_py.write_text('from setuptools import setup; setup(name="foo")') + assert not _is_setup_py(py.path.local(str(not_setup_py))) @pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) @@ -1499,11 +1505,11 @@ def test_is_setup_py_is_a_setup_py(tmpdir, mod): @pytest.mark.parametrize("mod", ("setuptools", "distutils.core")) -def test_is_setup_py_different_encoding(tmpdir, mod): - setup_py = tmpdir.join("setup.py") +def test_is_setup_py_different_encoding(tmp_path, mod): + setup_py = tmp_path.joinpath("setup.py") contents = ( "# -*- coding: cp1252 -*-\n" 'from {} import setup; setup(name="foo", description="€")\n'.format(mod) ) - setup_py.write_binary(contents.encode("cp1252")) - assert _is_setup_py(setup_py) + setup_py.write_bytes(contents.encode("cp1252")) + assert _is_setup_py(py.path.local(str(setup_py))) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index dd3855c69..fed201daf 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -801,9 +801,10 @@ def test_parse_summary_line_always_plural(): def test_makefile_joins_absolute_path(testdir: Testdir) -> None: absfile = testdir.tmpdir / "absfile" - if sys.platform == "win32": - with pytest.raises(OSError): - testdir.makepyfile(**{str(absfile): ""}) - else: - p1 = testdir.makepyfile(**{str(absfile): ""}) - assert str(p1) == (testdir.tmpdir / absfile) + ".py" + p1 = testdir.makepyfile(**{str(absfile): ""}) + assert str(p1) == str(testdir.tmpdir / "absfile.py") + + +def test_testtmproot(testdir): + """Check test_tmproot is a py.path attribute for backward compatibility.""" + assert testdir.test_tmproot.check(dir=1)