diff --git a/changelog/7425.feature.rst b/changelog/7425.feature.rst new file mode 100644 index 00000000000..0fce45100b0 --- /dev/null +++ b/changelog/7425.feature.rst @@ -0,0 +1,3 @@ +New :fixture:`test_path` 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. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index d81ba9bc7ea..b2563f900df 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -494,17 +494,21 @@ monkeypatch :members: -.. fixture:: testdir +.. fixture:: test_path -testdir -~~~~~~~ +test_path +~~~~~~~~~ + +.. versionadded:: 6.0 .. 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:`TestPath` instance that can be used to run and test pytest itself. + +It provides an empty directory where pytest can be executed in isolation, and contains facilities +to write test, configuration files, and match against expected output. -To use it, include in your top-most ``conftest.py`` file: +To use it, include in your topmost ``conftest.py`` file: .. code-block:: python @@ -512,7 +516,7 @@ To use it, include in your top-most ``conftest.py`` file: -.. autoclass:: Testdir() +.. autoclass:: TestPath() :members: .. autoclass:: RunResult() @@ -521,6 +525,15 @@ To use it, include in your top-most ``conftest.py`` file: .. autoclass:: LineMatcher() :members: +.. fixture:: testdir + +testdir +~~~~~~~ + +Identical to :fixture:`test_path`, 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:`test_path`. .. fixture:: recwarn diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 45f6f008ab1..974b6ec044e 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1,10 +1,12 @@ """(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 @@ -17,10 +19,12 @@ from typing import List from typing import Optional from typing import Sequence +from typing import TextIO from typing import Tuple from typing import Union from weakref import WeakKeyDictionary +import attr import py from iniconfig import IniConfig @@ -42,7 +46,7 @@ from _pytest.pathlib import Path from _pytest.python import Module from _pytest.reports import TestReport -from _pytest.tmpdir import TempdirFactory +from _pytest.tmpdir import TempPathFactory if TYPE_CHECKING: from typing import Type @@ -173,11 +177,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 @@ -376,15 +380,29 @@ def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]": @pytest.fixture -def testdir(request: FixtureRequest, tmpdir_factory) -> "Testdir": +def test_path(request: FixtureRequest, tmp_path_factory) -> "TestPath": """ - A :class: `TestDir` instance, that can be used to run and test pytest itself. + Provides a :class:`TestPath` 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. + It provides an empty folder (accessible through the ``tmp_path`` attribute) where pytest + can be executed in isolation, and contains facilities to write tests, configuration files, + and match against expected output. + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to ``tmp_path`` and environment variables during initialization. """ - return Testdir(request, tmpdir_factory) + return TestPath(request, tmp_path_factory) + + +@pytest.fixture +def testdir(test_path: "TestPath") -> "Testdir": + """ + Identical to :fixture:`test_path`, 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:`test_path`. + """ + return Testdir(test_path) @pytest.fixture @@ -547,22 +565,24 @@ def restore(self) -> None: sys.path[:], sys.meta_path[:] = self.__saved -class Testdir: +class TestPath: """Temporary test directory with tools to test/run pytest itself. - This is based on the ``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 provides an empty folder (accessible through the ``tmp_path`` attribute) where pytest + can be executed in isolation, and contains facilities to write tests, configuration files, + and match against expected output. + + It attempts to isolate the test run from external factors as much as possible, modifying + the current working directory to ``tmp_path`` and environment variables during initialization. Attributes: - :ivar tmpdir: The :py:class:`py.path.local` instance of the temporary directory. + :ivar Path tmp_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 :py:meth:`runpytest`. Initially this is an empty list but plugins can be added to the list. The type of items to add to the list depends on the method using them so refer to them for details. - """ __test__ = False @@ -572,8 +592,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() ) # type: WeakKeyDictionary[Module, List[Union[Item, Collector]]] @@ -582,36 +604,36 @@ def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> N 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._tmp_path = tmp_path_factory.mktemp(name, numbered=True) # type: Path self.plugins = [] # type: 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") - mp = self.monkeypatch = MonkeyPatch() - mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self.test_tmproot)) + self._monkeypatch = mp = MonkeyPatch() + mp.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path_factory.mktemp("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.tmp_path) mp.setenv("HOME", tmphome) mp.setenv("USERPROFILE", tmphome) # Do not use colors for inner runs by default. mp.setenv("PY_COLORS", "0") - def __repr__(self): - return "".format(self.tmpdir) + @property + def tmp_path(self) -> Path: + return self._tmp_path - def __str__(self): - return str(self.tmpdir) + def __repr__(self): + return "".format(self.tmp_path) - def finalize(self): + def _finalize(self): """Clean up global state artifacts. Some methods modify the global interpreter state and this tries to @@ -622,7 +644,7 @@ def finalize(self): 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): # some zope modules used by twisted-related tests keep internal state @@ -633,21 +655,21 @@ def preserve_module(name): return SysModulesSnapshot(preserve=preserve_module) - def make_hook_recorder(self, pluginmanager): + def make_hook_recorder(self, pluginmanager) -> 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): + def chdir(self) -> None: """Cd into the temporary directory. This is done automatically upon instantiation. """ - self.tmpdir.chdir() + os.chdir(str(self.tmp_path)) - def _makefile(self, ext, lines, files, encoding="utf-8"): + def _makefile(self, ext, lines, files, encoding="utf-8") -> Path: items = list(files.items()) def to_text(s): @@ -660,17 +682,18 @@ def to_text(s): ret = None for basename, value in items: - p = self.tmpdir.join(basename).new(ext=ext) - p.dirpath().ensure_dir() + p = self.tmp_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, *args, **kwargs): - r"""Create new file(s) in the testdir. + def makefile(self, ext, *args, **kwargs) -> 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`. :param list[str] args: All args will be treated as strings and joined using newlines. @@ -690,11 +713,11 @@ def makefile(self, ext, *args, **kwargs): """ return self._makefile(ext, args, kwargs) - def makeconftest(self, source): + def makeconftest(self, source) -> Path: """Write a contest.py file with 'source' as contents.""" return self.makepyfile(conftest=source) - def makeini(self, source): + def makeini(self, source) -> Path: """Write a tox.ini file with 'source' as contents.""" return self.makefile(".ini", tox=source) @@ -703,14 +726,14 @@ def getinicfg(self, source): p = self.makeini(source) return IniConfig(p)["pytest"] - def makepyprojecttoml(self, source): + def makepyprojecttoml(self, source) -> 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 existing files. @@ -729,7 +752,7 @@ def test_something(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 existing files. @@ -748,76 +771,82 @@ def test_something(testdir): """ return self._makefile(".txt", args, kwargs) - def syspathinsert(self, path=None): + def syspathinsert(self, path=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.tmp_path - self.monkeypatch.syspath_prepend(str(path)) + self._monkeypatch.syspath_prepend(str(path)) - def mkdir(self, name): + def mkdir(self, name: str) -> Path: """Create a new (sub)directory.""" - return self.tmpdir.mkdir(name) + p = self.tmp_path / name # type: Path + p.mkdir() + return p - def mkpydir(self, name): + def mkpydir(self, name) -> 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.tmp_path / name # type: Path + p.mkdir() + p.joinpath("__init__.py").touch() return p - def copy_example(self, name=None): + def copy_example(self, name=None) -> Path: """Copy file from project's directory into the testdir. :param str name: The name of the file to copy. - :return: path to the copied directory (inside ``self.tmpdir``). + :return: path to the copied directory (inside ``self.tmp_path``). """ import warnings from _pytest.warning_types import PYTESTER_COPY_EXAMPLE 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) + func_name, example_dir.bestrelpath(self._request.config.rootdir) ) ) else: - example_path = example_dir.join(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) + example_path = example_dir.joinpath(name) + + 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(str(example_path)).copy(py.path.local(str(self.tmp_path))) + return self.tmp_path + elif example_path.is_file(): + result = self.tmp_path.joinpath(example_path.name) + shutil.copy(str(example_path), str(result)) return result else: raise LookupError( @@ -826,7 +855,7 @@ def copy_example(self, name=None): Session = Session - def getnode(self, config, arg): + def getnode(self, config: Config, arg) -> Optional[Union[Collector, Item]]: """Return the collection node of a file. :param config: :py:class:`_pytest.config.Config` instance, see @@ -886,7 +915,7 @@ def runitem(self, source): # 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) @@ -966,7 +995,7 @@ def pytest_configure(x, config: Config) -> None: 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: @@ -974,7 +1003,7 @@ def pytest_configure(x, config: Config) -> None: 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 @@ -1047,7 +1076,7 @@ def _ensure_basetemp(self, args): if str(x).startswith("--basetemp"): break else: - args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp")) + args.append("--basetemp=%s" % self.tmp_path.parent.joinpath("basetemp")) return args def parseconfig(self, *args) -> Config: @@ -1062,15 +1091,16 @@ def parseconfig(self, *args) -> Config: 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: @@ -1131,10 +1161,10 @@ def getmodulecol(self, source, configargs=(), withinit=False): """ if isinstance(source, Path): - path = self.tmpdir.join(str(source)) + path = self.tmp_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__="#") @@ -1163,8 +1193,8 @@ def collect_by_name( 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 ): @@ -1204,7 +1234,8 @@ def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: 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`` + and ``py.path.local`` objects being converted to ``str`` automatically. :kwarg timeout: the period in seconds after which to timeout and raise :py:class:`Testdir.TimeoutExpired` :kwarg stdin: optional standard input. Bytes are being send, closing @@ -1218,15 +1249,15 @@ def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: __tracebackhide__ = True cmdargs = tuple( - str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs + str(arg) if isinstance(arg, (py.path.local, Path)) else arg + for arg in cmdargs ) - p1 = self.tmpdir.join("stdout") - p2 = self.tmpdir.join("stderr") + p1 = self.tmp_path.joinpath("stdout") + p2 = self.tmp_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, @@ -1257,23 +1288,16 @@ def handle_timeout(): 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): @@ -1314,7 +1338,7 @@ def runpytest_subprocess(self, *args, timeout=None) -> RunResult: Returns a :py:class:`RunResult`. """ __tracebackhide__ = True - p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-") + p = make_numbered_dir(root=self.tmp_path, prefix="runpytest-") args = ("--basetemp=%s" % p,) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: @@ -1333,7 +1357,8 @@ def spawn_pytest( The pexpect child is returned. """ - basetemp = self.tmpdir.mkdir("temp-pexpect") + basetemp = self.tmp_path / "temp-pexpect" + basetemp.mkdir() invoke = " ".join(map(str, self._getpytestargs())) cmd = "{} --basetemp={} {}".format(invoke, basetemp, string) return self.spawn(cmd, expect_timeout=expect_timeout) @@ -1349,10 +1374,10 @@ def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": 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.tmp_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 @@ -1375,6 +1400,165 @@ def assert_contains_lines(self, lines2: Sequence[str]) -> None: LineMatcher(lines1).fnmatch_lines(lines2) +@attr.s +class Testdir: + """ + Similar to TestPath, but this class works with legacy py.path.local objects instead. + + All methods just forward to an internal :class:`TestPath` instance, converting results + to `py.path.local` objects as necessary. + """ + + __test__ = False + + CLOSE_STDIN = TestPath.CLOSE_STDIN + TimeoutExpired = TestPath.TimeoutExpired + Session = TestPath.Session + + _test_path = attr.ib(type=TestPath) + + @property + def tmpdir(self) -> py.path.local: + """Backward compatibility: returns ``tmp_path`` as a py.path.local instance.""" + return py.path.local(str(self._test_path.tmp_path)) + + @property + def request(self): + return self._test_path._request + + @property + def plugins(self): + return self._test_path.plugins + + @plugins.setter + def plugins(self, plugins): + self._test_path.plugins = plugins + + @property + def monkeypatch(self) -> MonkeyPatch: + return self._test_path._monkeypatch + + def make_hook_recorder(self, pluginmanager) -> HookRecorder: + return self._test_path.make_hook_recorder(pluginmanager) + + def chdir(self) -> None: + return self._test_path.chdir() + + def makefile(self, ext, *args, **kwargs) -> py.path.local: + return py.path.local(str(self._test_path.makefile(ext, *args, **kwargs))) + + def makeconftest(self, source) -> py.path.local: + return py.path.local(str(self._test_path.makeconftest(source))) + + def makeini(self, source) -> py.path.local: + return py.path.local(str(self._test_path.makeini(source))) + + def getinicfg(self, source) -> py.path.local: + return py.path.local(str(self._test_path.getinicfg(source))) + + def makepyprojecttoml(self, source) -> py.path.local: + return py.path.local(str(self._test_path.makepyprojecttoml(source))) + + def makepyfile(self, *args, **kwargs) -> py.path.local: + return py.path.local(str(self._test_path.makepyfile(*args, **kwargs))) + + def maketxtfile(self, *args, **kwargs) -> py.path.local: + return py.path.local(str(self._test_path.maketxtfile(*args, **kwargs))) + + def syspathinsert(self, path=None) -> None: + return self._test_path.syspathinsert(path) + + def mkdir(self, name) -> py.path.local: + return py.path.local(str(self._test_path.mkdir(name))) + + def mkpydir(self, name) -> py.path.local: + return py.path.local(str(self._test_path.mkpydir(name))) + + def copy_example(self, name=None) -> py.path.local: + return py.path.local(str(self._test_path.copy_example(name))) + + def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]: + return self._test_path.getnode(config, arg) + + def getpathnode(self, path): + return self._test_path.getpathnode(path) + + def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]: + return self._test_path.genitems(colitems) + + def runitem(self, source): + return self._test_path.runitem(source) + + def inline_runsource(self, source, *cmdlineargs): + return self._test_path.inline_runsource(source, *cmdlineargs) + + def inline_genitems(self, *args): + return self._test_path.inline_genitems(*args) + + def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False): + return self._test_path.inline_run( + *args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc + ) + + def runpytest_inprocess(self, *args, **kwargs) -> RunResult: + return self._test_path.runpytest_inprocess(*args, **kwargs) + + def runpytest(self, *args, **kwargs) -> RunResult: + return self._test_path.runpytest(*args, **kwargs) + + def parseconfig(self, *args) -> Config: + return self._test_path.parseconfig(*args) + + def parseconfigure(self, *args) -> Config: + return self._test_path.parseconfigure(*args) + + def getitem(self, source, funcname="test_func"): + return self._test_path.getitem(source, funcname) + + def getitems(self, source): + return self._test_path.getitems(source) + + def getmodulecol(self, source, configargs=(), withinit=False): + return self._test_path.getmodulecol( + source, configargs=configargs, withinit=withinit + ) + + def collect_by_name( + self, modcol: Module, name: str + ) -> Optional[Union[Item, Collector]]: + return self._test_path.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._test_path.popen(cmdargs, stdout, stderr, stdin, **kw) + + def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: + return self._test_path.run(*cmdargs, timeout=timeout, stdin=stdin) + + def runpython(self, script) -> RunResult: + return self._test_path.runpython(script) + + def runpython_c(self, command): + return self._test_path.runpython_c(command) + + def runpytest_subprocess(self, *args, timeout=None) -> RunResult: + return self._test_path.runpytest_subprocess(*args, timeout=timeout) + + def spawn_pytest( + self, string: str, expect_timeout: float = 10.0 + ) -> "pexpect.spawn": + return self._test_path.spawn_pytest(string, expect_timeout=expect_timeout) + + def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn": + return self._test_path.spawn(cmd, expect_timeout=expect_timeout) + + class LineMatcher: """Flexible matching of text. diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index e558c7f6781..be63367e5c1 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -9,7 +9,7 @@ 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 TestPath def prepend_pythonpath(*dirs): @@ -1291,14 +1291,14 @@ def test_simple(): 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(test_path: TestPath) -> 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 = test_path.popen((*test_path._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 9ef9417cd7e..76dbc647075 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -3,6 +3,8 @@ from typing import Callable from typing import Optional +import py + import pytest from _pytest.compat import MODULE_NOT_FOUND_ERROR from _pytest.doctest import _get_checker @@ -15,9 +17,9 @@ class TestDoctests: - def test_collect_testtextfile(self, testdir): - w = testdir.maketxtfile(whatever="") - checkfile = testdir.maketxtfile( + def test_collect_testtextfile(self, test_path): + w = test_path.maketxtfile(whatever="") + checkfile = test_path.maketxtfile( test_something=""" alskdjalsdk >>> i = 5 @@ -26,48 +28,48 @@ def test_collect_testtextfile(self, testdir): """ ) - for x in (testdir.tmpdir, checkfile): + for x in (test_path.tmp_path, checkfile): # print "checking that %s returns custom items" % (x,) - items, reprec = testdir.inline_genitems(x) + items, reprec = test_path.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 = test_path.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, test_path): + path = test_path.makepyfile(whatever="#") + for p in (path, test_path.tmp_path): + items, reprec = test_path.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, test_path): + path = test_path.makepyfile(whatever='""">>> pass"""') + for p in (path, test_path.tmp_path): + items, reprec = test_path.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, test_path): + path = test_path.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, test_path.tmp_path): + items, reprec = test_path.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, test_path): + path = test_path.makepyfile( whatever=""" '# Empty' def my_func(): @@ -84,74 +86,74 @@ def another(): ''' """ ) - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + for p in (path, test_path.tmp_path): + items, reprec = test_path.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, test_path): + p = test_path.maketxtfile( test_doc=""" >>> x = 1 >>> x == 1 False """ ) - reprec = testdir.inline_run(p) + reprec = test_path.inline_run(p) reprec.assertoutcome(failed=1) - def test_new_pattern(self, testdir): - p = testdir.maketxtfile( + def test_new_pattern(self, test_path): + p = test_path.maketxtfile( xdoc=""" >>> x = 1 >>> x == 1 False """ ) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec = test_path.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(failed=1) - def test_multiple_patterns(self, testdir): + def test_multiple_patterns(self, test_path): """Test support for multiple --doctest-glob arguments (#1255). """ - testdir.maketxtfile( + test_path.maketxtfile( xdoc=""" >>> 1 1 """ ) - testdir.makefile( + test_path.makefile( ".foo", test=""" >>> 1 1 """, ) - testdir.maketxtfile( + test_path.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 test_path.tmp_path.iterdir()} == expected args = ["--doctest-glob=xdoc*.txt", "--doctest-glob=*.foo"] - result = testdir.runpytest(*args) + result = test_path.runpytest(*args) result.stdout.fnmatch_lines(["*test.foo *", "*xdoc.txt *", "*2 passed*"]) - result = testdir.runpytest() + result = test_path.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, test_path, test_string, encoding): """Test support for doctest_encoding ini option. """ - testdir.makeini( + test_path.makeini( """ [pytest] doctest_encoding={} @@ -165,21 +167,22 @@ def test_encoding(self, testdir, test_string, encoding): """.format( test_string, repr(test_string) ) - testdir._makefile(".txt", [doctest], {}, encoding=encoding) + fn = test_path.tmp_path / "test_encoding.txt" + fn.write_text(doctest, encoding=encoding) - result = testdir.runpytest() + result = test_path.runpytest() result.stdout.fnmatch_lines(["*1 passed*"]) - def test_doctest_unexpected_exception(self, testdir): - testdir.maketxtfile( + def test_doctest_unexpected_exception(self, test_path): + test_path.maketxtfile( """ >>> i = 0 >>> 0 / i 2 """ ) - result = testdir.runpytest("--doctest-modules") + result = test_path.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "test_doctest_unexpected_exception.txt F *", @@ -199,8 +202,8 @@ def test_doctest_unexpected_exception(self, testdir): consecutive=True, ) - def test_doctest_outcomes(self, testdir): - testdir.maketxtfile( + def test_doctest_outcomes(self, test_path): + test_path.maketxtfile( test_skip=""" >>> 1 1 @@ -222,7 +225,7 @@ def test_doctest_outcomes(self, testdir): bar """, ) - result = testdir.runpytest("--doctest-modules") + result = test_path.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "collected 3 items", @@ -235,11 +238,11 @@ def test_doctest_outcomes(self, testdir): ] ) - def test_docstring_partial_context_around_error(self, testdir): + def test_docstring_partial_context_around_error(self, test_path): """Test that we show some context before the actual line of a failing doctest. """ - testdir.makepyfile( + test_path.makepyfile( ''' def foo(): """ @@ -261,7 +264,7 @@ def foo(): """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = test_path.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*docstring_partial_context_around_error*", @@ -279,11 +282,11 @@ def foo(): 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, test_path): """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( + test_path.makepyfile( ''' def foo(): """ @@ -295,7 +298,7 @@ def foo(): """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = test_path.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*docstring_full_context_around_error*", @@ -309,8 +312,8 @@ def foo(): ] ) - def test_doctest_linedata_missing(self, testdir): - testdir.tmpdir.join("hello.py").write( + def test_doctest_linedata_missing(self, test_path): + test_path.tmp_path.joinpath("hello.py").write_text( textwrap.dedent( """\ class Fun(object): @@ -323,13 +326,13 @@ def test(self): """ ) ) - result = testdir.runpytest("--doctest-modules") + result = test_path.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, test_path): + test_path.makepyfile( """ class Sample(object): @property @@ -341,7 +344,7 @@ def some_property(self): return 'something' """ ) - result = testdir.runpytest("--doctest-modules") + result = test_path.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*= FAILURES =*", @@ -358,8 +361,8 @@ def some_property(self): ] ) - def test_doctest_no_linedata_on_overriden_property(self, testdir): - testdir.makepyfile( + def test_doctest_no_linedata_on_overriden_property(self, test_path): + test_path.makepyfile( """ class Sample(object): @property @@ -372,7 +375,7 @@ def some_property(self): some_property = property(some_property.__get__, None, None, some_property.__doc__) """ ) - result = testdir.runpytest("--doctest-modules") + result = test_path.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*= FAILURES =*", @@ -389,14 +392,14 @@ def some_property(self): ] ) - def test_doctest_unex_importerror_only_txt(self, testdir): - testdir.maketxtfile( + def test_doctest_unex_importerror_only_txt(self, test_path): + test_path.maketxtfile( """ >>> import asdalsdkjaslkdjasd >>> """ ) - result = testdir.runpytest() + result = test_path.runpytest() # doctest is never executed because of error during hello.py collection result.stdout.fnmatch_lines( [ @@ -406,21 +409,21 @@ def test_doctest_unex_importerror_only_txt(self, testdir): ] ) - def test_doctest_unex_importerror_with_module(self, testdir): - testdir.tmpdir.join("hello.py").write( + def test_doctest_unex_importerror_with_module(self, test_path): + test_path.tmp_path.joinpath("hello.py").write_text( textwrap.dedent( """\ import asdalsdkjaslkdjasd """ ) ) - testdir.maketxtfile( + test_path.maketxtfile( """ >>> import hello >>> """ ) - result = testdir.runpytest("--doctest-modules") + result = test_path.runpytest("--doctest-modules") # doctest is never executed because of error during hello.py collection result.stdout.fnmatch_lines( [ @@ -430,8 +433,8 @@ def test_doctest_unex_importerror_with_module(self, testdir): ] ) - def test_doctestmodule(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule(self, test_path): + p = test_path.makepyfile( """ ''' >>> x = 1 @@ -441,12 +444,12 @@ def test_doctestmodule(self, testdir): ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = test_path.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, test_path): + p = test_path.mkpydir("hello") + p.joinpath("__init__.py").write_text( textwrap.dedent( """\ def somefunc(): @@ -458,7 +461,7 @@ def somefunc(): """ ) ) - result = testdir.runpytest(p, "--doctest-modules") + result = test_path.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines( [ "003 *>>> i = 0", @@ -471,15 +474,15 @@ def somefunc(): ] ) - def test_txtfile_failing(self, testdir): - p = testdir.maketxtfile( + def test_txtfile_failing(self, test_path): + p = test_path.maketxtfile( """ >>> i = 0 >>> i + 1 2 """ ) - result = testdir.runpytest(p, "-s") + result = test_path.runpytest(p, "-s") result.stdout.fnmatch_lines( [ "001 >>> i = 0", @@ -492,25 +495,25 @@ def test_txtfile_failing(self, testdir): ] ) - def test_txtfile_with_fixtures(self, testdir): - p = testdir.maketxtfile( + def test_txtfile_with_fixtures(self, test_path): + p = test_path.maketxtfile( """ - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True """ ) - reprec = testdir.inline_run(p) + reprec = test_path.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, test_path): + test_path.makeini( """ [pytest] usefixtures = myfixture """ ) - testdir.makeconftest( + test_path.makeconftest( """ import pytest @pytest.fixture @@ -519,36 +522,36 @@ def myfixture(monkeypatch): """ ) - p = testdir.maketxtfile( + p = test_path.maketxtfile( """ >>> import os >>> os.environ["HELLO"] 'WORLD' """ ) - reprec = testdir.inline_run(p) + reprec = test_path.inline_run(p) reprec.assertoutcome(passed=1) - def test_doctestmodule_with_fixtures(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule_with_fixtures(self, test_path): + p = test_path.makepyfile( """ ''' - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = test_path.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, test_path): + p = test_path.makepyfile( """ ''' - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True ''' def my_func(): ''' @@ -566,11 +569,11 @@ def another(): ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = test_path.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, test_path): + p = test_path.makepyfile( """ class MyClass(object): def bad_meth(self): @@ -587,17 +590,17 @@ def nice_meth(self): ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = test_path.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1, passed=1) - def test_ignored_whitespace(self, testdir): - testdir.makeini( + def test_ignored_whitespace(self, test_path): + test_path.makeini( """ [pytest] doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE """ ) - p = testdir.makepyfile( + p = test_path.makepyfile( """ class MyClass(object): ''' @@ -608,17 +611,17 @@ class MyClass(object): pass """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = test_path.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) - def test_non_ignored_whitespace(self, testdir): - testdir.makeini( + def test_non_ignored_whitespace(self, test_path): + test_path.makeini( """ [pytest] doctest_optionflags = ELLIPSIS """ ) - p = testdir.makepyfile( + p = test_path.makepyfile( """ class MyClass(object): ''' @@ -629,47 +632,47 @@ class MyClass(object): pass """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = test_path.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, test_path): + test_path.makeini( """ [pytest] doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE """ ) - p = testdir.maketxtfile( + p = test_path.maketxtfile( xdoc=""" >>> a = "foo " >>> print(a) foo """ ) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec = test_path.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, test_path): + test_path.makeini( """ [pytest] doctest_optionflags = ELLIPSIS """ ) - p = testdir.maketxtfile( + p = test_path.maketxtfile( xdoc=""" >>> a = "foo " >>> print(a) foo """ ) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec = test_path.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(failed=1, passed=0) - def test_contains_unicode(self, testdir): + def test_contains_unicode(self, test_path): """Fix internal error with docstrings containing non-ascii characters. """ - testdir.makepyfile( + test_path.makepyfile( '''\ def foo(): """ @@ -678,11 +681,11 @@ def foo(): """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = test_path.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, test_path): + p = test_path.makepyfile( """ import asdf @@ -695,16 +698,16 @@ def add_one(x): """ ) - reprec = testdir.inline_run( + reprec = test_path.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, test_path): """ #713: Fix --junit-xml option when used with --doctest-modules. """ - p = testdir.makepyfile( + p = test_path.makepyfile( """ def foo(): ''' @@ -714,15 +717,15 @@ def foo(): pass """ ) - reprec = testdir.inline_run(p, "--doctest-modules", "--junit-xml=junit.xml") + reprec = test_path.inline_run(p, "--doctest-modules", "--junit-xml=junit.xml") reprec.assertoutcome(failed=1) - def test_unicode_doctest(self, testdir): + def test_unicode_doctest(self, test_path): """ Test case for issue 2434: DecodeError on Python 2 when doctest contains non-ascii characters. """ - p = testdir.maketxtfile( + p = test_path.maketxtfile( test_unicode_doctest=""" .. doctest:: @@ -735,17 +738,17 @@ def test_unicode_doctest(self, testdir): 1 """ ) - result = testdir.runpytest(p) + result = test_path.runpytest(p) result.stdout.fnmatch_lines( ["*UNEXPECTED EXCEPTION: ZeroDivisionError*", "*1 failed*"] ) - def test_unicode_doctest_module(self, testdir): + def test_unicode_doctest_module(self, test_path): """ Test case for issue 2434: DecodeError on Python 2 when doctest docstring contains non-ascii characters. """ - p = testdir.makepyfile( + p = test_path.makepyfile( test_unicode_doctest_module=""" def fix_bad_unicode(text): ''' @@ -755,15 +758,15 @@ def fix_bad_unicode(text): return "único" """ ) - result = testdir.runpytest(p, "--doctest-modules") + result = test_path.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) - def test_print_unicode_value(self, testdir): + def test_print_unicode_value(self, test_path): """ Test case for issue 3583: Printing Unicode in doctest under Python 2.7 doesn't work """ - p = testdir.maketxtfile( + p = test_path.maketxtfile( test_print_unicode_value=r""" Here is a doctest:: @@ -771,14 +774,14 @@ def test_print_unicode_value(self, testdir): åéîøü """ ) - result = testdir.runpytest(p) + result = test_path.runpytest(p) result.stdout.fnmatch_lines(["* 1 passed *"]) - def test_reportinfo(self, testdir): + def test_reportinfo(self, test_path): """ Test case to make sure that DoctestItem.reportinfo() returns lineno. """ - p = testdir.makepyfile( + p = test_path.makepyfile( test_reportinfo=""" def foo(x): ''' @@ -788,16 +791,16 @@ def foo(x): return 'c' """ ) - items, reprec = testdir.inline_genitems(p, "--doctest-modules") + items, reprec = test_path.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, test_path): """ Test to make sure that pytest ignores valid setup.py files when ran with --doctest-modules """ - p = testdir.makepyfile( + p = test_path.makepyfile( setup=""" from setuptools import setup, find_packages setup(name='sample', @@ -807,33 +810,33 @@ def test_valid_setup_py(self, testdir): ) """ ) - result = testdir.runpytest(p, "--doctest-modules") + result = test_path.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["*collected 0 items*"]) - def test_invalid_setup_py(self, testdir): + def test_invalid_setup_py(self, test_path): """ 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 = test_path.makepyfile( setup=""" def test_foo(): return 'bar' """ ) - result = testdir.runpytest(p, "--doctest-modules") + result = test_path.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, test_path, 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( + test_path.makeini( """ [pytest] doctest_optionflags = ALLOW_UNICODE @@ -843,7 +846,7 @@ def test_allow_unicode(self, testdir, config_mode): else: comment = "#doctest: +ALLOW_UNICODE" - testdir.maketxtfile( + test_path.maketxtfile( test_doc=""" >>> b'12'.decode('ascii') {comment} '12' @@ -851,7 +854,7 @@ def test_allow_unicode(self, testdir, config_mode): comment=comment ) ) - testdir.makepyfile( + test_path.makepyfile( foo=""" def foo(): ''' @@ -862,17 +865,17 @@ def foo(): comment=comment ) ) - reprec = testdir.inline_run("--doctest-modules") + reprec = test_path.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, test_path, 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( + test_path.makeini( """ [pytest] doctest_optionflags = ALLOW_BYTES @@ -882,7 +885,7 @@ def test_allow_bytes(self, testdir, config_mode): else: comment = "#doctest: +ALLOW_BYTES" - testdir.maketxtfile( + test_path.maketxtfile( test_doc=""" >>> b'foo' {comment} 'foo' @@ -890,7 +893,7 @@ def test_allow_bytes(self, testdir, config_mode): comment=comment ) ) - testdir.makepyfile( + test_path.makepyfile( foo=""" def foo(): ''' @@ -901,34 +904,34 @@ def foo(): comment=comment ) ) - reprec = testdir.inline_run("--doctest-modules") + reprec = test_path.inline_run("--doctest-modules") reprec.assertoutcome(passed=2) - def test_unicode_string(self, testdir): + def test_unicode_string(self, test_path): """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( + test_path.maketxtfile( test_doc=""" >>> b'12'.decode('ascii') '12' """ ) - reprec = testdir.inline_run() + reprec = test_path.inline_run() reprec.assertoutcome(passed=1) - def test_bytes_literal(self, testdir): + def test_bytes_literal(self, test_path): """Test that doctests which output bytes fail in Python 3 when the ALLOW_BYTES option is not used. (#1287). """ - testdir.maketxtfile( + test_path.maketxtfile( test_doc=""" >>> b'foo' 'foo' """ ) - reprec = testdir.inline_run() + reprec = test_path.inline_run() reprec.assertoutcome(failed=1) def test_number_re(self) -> None: @@ -962,10 +965,10 @@ def test_number_re(self) -> None: 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, test_path, config_mode): """Test the NUMBER option.""" if config_mode == "ini": - testdir.makeini( + test_path.makeini( """ [pytest] doctest_optionflags = NUMBER @@ -975,7 +978,7 @@ def test_number_precision(self, testdir, config_mode): else: comment = "#doctest: +NUMBER" - testdir.maketxtfile( + test_path.maketxtfile( test_doc=""" Scalars: @@ -1032,7 +1035,7 @@ def test_number_precision(self, testdir, config_mode): comment=comment ) ) - reprec = testdir.inline_run() + reprec = test_path.inline_run() reprec.assertoutcome(passed=1) @pytest.mark.parametrize( @@ -1056,8 +1059,8 @@ def test_number_precision(self, testdir, config_mode): 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, test_path, expression, output): + test_path.maketxtfile( test_doc=""" >>> {expression} #doctest: +NUMBER {output} @@ -1065,11 +1068,11 @@ def test_number_non_matches(self, testdir, expression, output): expression=expression, output=output ) ) - reprec = testdir.inline_run() + reprec = test_path.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, test_path): + test_path.maketxtfile( test_doc=""" >>> from collections import namedtuple >>> T = namedtuple('T', 'a b c') @@ -1077,7 +1080,7 @@ def test_number_and_allow_unicode(self, testdir): T(a=0.233, b=u'str', c='bytes') """ ) - reprec = testdir.inline_run() + reprec = test_path.inline_run() reprec.assertoutcome(passed=1) @@ -1088,18 +1091,18 @@ class TestDoctestSkips: """ @pytest.fixture(params=["text", "module"]) - def makedoctest(self, testdir, request): + def makedoctest(self, test_path, request): def makeit(doctest): mode = request.param if mode == "text": - testdir.maketxtfile(doctest) + test_path.maketxtfile(doctest) else: assert mode == "module" - testdir.makepyfile('"""\n%s"""' % doctest) + test_path.makepyfile('"""\n%s"""' % doctest) return makeit - def test_one_skipped(self, testdir, makedoctest): + def test_one_skipped(self, test_path, makedoctest): makedoctest( """ >>> 1 + 1 # doctest: +SKIP @@ -1108,10 +1111,10 @@ def test_one_skipped(self, testdir, makedoctest): 4 """ ) - reprec = testdir.inline_run("--doctest-modules") + reprec = test_path.inline_run("--doctest-modules") reprec.assertoutcome(passed=1) - def test_one_skipped_failed(self, testdir, makedoctest): + def test_one_skipped_failed(self, test_path, makedoctest): makedoctest( """ >>> 1 + 1 # doctest: +SKIP @@ -1120,10 +1123,10 @@ def test_one_skipped_failed(self, testdir, makedoctest): 200 """ ) - reprec = testdir.inline_run("--doctest-modules") + reprec = test_path.inline_run("--doctest-modules") reprec.assertoutcome(failed=1) - def test_all_skipped(self, testdir, makedoctest): + def test_all_skipped(self, test_path, makedoctest): makedoctest( """ >>> 1 + 1 # doctest: +SKIP @@ -1132,16 +1135,16 @@ def test_all_skipped(self, testdir, makedoctest): 200 """ ) - reprec = testdir.inline_run("--doctest-modules") + reprec = test_path.inline_run("--doctest-modules") reprec.assertoutcome(skipped=1) - def test_vacuous_all_skipped(self, testdir, makedoctest): + def test_vacuous_all_skipped(self, test_path, makedoctest): makedoctest("") - reprec = testdir.inline_run("--doctest-modules") + reprec = test_path.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, test_path): + test_path.maketxtfile( test_something=""" >>> i = 5 >>> def foo(): @@ -1153,7 +1156,9 @@ def test_continue_on_failure(self, testdir): >>> i + 1 """ ) - result = testdir.runpytest("--doctest-modules", "--doctest-continue-on-failure") + result = test_path.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. @@ -1166,12 +1171,12 @@ class TestDoctestAutoUseFixtures: SCOPES = ["module", "session", "class", "function"] - def test_doctest_module_session_fixture(self, testdir): + def test_doctest_module_session_fixture(self, test_path): """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( + test_path.makeconftest( """ import pytest import sys @@ -1184,7 +1189,7 @@ def myfixture(): del sys.pytest_session_data """ ) - testdir.makepyfile( + test_path.makepyfile( foo=""" import sys @@ -1199,16 +1204,16 @@ def bar(): ''' """ ) - result = testdir.runpytest("--doctest-modules") + result = test_path.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, test_path, scope, enable_doctest): """Test that auto-use fixtures work properly with doctest modules. See #1057 and #1100. """ - testdir.makeconftest( + test_path.makeconftest( """ import pytest @@ -1219,7 +1224,7 @@ def auto(request): scope=scope ) ) - testdir.makepyfile( + test_path.makepyfile( test_1=''' def test_foo(): """ @@ -1232,19 +1237,19 @@ def test_bar(): ) params = ("--doctest-modules",) if enable_doctest else () passes = 3 if enable_doctest else 2 - result = testdir.runpytest(*params) + result = test_path.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, test_path, scope, autouse, use_fixture_in_doctest ): """Test that auto-use fixtures work properly with doctest files. See #1057 and #1100. """ - testdir.makeconftest( + test_path.makeconftest( """ import pytest @@ -1256,29 +1261,29 @@ def auto(request): ) ) if use_fixture_in_doctest: - testdir.maketxtfile( + test_path.maketxtfile( test_doc=""" >>> getfixture('auto') 99 """ ) else: - testdir.maketxtfile( + test_path.maketxtfile( test_doc=""" >>> 1 + 1 2 """ ) - result = testdir.runpytest("--doctest-modules") + result = test_path.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, test_path, scope): """Check that all attributes of a request in an autouse fixture behave as expected when requested for a doctest item. """ - testdir.makeconftest( + test_path.makeconftest( """ import pytest @@ -1295,13 +1300,13 @@ def auto(request): scope=scope ) ) - testdir.maketxtfile( + test_path.maketxtfile( test_doc=""" >>> 1 + 1 2 """ ) - result = testdir.runpytest("--doctest-modules") + result = test_path.runpytest("--doctest-modules") str(result.stdout.no_fnmatch_line("*FAILURES*")) result.stdout.fnmatch_lines(["*=== 1 passed in *"]) @@ -1311,12 +1316,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, test_path, scope): """ Check that inserting something into the namespace works in a simple text file doctest """ - testdir.makeconftest( + test_path.makeconftest( """ import pytest import contextlib @@ -1328,22 +1333,22 @@ def add_contextlib(doctest_namespace): scope=scope ) ) - p = testdir.maketxtfile( + p = test_path.maketxtfile( """ >>> print(cl.__name__) contextlib """ ) - reprec = testdir.inline_run(p) + reprec = test_path.inline_run(p) reprec.assertoutcome(passed=1) @pytest.mark.parametrize("scope", SCOPES) - def test_namespace_pyfile(self, testdir, scope): + def test_namespace_pyfile(self, test_path, scope): """ Check that inserting something into the namespace works in a simple Python file docstring doctest """ - testdir.makeconftest( + test_path.makeconftest( """ import pytest import contextlib @@ -1355,7 +1360,7 @@ def add_contextlib(doctest_namespace): scope=scope ) ) - p = testdir.makepyfile( + p = test_path.makepyfile( """ def foo(): ''' @@ -1364,13 +1369,13 @@ def foo(): ''' """ ) - reprec = testdir.inline_run(p, "--doctest-modules") + reprec = test_path.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, test_path, format): + test_path.makepyfile( """ def foo(): ''' @@ -1386,17 +1391,17 @@ def foo(): '2 3 6') """ ) - return testdir.runpytest("--doctest-modules", "--doctest-report", format) + return test_path.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, test_path, format): + result = self._run_doctest_report(test_path, 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, test_path): + result = self._run_doctest_report(test_path, "cdiff") result.stdout.fnmatch_lines( [ " a b", @@ -1411,8 +1416,8 @@ def test_doctest_report_cdiff(self, testdir): ] ) - def test_doctest_report_ndiff(self, testdir): - result = self._run_doctest_report(testdir, "ndiff") + def test_doctest_report_ndiff(self, test_path): + result = self._run_doctest_report(test_path, "ndiff") result.stdout.fnmatch_lines( [ " a b", @@ -1426,8 +1431,8 @@ def test_doctest_report_ndiff(self, testdir): ) @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, test_path, format): + result = self._run_doctest_report(test_path, format) result.stdout.fnmatch_lines( [ "Expected:", @@ -1443,8 +1448,8 @@ def test_doctest_report_none_or_only_first_failure(self, testdir, format): ] ) - def test_doctest_report_invalid(self, testdir): - result = self._run_doctest_report(testdir, "obviously_invalid_format") + def test_doctest_report_invalid(self, test_path): + result = self._run_doctest_report(test_path, "obviously_invalid_format") result.stderr.fnmatch_lines( [ "*error: argument --doctest-report: invalid choice: 'obviously_invalid_format' (choose from*" @@ -1453,9 +1458,9 @@ def test_doctest_report_invalid(self, testdir): @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, test_path): pytest.importorskip(mock_module) - testdir.makepyfile( + test_path.makepyfile( """ from {mock_module} import call class Example(object): @@ -1467,7 +1472,7 @@ class Example(object): mock_module=mock_module ) ) - result = testdir.runpytest("--doctest-modules") + result = test_path.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) @@ -1494,25 +1499,25 @@ 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")) -def test_is_setup_py_is_a_setup_py(tmpdir, mod): - setup_py = tmpdir.join("setup.py") - setup_py.write('from {} import setup; setup(name="foo")'.format(mod)) - assert _is_setup_py(setup_py) +def test_is_setup_py_is_a_setup_py(tmp_path, mod): + setup_py = tmp_path.joinpath("setup.py") + setup_py.write_text('from {} import setup; setup(name="foo")'.format(mod)) + assert _is_setup_py(py.path.local(str(setup_py))) @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 d0afb40b07d..9171f2008a0 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -612,7 +612,7 @@ def test_pytester_addopts_before_testdir(request, monkeypatch) -> None: monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused") testdir = request.getfixturevalue("testdir") assert "PYTEST_ADDOPTS" not in os.environ - testdir.finalize() + testdir._test_path._finalize() assert os.environ.get("PYTEST_ADDOPTS") == "--orig-unused" monkeypatch.undo() assert os.environ.get("PYTEST_ADDOPTS") == orig @@ -799,9 +799,5 @@ 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")