From 432916a0aba085b7c1fdbba5eefd01a7c2a699d6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 27 Jun 2020 13:50:25 -0300 Subject: [PATCH 1/8] New pytester fixture New pytester fixture with same functionality as testdir, but dealing with pathlib.Path objects. Also: * Change Testdir to be a thin wrapper which only delegates to PyTester. * Convert test_doctest to use test_path and tmp_path. * Removed some str() calls now unnecessary given py35 support has been dropped. * Use test_path in test_no_brokenpipeerror_message: it uses a private method which I don't think we should expose in Testdir for now. --- changelog/7425.feature.rst | 5 + doc/en/reference.rst | 27 ++- src/_pytest/pytester.py | 422 +++++++++++++++++++++++--------- testing/acceptance_test.py | 6 +- testing/test_doctest.py | 475 +++++++++++++++++++------------------ testing/test_pytester.py | 8 +- 6 files changed, 581 insertions(+), 362 deletions(-) create mode 100644 changelog/7425.feature.rst diff --git a/changelog/7425.feature.rst b/changelog/7425.feature.rst new file mode 100644 index 00000000000..44eea4c07a0 --- /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 15d8250844f..d34f2cd40ac 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. + +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 @@ -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 e66e718f100..49df62cffa3 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 @@ -19,12 +21,14 @@ 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 +51,7 @@ 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 +180,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 +434,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. +def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "PyTester": + """ + 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 `tmpdir` fixture - but provides methods which aid in testing pytest itself. + 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 PyTester(request, tmp_path_factory) + + +@pytest.fixture +def testdir(pytester: "PyTester") -> "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(pytester) @pytest.fixture @@ -599,16 +619,17 @@ def restore(self) -> None: @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 +645,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 +657,48 @@ 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._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") - 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.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) -> str: - return f"" + @property + def path(self) -> Path: + """Temporary directory where files are created and pytest is executed.""" + return self._path - def __str__(self) -> str: - return str(self.tmpdir) + def __repr__(self) -> str: + return f"" - 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 +712,7 @@ def preserve_module(name): 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,9 +720,9 @@ def chdir(self) -> None: 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, files, encoding: str = "utf-8") -> Path: items = list(files.items()) def to_text(s): @@ -710,17 +735,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.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) -> 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,11 +769,11 @@ def makefile(self, ext: str, *args: str, **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) @@ -756,14 +782,14 @@ def getinicfg(self, source) -> IniConfig: 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 @@ -783,7 +809,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 @@ -810,67 +836,74 @@ def syspathinsert(self, path=None) -> None: 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=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``). + :return: path to the copied directory (inside ``self.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(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 +912,7 @@ def copy_example(self, name=None) -> py.path.local: Session = Session - def getnode(self, config: Config, arg): + def getnode(self, config: Config, arg) -> Optional[Union[Collector, Item]]: """Return the collection node of a file. :param _pytest.config.Config config: @@ -935,7 +968,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) @@ -1016,7 +1049,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: @@ -1024,7 +1057,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. @@ -1095,7 +1128,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.path.parent.joinpath("basetemp")) return args def parseconfig(self, *args) -> Config: @@ -1109,15 +1142,16 @@ def parseconfig(self, *args) -> Config: 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: @@ -1177,10 +1211,10 @@ def getmodulecol(self, source, configargs=(), withinit: bool = False): 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 +1242,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, ): @@ -1244,18 +1278,22 @@ def popen( return popen def run( - self, *cmdargs, timeout: Optional[float] = None, stdin=CLOSE_STDIN + self, + *cmdargs: Union[str, py.path.local, Path], + 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 timeout: + :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`. - :param stdin: + :py:class:`Testdir.TimeoutExpired` + :kwarg stdin: Optional standard input. Bytes are being send, closing the pipe, otherwise it is passed through to ``popen``. Defaults to ``CLOSE_STDIN``, which translates to using a pipe @@ -1266,15 +1304,15 @@ def run( __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.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 +1343,16 @@ def handle_timeout() -> None: 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 +1397,7 @@ def runpytest_subprocess(self, *args, timeout: Optional[float] = None) -> RunRes :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 +1415,8 @@ def spawn_pytest( 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 +1431,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.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 +1457,174 @@ def assert_contains_lines(self, lines2: Sequence[str]) -> None: 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 = attr.ib(type=PyTester) + + @property + def tmpdir(self) -> py.path.local: + return py.path.local(self._pytester.path) + + @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/testing/acceptance_test.py b/testing/acceptance_test.py index f4b7d6135ed..a55473d3601 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 PyTester def prepend_pythonpath(*dirs): @@ -1276,14 +1276,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(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 0e8bba980f0..1274df56834 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.doctest import _get_checker from _pytest.doctest import _is_mocked @@ -14,9 +16,9 @@ class TestDoctests: - def test_collect_testtextfile(self, testdir): - w = testdir.maketxtfile(whatever="") - checkfile = testdir.maketxtfile( + def test_collect_testtextfile(self, pytester): + w = pytester.maketxtfile(whatever="") + checkfile = pytester.maketxtfile( test_something=""" alskdjalsdk >>> i = 5 @@ -25,48 +27,48 @@ def test_collect_testtextfile(self, testdir): """ ) - 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): + 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): + 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): + 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): + path = pytester.makepyfile( whatever=""" '# Empty' def my_func(): @@ -83,72 +85,72 @@ def another(): ''' """ ) - 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): + 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): + 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): """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 +164,22 @@ def test_encoding(self, testdir, test_string, encoding): """.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.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 +199,8 @@ def test_doctest_unexpected_exception(self, testdir): consecutive=True, ) - def test_doctest_outcomes(self, testdir): - testdir.maketxtfile( + def test_doctest_outcomes(self, pytester): + pytester.maketxtfile( test_skip=""" >>> 1 1 @@ -219,7 +222,7 @@ def test_doctest_outcomes(self, testdir): bar """, ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "collected 3 items", @@ -232,11 +235,11 @@ def test_doctest_outcomes(self, testdir): ] ) - def test_docstring_partial_context_around_error(self, testdir): + def test_docstring_partial_context_around_error(self, pytester): """Test that we show some context before the actual line of a failing doctest. """ - testdir.makepyfile( + pytester.makepyfile( ''' def foo(): """ @@ -258,7 +261,7 @@ def foo(): """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*docstring_partial_context_around_error*", @@ -276,11 +279,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, 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 +295,7 @@ def foo(): """ ''' ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*docstring_full_context_around_error*", @@ -306,8 +309,8 @@ def foo(): ] ) - def test_doctest_linedata_missing(self, testdir): - testdir.tmpdir.join("hello.py").write( + def test_doctest_linedata_missing(self, pytester): + pytester.path.joinpath("hello.py").write_text( textwrap.dedent( """\ class Fun(object): @@ -320,13 +323,13 @@ def test(self): """ ) ) - 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.makepyfile( """ class Sample(object): @property @@ -338,7 +341,7 @@ def some_property(self): return 'something' """ ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines( [ "*= FAILURES =*", @@ -355,8 +358,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, pytester): + pytester.makepyfile( """ class Sample(object): @property @@ -369,7 +372,7 @@ def some_property(self): 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 +389,14 @@ def some_property(self): ] ) - def test_doctest_unex_importerror_only_txt(self, testdir): - testdir.maketxtfile( + def test_doctest_unex_importerror_only_txt(self, 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 +406,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, 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 +430,8 @@ def test_doctest_unex_importerror_with_module(self, testdir): ] ) - def test_doctestmodule(self, testdir): - p = testdir.makepyfile( + def test_doctestmodule(self, pytester): + p = pytester.makepyfile( """ ''' >>> x = 1 @@ -438,12 +441,12 @@ def test_doctestmodule(self, testdir): ''' """ ) - 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): + p = pytester.mkpydir("hello") + p.joinpath("__init__.py").write_text( textwrap.dedent( """\ def somefunc(): @@ -455,7 +458,7 @@ def somefunc(): """ ) ) - result = testdir.runpytest(p, "--doctest-modules") + result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines( [ "003 *>>> i = 0", @@ -468,15 +471,15 @@ def somefunc(): ] ) - def test_txtfile_failing(self, testdir): - p = testdir.maketxtfile( + def test_txtfile_failing(self, 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 +492,25 @@ def test_txtfile_failing(self, testdir): ] ) - def test_txtfile_with_fixtures(self, testdir): - p = testdir.maketxtfile( + def test_txtfile_with_fixtures(self, 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.makeini( """ [pytest] usefixtures = myfixture """ ) - testdir.makeconftest( + pytester.makeconftest( """ import pytest @pytest.fixture @@ -516,36 +519,36 @@ def myfixture(monkeypatch): """ ) - 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): + 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): + p = pytester.makepyfile( """ ''' - >>> dir = getfixture('tmpdir') - >>> type(dir).__name__ - 'LocalPath' + >>> p = getfixture('tmp_path') + >>> p.is_dir() + True ''' def my_func(): ''' @@ -563,11 +566,11 @@ def another(): ''' """ ) - 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): + p = pytester.makepyfile( """ class MyClass(object): def bad_meth(self): @@ -584,17 +587,17 @@ def nice_meth(self): ''' """ ) - 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.makeini( """ [pytest] doctest_optionflags = ELLIPSIS NORMALIZE_WHITESPACE """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ class MyClass(object): ''' @@ -605,17 +608,17 @@ class MyClass(object): 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.makeini( """ [pytest] doctest_optionflags = ELLIPSIS """ ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ class MyClass(object): ''' @@ -626,46 +629,46 @@ class MyClass(object): 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.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.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): """Fix internal error with docstrings containing non-ascii characters.""" - testdir.makepyfile( + pytester.makepyfile( '''\ def foo(): """ @@ -674,11 +677,11 @@ def foo(): """ ''' ) - 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): + p = pytester.makepyfile( """ import asdf @@ -691,14 +694,14 @@ def add_one(x): """ ) - 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): """#713: Fix --junit-xml option when used with --doctest-modules.""" - p = testdir.makepyfile( + p = pytester.makepyfile( """ def foo(): ''' @@ -708,15 +711,15 @@ def foo(): 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): """ 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 +732,17 @@ def test_unicode_doctest(self, testdir): 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): """ 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 +752,15 @@ def fix_bad_unicode(text): 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): """ 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 +768,12 @@ def test_print_unicode_value(self, testdir): åéîøü """ ) - result = testdir.runpytest(p) + result = pytester.runpytest(p) result.stdout.fnmatch_lines(["* 1 passed *"]) - def test_reportinfo(self, testdir): + def test_reportinfo(self, pytester): """Make sure that DoctestItem.reportinfo() returns lineno.""" - p = testdir.makepyfile( + p = pytester.makepyfile( test_reportinfo=""" def foo(x): ''' @@ -780,16 +783,16 @@ def foo(x): 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): """ 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 +802,33 @@ def test_valid_setup_py(self, testdir): ) """ ) - 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): """ 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 +838,7 @@ def test_allow_unicode(self, testdir, config_mode): else: comment = "#doctest: +ALLOW_UNICODE" - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'12'.decode('ascii') {comment} '12' @@ -843,7 +846,7 @@ def test_allow_unicode(self, testdir, config_mode): comment=comment ) ) - testdir.makepyfile( + pytester.makepyfile( foo=""" def foo(): ''' @@ -854,17 +857,17 @@ def foo(): 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 +877,7 @@ def test_allow_bytes(self, testdir, config_mode): else: comment = "#doctest: +ALLOW_BYTES" - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" >>> b'foo' {comment} 'foo' @@ -882,7 +885,7 @@ def test_allow_bytes(self, testdir, config_mode): comment=comment ) ) - testdir.makepyfile( + pytester.makepyfile( foo=""" def foo(): ''' @@ -893,34 +896,34 @@ def foo(): 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): """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): """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 +957,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, pytester, config_mode): """Test the NUMBER option.""" if config_mode == "ini": - testdir.makeini( + pytester.makeini( """ [pytest] doctest_optionflags = NUMBER @@ -967,7 +970,7 @@ def test_number_precision(self, testdir, config_mode): else: comment = "#doctest: +NUMBER" - testdir.maketxtfile( + pytester.maketxtfile( test_doc=""" Scalars: @@ -1024,7 +1027,7 @@ def test_number_precision(self, testdir, config_mode): comment=comment ) ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @pytest.mark.parametrize( @@ -1048,8 +1051,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, pytester, expression, output): + pytester.maketxtfile( test_doc=""" >>> {expression} #doctest: +NUMBER {output} @@ -1057,11 +1060,11 @@ def test_number_non_matches(self, testdir, expression, output): 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.maketxtfile( test_doc=""" >>> from collections import namedtuple >>> T = namedtuple('T', 'a b c') @@ -1069,7 +1072,7 @@ def test_number_and_allow_unicode(self, testdir): T(a=0.233, b=u'str', c='bytes') """ ) - reprec = testdir.inline_run() + reprec = pytester.inline_run() reprec.assertoutcome(passed=1) @@ -1080,18 +1083,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 +1103,10 @@ def test_one_skipped(self, testdir, makedoctest): 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 +1115,10 @@ def test_one_skipped_failed(self, testdir, makedoctest): 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 +1127,16 @@ def test_all_skipped(self, testdir, makedoctest): 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.maketxtfile( test_something=""" >>> i = 5 >>> def foo(): @@ -1145,7 +1148,9 @@ def test_continue_on_failure(self, testdir): >>> 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 +1163,11 @@ class TestDoctestAutoUseFixtures: SCOPES = ["module", "session", "class", "function"] - def test_doctest_module_session_fixture(self, testdir): + def test_doctest_module_session_fixture(self, 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 +1180,7 @@ def myfixture(): del sys.pytest_session_data """ ) - testdir.makepyfile( + pytester.makepyfile( foo=""" import sys @@ -1190,16 +1195,16 @@ def bar(): ''' """ ) - 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 +1215,7 @@ def auto(request): scope=scope ) ) - testdir.makepyfile( + pytester.makepyfile( test_1=''' def test_foo(): """ @@ -1223,19 +1228,19 @@ def test_bar(): ) 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 +1252,29 @@ def auto(request): ) ) 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 +1291,13 @@ def auto(request): 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 +1307,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 +1324,22 @@ def add_contextlib(doctest_namespace): 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 +1351,7 @@ def add_contextlib(doctest_namespace): scope=scope ) ) - p = testdir.makepyfile( + p = pytester.makepyfile( """ def foo(): ''' @@ -1355,13 +1360,13 @@ def foo(): ''' """ ) - 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 +1382,17 @@ def foo(): '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): + result = self._run_doctest_report(pytester, "cdiff") result.stdout.fnmatch_lines( [ " a b", @@ -1402,8 +1407,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, pytester): + result = self._run_doctest_report(pytester, "ndiff") result.stdout.fnmatch_lines( [ " a b", @@ -1417,8 +1422,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, pytester, format): + result = self._run_doctest_report(pytester, format) result.stdout.fnmatch_lines( [ "Expected:", @@ -1434,8 +1439,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, 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 +1449,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, pytester): pytest.importorskip(mock_module) - testdir.makepyfile( + pytester.makepyfile( """ from {mock_module} import call class Example(object): @@ -1458,7 +1463,7 @@ class Example(object): mock_module=mock_module ) ) - result = testdir.runpytest("--doctest-modules") + result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) @@ -1485,10 +1490,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 +1504,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 dd3855c69ff..d42db8b8347 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -801,9 +801,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") From a8b07cec54e89b1c5d57986a9dd4bdd7b01c868e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Oct 2020 10:22:06 -0300 Subject: [PATCH 2/8] Code review improvements --- changelog/7425.feature.rst | 2 +- doc/en/reference.rst | 4 +- src/_pytest/pytester.py | 47 ++++++++++--------- testing/acceptance_test.py | 4 +- testing/test_doctest.py | 95 +++++++++++++++++++------------------- 5 files changed, 79 insertions(+), 73 deletions(-) diff --git a/changelog/7425.feature.rst b/changelog/7425.feature.rst index 44eea4c07a0..55881d2074f 100644 --- a/changelog/7425.feature.rst +++ b/changelog/7425.feature.rst @@ -2,4 +2,4 @@ New :fixture:`pytester` fixture, which is identical to :fixture:`testdir` but it 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. +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 d34f2cd40ac..b78092c2d90 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -508,7 +508,7 @@ pytester .. currentmodule:: _pytest.pytester -Provides a :class:`PyTester` instance that can be used to run and test pytest itself. +Provides a :class:`Pytester` 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. @@ -521,7 +521,7 @@ To use it, include in your topmost ``conftest.py`` file: -.. autoclass:: PyTester() +.. autoclass:: Pytester() :members: .. autoclass:: RunResult() diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 49df62cffa3..92072f373f8 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -58,6 +58,10 @@ import pexpect + PathLike = os.PathLike[str] +else: + PathLike = os.PathLike + IGNORE_PAM = [ # filenames added when obtaining details about the current user "/var/lib/sss/mc/passwd" @@ -434,7 +438,7 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]: @pytest.fixture -def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "PyTester": +def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester": """ Facilities to write tests/configuration files, execute pytest in isolation, and match against expected output, perfect for black-box testing of pytest plugins. @@ -442,14 +446,14 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "PyT 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 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. + 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) + return Pytester(request, tmp_path_factory) @pytest.fixture -def testdir(pytester: "PyTester") -> "Testdir": +def testdir(pytester: "Pytester") -> "Testdir": """ Identical to :fixture:`test_path`, and provides an instance whose methods return legacy ``py.path.local`` objects instead when applicable. @@ -619,7 +623,7 @@ def restore(self) -> None: @final -class PyTester: +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. @@ -685,7 +689,7 @@ def path(self) -> Path: return self._path def __repr__(self) -> str: - return f"" + return f"" def _finalize(self) -> None: """ @@ -888,9 +892,7 @@ def copy_example(self, name=None) -> Path: 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} cant be found as module or package in {example_dir}" ) else: example_path = example_dir.joinpath(name) @@ -912,7 +914,9 @@ def copy_example(self, name=None) -> Path: Session = Session - def getnode(self, config: Config, arg) -> Optional[Union[Collector, Item]]: + def getnode( + self, config: Config, arg: Union[PathLike, str] + ) -> Optional[Union[Collector, Item]]: """Return the collection node of a file. :param _pytest.config.Config config: @@ -929,7 +933,7 @@ def getnode(self, config: Config, arg) -> Optional[Union[Collector, Item]]: config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res - def getpathnode(self, path): + def getpathnode(self, path: Union[PathLike, str]): """Return the collection node of a file. This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to @@ -937,6 +941,7 @@ def getpathnode(self, path): :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) @@ -1279,7 +1284,7 @@ def popen( def run( self, - *cmdargs: Union[str, py.path.local, Path], + *cmdargs: Union[str, PathLike], timeout: Optional[float] = None, stdin=CLOSE_STDIN, ) -> RunResult: @@ -1290,10 +1295,10 @@ def run( :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: + :param timeout: The period in seconds after which to timeout and raise :py:class:`Testdir.TimeoutExpired` - :kwarg stdin: + :param stdin: Optional standard input. Bytes are being send, closing the pipe, otherwise it is passed through to ``popen``. Defaults to ``CLOSE_STDIN``, which translates to using a pipe @@ -1461,19 +1466,19 @@ def assert_contains_lines(self, lines2: Sequence[str]) -> None: @attr.s(repr=False, str=False) class Testdir: """ - Similar to :class:`PyTester`, but this class works with legacy py.path.local objects instead. + 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 + 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 + CLOSE_STDIN = Pytester.CLOSE_STDIN + TimeoutExpired = Pytester.TimeoutExpired + Session = Pytester.Session - _pytester = attr.ib(type=PyTester) + _pytester = attr.ib(type=Pytester) @property def tmpdir(self) -> py.path.local: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index a55473d3601..c937ce9dc1e 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 PyTester +from _pytest.pytester import Pytester def prepend_pythonpath(*dirs): @@ -1276,7 +1276,7 @@ def test_simple(): sys.platform == "win32", reason="Windows raises `OSError: [Errno 22] Invalid argument` instead", ) -def test_no_brokenpipeerror_message(pytester: PyTester) -> 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 diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 1274df56834..67e93b76a49 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -13,10 +13,11 @@ 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, pytester): + def test_collect_testtextfile(self, pytester: Pytester): w = pytester.maketxtfile(whatever="") checkfile = pytester.maketxtfile( test_something=""" @@ -37,13 +38,13 @@ def test_collect_testtextfile(self, pytester): items, reprec = pytester.inline_genitems(w) assert len(items) == 0 - def test_collect_module_empty(self, pytester): + 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, pytester): + 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") @@ -51,7 +52,7 @@ def test_collect_module_single_modulelevel_doctest(self, pytester): assert isinstance(items[0], DoctestItem) assert isinstance(items[0].parent, DoctestModule) - def test_collect_module_two_doctest_one_modulelevel(self, pytester): + def test_collect_module_two_doctest_one_modulelevel(self, pytester: Pytester): path = pytester.makepyfile( whatever=""" '>>> x = None' @@ -67,7 +68,7 @@ def my_func(): assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - def test_collect_module_two_doctest_no_modulelevel(self, pytester): + def test_collect_module_two_doctest_no_modulelevel(self, pytester: Pytester): path = pytester.makepyfile( whatever=""" '# Empty' @@ -93,7 +94,7 @@ def another(): assert isinstance(items[0].parent, DoctestModule) assert items[0].parent is items[1].parent - def test_simple_doctestfile(self, pytester): + def test_simple_doctestfile(self, pytester: Pytester): p = pytester.maketxtfile( test_doc=""" >>> x = 1 @@ -104,7 +105,7 @@ def test_simple_doctestfile(self, pytester): reprec = pytester.inline_run(p) reprec.assertoutcome(failed=1) - def test_new_pattern(self, pytester): + def test_new_pattern(self, pytester: Pytester): p = pytester.maketxtfile( xdoc=""" >>> x = 1 @@ -115,7 +116,7 @@ def test_new_pattern(self, pytester): reprec = pytester.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(failed=1) - def test_multiple_patterns(self, pytester): + def test_multiple_patterns(self, pytester: Pytester): """Test support for multiple --doctest-glob arguments (#1255).""" pytester.maketxtfile( xdoc=""" @@ -171,7 +172,7 @@ def test_encoding(self, pytester, test_string, encoding): result.stdout.fnmatch_lines(["*1 passed*"]) - def test_doctest_unexpected_exception(self, pytester): + def test_doctest_unexpected_exception(self, pytester: Pytester): pytester.maketxtfile( """ >>> i = 0 @@ -199,7 +200,7 @@ def test_doctest_unexpected_exception(self, pytester): consecutive=True, ) - def test_doctest_outcomes(self, pytester): + def test_doctest_outcomes(self, pytester: Pytester): pytester.maketxtfile( test_skip=""" >>> 1 @@ -235,7 +236,7 @@ def test_doctest_outcomes(self, pytester): ] ) - def test_docstring_partial_context_around_error(self, pytester): + def test_docstring_partial_context_around_error(self, pytester: Pytester): """Test that we show some context before the actual line of a failing doctest. """ @@ -279,7 +280,7 @@ 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, pytester): + 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. """ @@ -309,7 +310,7 @@ def foo(): ] ) - def test_doctest_linedata_missing(self, pytester): + def test_doctest_linedata_missing(self, pytester: Pytester): pytester.path.joinpath("hello.py").write_text( textwrap.dedent( """\ @@ -328,7 +329,7 @@ def test(self): ["*hello*", "006*>>> 1/0*", "*UNEXPECTED*ZeroDivision*", "*1 failed*"] ) - def test_doctest_linedata_on_property(self, pytester): + def test_doctest_linedata_on_property(self, pytester: Pytester): pytester.makepyfile( """ class Sample(object): @@ -358,7 +359,7 @@ def some_property(self): ] ) - def test_doctest_no_linedata_on_overriden_property(self, pytester): + def test_doctest_no_linedata_on_overriden_property(self, pytester: Pytester): pytester.makepyfile( """ class Sample(object): @@ -389,7 +390,7 @@ def some_property(self): ] ) - def test_doctest_unex_importerror_only_txt(self, pytester): + def test_doctest_unex_importerror_only_txt(self, pytester: Pytester): pytester.maketxtfile( """ >>> import asdalsdkjaslkdjasd @@ -406,7 +407,7 @@ def test_doctest_unex_importerror_only_txt(self, pytester): ] ) - def test_doctest_unex_importerror_with_module(self, pytester): + def test_doctest_unex_importerror_with_module(self, pytester: Pytester): pytester.path.joinpath("hello.py").write_text( textwrap.dedent( """\ @@ -430,7 +431,7 @@ def test_doctest_unex_importerror_with_module(self, pytester): ] ) - def test_doctestmodule(self, pytester): + def test_doctestmodule(self, pytester: Pytester): p = pytester.makepyfile( """ ''' @@ -444,7 +445,7 @@ def test_doctestmodule(self, pytester): reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1) - def test_doctestmodule_external_and_issue116(self, pytester): + def test_doctestmodule_external_and_issue116(self, pytester: Pytester): p = pytester.mkpydir("hello") p.joinpath("__init__.py").write_text( textwrap.dedent( @@ -471,7 +472,7 @@ def somefunc(): ] ) - def test_txtfile_failing(self, pytester): + def test_txtfile_failing(self, pytester: Pytester): p = pytester.maketxtfile( """ >>> i = 0 @@ -492,7 +493,7 @@ def test_txtfile_failing(self, pytester): ] ) - def test_txtfile_with_fixtures(self, pytester): + def test_txtfile_with_fixtures(self, pytester: Pytester): p = pytester.maketxtfile( """ >>> p = getfixture('tmp_path') @@ -503,7 +504,7 @@ def test_txtfile_with_fixtures(self, pytester): reprec = pytester.inline_run(p) reprec.assertoutcome(passed=1) - def test_txtfile_with_usefixtures_in_ini(self, pytester): + def test_txtfile_with_usefixtures_in_ini(self, pytester: Pytester): pytester.makeini( """ [pytest] @@ -529,7 +530,7 @@ def myfixture(monkeypatch): reprec = pytester.inline_run(p) reprec.assertoutcome(passed=1) - def test_doctestmodule_with_fixtures(self, pytester): + def test_doctestmodule_with_fixtures(self, pytester: Pytester): p = pytester.makepyfile( """ ''' @@ -542,7 +543,7 @@ def test_doctestmodule_with_fixtures(self, pytester): reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) - def test_doctestmodule_three_tests(self, pytester): + def test_doctestmodule_three_tests(self, pytester: Pytester): p = pytester.makepyfile( """ ''' @@ -569,7 +570,7 @@ def another(): reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=3) - def test_doctestmodule_two_tests_one_fail(self, pytester): + def test_doctestmodule_two_tests_one_fail(self, pytester: Pytester): p = pytester.makepyfile( """ class MyClass(object): @@ -590,7 +591,7 @@ def nice_meth(self): reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1, passed=1) - def test_ignored_whitespace(self, pytester): + def test_ignored_whitespace(self, pytester: Pytester): pytester.makeini( """ [pytest] @@ -611,7 +612,7 @@ class MyClass(object): reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) - def test_non_ignored_whitespace(self, pytester): + def test_non_ignored_whitespace(self, pytester: Pytester): pytester.makeini( """ [pytest] @@ -632,7 +633,7 @@ class MyClass(object): reprec = pytester.inline_run(p, "--doctest-modules") reprec.assertoutcome(failed=1, passed=0) - def test_ignored_whitespace_glob(self, pytester): + def test_ignored_whitespace_glob(self, pytester: Pytester): pytester.makeini( """ [pytest] @@ -649,7 +650,7 @@ def test_ignored_whitespace_glob(self, pytester): reprec = pytester.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(passed=1) - def test_non_ignored_whitespace_glob(self, pytester): + def test_non_ignored_whitespace_glob(self, pytester: Pytester): pytester.makeini( """ [pytest] @@ -666,7 +667,7 @@ def test_non_ignored_whitespace_glob(self, pytester): reprec = pytester.inline_run(p, "--doctest-glob=x*.txt") reprec.assertoutcome(failed=1, passed=0) - def test_contains_unicode(self, pytester): + def test_contains_unicode(self, pytester: Pytester): """Fix internal error with docstrings containing non-ascii characters.""" pytester.makepyfile( '''\ @@ -680,7 +681,7 @@ def foo(): result = pytester.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["Got nothing", "* 1 failed in*"]) - def test_ignore_import_errors_on_doctest(self, pytester): + def test_ignore_import_errors_on_doctest(self, pytester: Pytester): p = pytester.makepyfile( """ import asdf @@ -699,7 +700,7 @@ def add_one(x): ) reprec.assertoutcome(skipped=1, failed=1, passed=0) - def test_junit_report_for_doctest(self, pytester): + def test_junit_report_for_doctest(self, pytester: Pytester): """#713: Fix --junit-xml option when used with --doctest-modules.""" p = pytester.makepyfile( """ @@ -714,7 +715,7 @@ def foo(): reprec = pytester.inline_run(p, "--doctest-modules", "--junit-xml=junit.xml") reprec.assertoutcome(failed=1) - def test_unicode_doctest(self, pytester): + def test_unicode_doctest(self, pytester: Pytester): """ Test case for issue 2434: DecodeError on Python 2 when doctest contains non-ascii characters. @@ -737,7 +738,7 @@ def test_unicode_doctest(self, pytester): ["*UNEXPECTED EXCEPTION: ZeroDivisionError*", "*1 failed*"] ) - def test_unicode_doctest_module(self, pytester): + def test_unicode_doctest_module(self, pytester: Pytester): """ Test case for issue 2434: DecodeError on Python 2 when doctest docstring contains non-ascii characters. @@ -755,7 +756,7 @@ def fix_bad_unicode(text): result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) - def test_print_unicode_value(self, pytester): + def test_print_unicode_value(self, pytester: Pytester): """ Test case for issue 3583: Printing Unicode in doctest under Python 2.7 doesn't work @@ -771,7 +772,7 @@ def test_print_unicode_value(self, pytester): result = pytester.runpytest(p) result.stdout.fnmatch_lines(["* 1 passed *"]) - def test_reportinfo(self, pytester): + def test_reportinfo(self, pytester: Pytester): """Make sure that DoctestItem.reportinfo() returns lineno.""" p = pytester.makepyfile( test_reportinfo=""" @@ -787,7 +788,7 @@ def foo(x): reportinfo = items[0].reportinfo() assert reportinfo[1] == 1 - def test_valid_setup_py(self, pytester): + def test_valid_setup_py(self, pytester: Pytester): """ Test to make sure that pytest ignores valid setup.py files when ran with --doctest-modules @@ -805,7 +806,7 @@ def test_valid_setup_py(self, pytester): result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["*collected 0 items*"]) - def test_invalid_setup_py(self, pytester): + 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 @@ -899,7 +900,7 @@ def foo(): reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=2) - def test_unicode_string(self, pytester): + 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. @@ -913,7 +914,7 @@ def test_unicode_string(self, pytester): reprec = pytester.inline_run() reprec.assertoutcome(passed=1) - def test_bytes_literal(self, pytester): + 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). """ @@ -1063,7 +1064,7 @@ def test_number_non_matches(self, pytester, expression, output): reprec = pytester.inline_run() reprec.assertoutcome(passed=0, failed=1) - def test_number_and_allow_unicode(self, pytester): + def test_number_and_allow_unicode(self, pytester: Pytester): pytester.maketxtfile( test_doc=""" >>> from collections import namedtuple @@ -1135,7 +1136,7 @@ def test_vacuous_all_skipped(self, pytester, makedoctest): reprec = pytester.inline_run("--doctest-modules") reprec.assertoutcome(passed=0, skipped=0) - def test_continue_on_failure(self, pytester): + def test_continue_on_failure(self, pytester: Pytester): pytester.maketxtfile( test_something=""" >>> i = 5 @@ -1163,7 +1164,7 @@ class TestDoctestAutoUseFixtures: SCOPES = ["module", "session", "class", "function"] - def test_doctest_module_session_fixture(self, pytester): + 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 @@ -1391,7 +1392,7 @@ def test_doctest_report_udiff(self, pytester, format): [" 0 1 4", " -1 2 4", " +1 2 5", " 2 3 6"] ) - def test_doctest_report_cdiff(self, pytester): + def test_doctest_report_cdiff(self, pytester: Pytester): result = self._run_doctest_report(pytester, "cdiff") result.stdout.fnmatch_lines( [ @@ -1407,7 +1408,7 @@ def test_doctest_report_cdiff(self, pytester): ] ) - def test_doctest_report_ndiff(self, pytester): + def test_doctest_report_ndiff(self, pytester: Pytester): result = self._run_doctest_report(pytester, "ndiff") result.stdout.fnmatch_lines( [ @@ -1439,7 +1440,7 @@ def test_doctest_report_none_or_only_first_failure(self, pytester, format): ] ) - def test_doctest_report_invalid(self, pytester): + def test_doctest_report_invalid(self, pytester: Pytester): result = self._run_doctest_report(pytester, "obviously_invalid_format") result.stderr.fnmatch_lines( [ @@ -1449,7 +1450,7 @@ def test_doctest_report_invalid(self, pytester): @pytest.mark.parametrize("mock_module", ["mock", "unittest.mock"]) -def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, pytester): +def test_doctest_mock_objects_dont_recurse_missbehaved(mock_module, pytester: Pytester): pytest.importorskip(mock_module) pytester.makepyfile( """ From 1a15d646ca08a7269df8abee8757b4ed93889ae3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Oct 2020 10:58:01 -0300 Subject: [PATCH 3/8] Fix docs: test_path -> pytester --- src/_pytest/pytester.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 92072f373f8..ee877659d33 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -444,7 +444,7 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt 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. + 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. @@ -455,10 +455,10 @@ def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pyt @pytest.fixture def testdir(pytester: "Pytester") -> "Testdir": """ - Identical to :fixture:`test_path`, and provides an instance whose methods return + 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:`test_path`. + New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`. """ return Testdir(pytester) From 57cb83ef3234a170222573006dec69ccb339656f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Oct 2020 11:58:43 -0300 Subject: [PATCH 4/8] New code review suggestions --- src/_pytest/pytester.py | 12 ++++++++---- src/_pytest/warning_types.py | 3 --- testing/test_pytester.py | 5 +++++ 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index ee877659d33..8562b175f3d 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -661,6 +661,7 @@ def __init__( else: name = request.node.name self._name = name + self._tmp_path_factory = tmp_path_factory self._path: Path = tmp_path_factory.mktemp(name, numbered=True) self.plugins: List[Union[str, _PluggyPlugin]] = [] self._cwd_snapshot = CwdSnapshot() @@ -868,10 +869,6 @@ def copy_example(self, name=None) -> Path: :return: path to the copied directory (inside ``self.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") if example_dir is None: raise ValueError("pytester_example_dir is unset, can't copy examples") @@ -1480,6 +1477,13 @@ class Testdir: _pytester = attr.ib(type=Pytester) + def __attrs_post_init__(self): + self.test_tmproot = py.path.local( + self._pytester._tmp_path_factory.mktemp( + "tmp-" + self._pytester._name, numbered=True + ) + ) + @property def tmpdir(self) -> py.path.local: return py.path.local(self._pytester.path) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index bd3a1d0b720..2fd4d4f6e8f 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/test_pytester.py b/testing/test_pytester.py index d42db8b8347..fed201dafe5 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -803,3 +803,8 @@ def test_makefile_joins_absolute_path(testdir: Testdir) -> None: absfile = testdir.tmpdir / "absfile" 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) From 3eef01b61c54c8414bb4566a22b58c8581b56935 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Oct 2020 12:02:29 -0300 Subject: [PATCH 5/8] New code review suggestions: test_tmproot --- src/_pytest/pytester.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8562b175f3d..db8dcea0f45 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -661,7 +661,6 @@ def __init__( else: name = request.node.name self._name = name - self._tmp_path_factory = tmp_path_factory self._path: Path = tmp_path_factory.mktemp(name, numbered=True) self.plugins: List[Union[str, _PluggyPlugin]] = [] self._cwd_snapshot = CwdSnapshot() @@ -670,9 +669,10 @@ def __init__( self.chdir() self._request.addfinalizer(self._finalize) self._method = self._request.config.getoption("--runpytest") + self.test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) self._monkeypatch = mp = MonkeyPatch() - mp.setenv("PYTEST_DEBUG_TEMPROOT", str(tmp_path_factory.mktemp("tmproot"))) + 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. @@ -1475,19 +1475,16 @@ class Testdir: TimeoutExpired = Pytester.TimeoutExpired Session = Pytester.Session - _pytester = attr.ib(type=Pytester) - - def __attrs_post_init__(self): - self.test_tmproot = py.path.local( - self._pytester._tmp_path_factory.mktemp( - "tmp-" + self._pytester._name, numbered=True - ) - ) + _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 From 6f66fd8638074ce2d431ab9f22fa4ebae0ad1e97 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Oct 2020 12:03:47 -0300 Subject: [PATCH 6/8] Make test_tmp_root private in Pytester --- src/_pytest/pytester.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index db8dcea0f45..2ba6f3d2931 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -669,10 +669,10 @@ def __init__( self.chdir() self._request.addfinalizer(self._finalize) self._method = self._request.config.getoption("--runpytest") - self.test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) + self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True) self._monkeypatch = mp = MonkeyPatch() - mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self.test_tmproot)) + 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. @@ -1483,7 +1483,7 @@ def tmpdir(self) -> py.path.local: @property def test_tmproot(self) -> py.path.local: - return py.path.local(self._pytester.test_tmproot) + return py.path.local(self._pytester._test_tmproot) @property def request(self): From ecf523cffc79a655b79086211e7578187135eebb Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 11 Oct 2020 20:01:12 -0300 Subject: [PATCH 7/8] New type annotations --- src/_pytest/pytester.py | 95 ++++++++++++++++++++++++----------------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 2ba6f3d2931..00628cdd38f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -13,6 +13,7 @@ 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 @@ -58,10 +59,6 @@ import pexpect - PathLike = os.PathLike[str] -else: - PathLike = os.PathLike - IGNORE_PAM = [ # filenames added when obtaining details about the current user "/var/lib/sss/mc/passwd" @@ -727,10 +724,16 @@ def chdir(self) -> None: """ os.chdir(self.path) - def _makefile(self, ext: str, lines, files, encoding: str = "utf-8") -> Path: + 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: @@ -750,7 +753,7 @@ def to_text(s): assert ret is not None return ret - def makefile(self, ext: str, *args: str, **kwargs) -> Path: + def makefile(self, ext: str, *args: str, **kwargs: str) -> Path: r"""Create new file(s) in the test directory. :param str ext: @@ -774,20 +777,20 @@ def makefile(self, ext: str, *args: str, **kwargs) -> Path: """ return self._makefile(ext, args, kwargs) - def makeconftest(self, source) -> Path: + def makeconftest(self, source: str) -> Path: """Write a contest.py file with 'source' as contents.""" return self.makepyfile(conftest=source) - def makeini(self, source) -> Path: + 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) -> Path: + def makepyprojecttoml(self, source: str) -> Path: """Write a pyproject.toml file with 'source' as contents. .. versionadded:: 6.0 @@ -834,7 +837,9 @@ def test_something(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 @@ -862,7 +867,7 @@ def mkpydir(self, name: str) -> Path: p.joinpath("__init__.py").touch() return p - def copy_example(self, name=None) -> Path: + 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. @@ -889,7 +894,7 @@ def copy_example(self, name=None) -> Path: example_path = maybe_file else: raise LookupError( - f"{func_name} cant be found as module or package in {example_dir}" + f"{func_name} can't be found as module or package in {example_dir}" ) else: example_path = example_dir.joinpath(name) @@ -912,7 +917,7 @@ def copy_example(self, name=None) -> Path: Session = Session def getnode( - self, config: Config, arg: Union[PathLike, str] + self, config: Config, arg: Union[str, "os.PathLike[str]"] ) -> Optional[Union[Collector, Item]]: """Return the collection node of a file. @@ -930,7 +935,7 @@ def getnode( config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK) return res - def getpathnode(self, path: Union[PathLike, str]): + 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 @@ -959,7 +964,7 @@ def genitems(self, colitems: Sequence[Union[Item, Collector]]) -> List[Item]: 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 @@ -974,7 +979,7 @@ def runitem(self, source): 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 @@ -1003,7 +1008,10 @@ def inline_genitems(self, *args) -> Tuple[List[Item], HookRecorder]: 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. @@ -1072,7 +1080,9 @@ class reprec: # type: ignore 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) @@ -1114,26 +1124,30 @@ class reprec: # type: ignore 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.path.parent.joinpath("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 @@ -1156,7 +1170,7 @@ def parseconfig(self, *args) -> Config: 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 @@ -1166,7 +1180,7 @@ def parseconfigure(self, *args) -> Config: 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 @@ -1186,7 +1200,7 @@ def getitem(self, source, funcname: str = "test_func") -> Item: 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 @@ -1195,7 +1209,9 @@ def getitems(self, source) -> List[Item]: 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 @@ -1281,7 +1297,7 @@ def popen( def run( self, - *cmdargs: Union[str, PathLike], + *cmdargs: Union[str, "os.PathLike[str]"], timeout: Optional[float] = None, stdin=CLOSE_STDIN, ) -> RunResult: @@ -1290,11 +1306,11 @@ def run( Run a process using subprocess.Popen saving the stdout and stderr. :param cmdargs: - The sequence of arguments to pass to `subprocess.Popen()`, with ``Path`` - and ``py.path.local`` objects being converted to ``str`` automatically. + 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` + :py:class:`Testdir.TimeoutExpired`. :param stdin: Optional standard input. Bytes are being send, closing the pipe, otherwise it is passed through to ``popen``. @@ -1306,8 +1322,7 @@ def run( __tracebackhide__ = True cmdargs = tuple( - str(arg) if isinstance(arg, (py.path.local, Path)) else arg - for arg in cmdargs + os.fspath(arg) if isinstance(arg, os.PathLike) else arg for arg in cmdargs ) p1 = self.path.joinpath("stdout") p2 = self.path.joinpath("stderr") From d2b576c4e955032ab11f580e6d943c57b97f0129 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 11 Oct 2020 20:11:32 -0300 Subject: [PATCH 8/8] Fix small typos --- doc/en/reference.rst | 2 +- src/_pytest/pytester.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index b78092c2d90..6795b721c8f 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -511,7 +511,7 @@ pytester Provides a :class:`Pytester` 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 write tests, configuration files, and match against expected output. To use it, include in your topmost ``conftest.py`` file: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 00628cdd38f..b7a79b90299 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -626,7 +626,7 @@ class Pytester: 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. + the current working directory to ``path`` and environment variables during initialization. Attributes: