New pytester fixture (#7854)
This commit is contained in:
parent
cb578a918e
commit
69419cb700
|
@ -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.
|
|
@ -499,17 +499,21 @@ monkeypatch
|
|||
:members:
|
||||
|
||||
|
||||
.. fixture:: testdir
|
||||
.. fixture:: pytester
|
||||
|
||||
testdir
|
||||
~~~~~~~
|
||||
pytester
|
||||
~~~~~~~~
|
||||
|
||||
.. versionadded:: 6.2
|
||||
|
||||
.. currentmodule:: _pytest.pytester
|
||||
|
||||
This fixture provides a :class:`Testdir` instance useful for black-box testing of test files, making it ideal to
|
||||
test plugins.
|
||||
Provides a :class:`Pytester` instance that can be used to run and test pytest itself.
|
||||
|
||||
To use it, include in your top-most ``conftest.py`` file:
|
||||
It provides an empty directory where pytest can be executed in isolation, and contains facilities
|
||||
to write tests, configuration files, and match against expected output.
|
||||
|
||||
To use it, include in your topmost ``conftest.py`` file:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
|
@ -517,7 +521,7 @@ To use it, include in your top-most ``conftest.py`` file:
|
|||
|
||||
|
||||
|
||||
.. autoclass:: Testdir()
|
||||
.. autoclass:: Pytester()
|
||||
:members:
|
||||
|
||||
.. autoclass:: RunResult()
|
||||
|
@ -526,6 +530,15 @@ To use it, include in your top-most ``conftest.py`` file:
|
|||
.. autoclass:: LineMatcher()
|
||||
:members:
|
||||
|
||||
.. fixture:: testdir
|
||||
|
||||
testdir
|
||||
~~~~~~~
|
||||
|
||||
Identical to :fixture:`pytester`, but provides an instance whose methods return
|
||||
legacy ``py.path.local`` objects instead when applicable.
|
||||
|
||||
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
|
||||
|
||||
.. fixture:: recwarn
|
||||
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
"""(Disabled by default) support for testing pytest and pytest plugins."""
|
||||
import collections.abc
|
||||
import contextlib
|
||||
import gc
|
||||
import importlib
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
from fnmatch import fnmatch
|
||||
from io import StringIO
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Dict
|
||||
from typing import Generator
|
||||
|
@ -19,12 +22,14 @@ from typing import List
|
|||
from typing import Optional
|
||||
from typing import overload
|
||||
from typing import Sequence
|
||||
from typing import TextIO
|
||||
from typing import Tuple
|
||||
from typing import Type
|
||||
from typing import TYPE_CHECKING
|
||||
from typing import Union
|
||||
from weakref import WeakKeyDictionary
|
||||
|
||||
import attr
|
||||
import py
|
||||
from iniconfig import IniConfig
|
||||
|
||||
|
@ -47,7 +52,7 @@ from _pytest.pathlib import make_numbered_dir
|
|||
from _pytest.python import Module
|
||||
from _pytest.reports import CollectReport
|
||||
from _pytest.reports import TestReport
|
||||
from _pytest.tmpdir import TempdirFactory
|
||||
from _pytest.tmpdir import TempPathFactory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing_extensions import Literal
|
||||
|
@ -176,11 +181,11 @@ def _pytest(request: FixtureRequest) -> "PytestArg":
|
|||
|
||||
class PytestArg:
|
||||
def __init__(self, request: FixtureRequest) -> None:
|
||||
self.request = request
|
||||
self._request = request
|
||||
|
||||
def gethookrecorder(self, hook) -> "HookRecorder":
|
||||
hookrecorder = HookRecorder(hook._pm)
|
||||
self.request.addfinalizer(hookrecorder.finish_recording)
|
||||
self._request.addfinalizer(hookrecorder.finish_recording)
|
||||
return hookrecorder
|
||||
|
||||
|
||||
|
@ -430,13 +435,29 @@ def LineMatcher_fixture(request: FixtureRequest) -> Type["LineMatcher"]:
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def testdir(request: FixtureRequest, tmpdir_factory: TempdirFactory) -> "Testdir":
|
||||
"""A :class: `TestDir` instance, that can be used to run and test pytest itself.
|
||||
|
||||
It is particularly useful for testing plugins. It is similar to the `tmpdir` fixture
|
||||
but provides methods which aid in testing pytest itself.
|
||||
def pytester(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> "Pytester":
|
||||
"""
|
||||
return Testdir(request, tmpdir_factory)
|
||||
Facilities to write tests/configuration files, execute pytest in isolation, and match
|
||||
against expected output, perfect for black-box testing of pytest plugins.
|
||||
|
||||
It attempts to isolate the test run from external factors as much as possible, modifying
|
||||
the current working directory to ``path`` and environment variables during initialization.
|
||||
|
||||
It is particularly useful for testing plugins. It is similar to the :fixture:`tmp_path`
|
||||
fixture but provides methods which aid in testing pytest itself.
|
||||
"""
|
||||
return Pytester(request, tmp_path_factory)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def testdir(pytester: "Pytester") -> "Testdir":
|
||||
"""
|
||||
Identical to :fixture:`pytester`, and provides an instance whose methods return
|
||||
legacy ``py.path.local`` objects instead when applicable.
|
||||
|
||||
New code should avoid using :fixture:`testdir` in favor of :fixture:`pytester`.
|
||||
"""
|
||||
return Testdir(pytester)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -599,16 +620,17 @@ class SysPathsSnapshot:
|
|||
|
||||
|
||||
@final
|
||||
class Testdir:
|
||||
"""Temporary test directory with tools to test/run pytest itself.
|
||||
class Pytester:
|
||||
"""
|
||||
Facilities to write tests/configuration files, execute pytest in isolation, and match
|
||||
against expected output, perfect for black-box testing of pytest plugins.
|
||||
|
||||
This is based on the :fixture:`tmpdir` fixture but provides a number of methods
|
||||
which aid with testing pytest itself. Unless :py:meth:`chdir` is used all
|
||||
methods will use :py:attr:`tmpdir` as their current working directory.
|
||||
It attempts to isolate the test run from external factors as much as possible, modifying
|
||||
the current working directory to ``path`` and environment variables during initialization.
|
||||
|
||||
Attributes:
|
||||
|
||||
:ivar tmpdir: The :py:class:`py.path.local` instance of the temporary directory.
|
||||
:ivar Path path: temporary directory path used to create files/run tests from, etc.
|
||||
|
||||
:ivar plugins:
|
||||
A list of plugins to use with :py:meth:`parseconfig` and
|
||||
|
@ -624,8 +646,10 @@ class Testdir:
|
|||
class TimeoutExpired(Exception):
|
||||
pass
|
||||
|
||||
def __init__(self, request: FixtureRequest, tmpdir_factory: TempdirFactory) -> None:
|
||||
self.request = request
|
||||
def __init__(
|
||||
self, request: FixtureRequest, tmp_path_factory: TempPathFactory
|
||||
) -> None:
|
||||
self._request = request
|
||||
self._mod_collections: WeakKeyDictionary[
|
||||
Module, List[Union[Item, Collector]]
|
||||
] = (WeakKeyDictionary())
|
||||
|
@ -634,46 +658,49 @@ class Testdir:
|
|||
else:
|
||||
name = request.node.name
|
||||
self._name = name
|
||||
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
|
||||
self.test_tmproot = tmpdir_factory.mktemp("tmp-" + name, numbered=True)
|
||||
self._path: Path = tmp_path_factory.mktemp(name, numbered=True)
|
||||
self.plugins: List[Union[str, _PluggyPlugin]] = []
|
||||
self._cwd_snapshot = CwdSnapshot()
|
||||
self._sys_path_snapshot = SysPathsSnapshot()
|
||||
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
|
||||
self.chdir()
|
||||
self.request.addfinalizer(self.finalize)
|
||||
self._method = self.request.config.getoption("--runpytest")
|
||||
self._request.addfinalizer(self._finalize)
|
||||
self._method = self._request.config.getoption("--runpytest")
|
||||
self._test_tmproot = tmp_path_factory.mktemp(f"tmp-{name}", numbered=True)
|
||||
|
||||
mp = self.monkeypatch = MonkeyPatch()
|
||||
mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self.test_tmproot))
|
||||
self._monkeypatch = mp = MonkeyPatch()
|
||||
mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self._test_tmproot))
|
||||
# Ensure no unexpected caching via tox.
|
||||
mp.delenv("TOX_ENV_DIR", raising=False)
|
||||
# Discard outer pytest options.
|
||||
mp.delenv("PYTEST_ADDOPTS", raising=False)
|
||||
# Ensure no user config is used.
|
||||
tmphome = str(self.tmpdir)
|
||||
tmphome = str(self.path)
|
||||
mp.setenv("HOME", tmphome)
|
||||
mp.setenv("USERPROFILE", tmphome)
|
||||
# Do not use colors for inner runs by default.
|
||||
mp.setenv("PY_COLORS", "0")
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
"""Temporary directory where files are created and pytest is executed."""
|
||||
return self._path
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Testdir {self.tmpdir!r}>"
|
||||
return f"<Pytester {self.path!r}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.tmpdir)
|
||||
|
||||
def finalize(self) -> None:
|
||||
"""Clean up global state artifacts.
|
||||
def _finalize(self) -> None:
|
||||
"""
|
||||
Clean up global state artifacts.
|
||||
|
||||
Some methods modify the global interpreter state and this tries to
|
||||
clean this up. It does not remove the temporary directory however so
|
||||
clean this up. It does not remove the temporary directory however so
|
||||
it can be looked at after the test run has finished.
|
||||
"""
|
||||
self._sys_modules_snapshot.restore()
|
||||
self._sys_path_snapshot.restore()
|
||||
self._cwd_snapshot.restore()
|
||||
self.monkeypatch.undo()
|
||||
self._monkeypatch.undo()
|
||||
|
||||
def __take_sys_modules_snapshot(self) -> SysModulesSnapshot:
|
||||
# Some zope modules used by twisted-related tests keep internal state
|
||||
|
@ -687,7 +714,7 @@ class Testdir:
|
|||
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
|
||||
"""Create a new :py:class:`HookRecorder` for a PluginManager."""
|
||||
pluginmanager.reprec = reprec = HookRecorder(pluginmanager)
|
||||
self.request.addfinalizer(reprec.finish_recording)
|
||||
self._request.addfinalizer(reprec.finish_recording)
|
||||
return reprec
|
||||
|
||||
def chdir(self) -> None:
|
||||
|
@ -695,12 +722,18 @@ class Testdir:
|
|||
|
||||
This is done automatically upon instantiation.
|
||||
"""
|
||||
self.tmpdir.chdir()
|
||||
os.chdir(self.path)
|
||||
|
||||
def _makefile(self, ext: str, lines, files, encoding: str = "utf-8"):
|
||||
def _makefile(
|
||||
self,
|
||||
ext: str,
|
||||
lines: Sequence[Union[Any, bytes]],
|
||||
files: Dict[str, str],
|
||||
encoding: str = "utf-8",
|
||||
) -> Path:
|
||||
items = list(files.items())
|
||||
|
||||
def to_text(s):
|
||||
def to_text(s: Union[Any, bytes]) -> str:
|
||||
return s.decode(encoding) if isinstance(s, bytes) else str(s)
|
||||
|
||||
if lines:
|
||||
|
@ -710,17 +743,18 @@ class Testdir:
|
|||
|
||||
ret = None
|
||||
for basename, value in items:
|
||||
p = self.tmpdir.join(basename).new(ext=ext)
|
||||
p.dirpath().ensure_dir()
|
||||
p = self.path.joinpath(basename).with_suffix(ext)
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
source_ = Source(value)
|
||||
source = "\n".join(to_text(line) for line in source_.lines)
|
||||
p.write(source.strip().encode(encoding), "wb")
|
||||
p.write_text(source.strip(), encoding=encoding)
|
||||
if ret is None:
|
||||
ret = p
|
||||
assert ret is not None
|
||||
return ret
|
||||
|
||||
def makefile(self, ext: str, *args: str, **kwargs):
|
||||
r"""Create new file(s) in the testdir.
|
||||
def makefile(self, ext: str, *args: str, **kwargs: str) -> Path:
|
||||
r"""Create new file(s) in the test directory.
|
||||
|
||||
:param str ext:
|
||||
The extension the file(s) should use, including the dot, e.g. `.py`.
|
||||
|
@ -743,27 +777,27 @@ class Testdir:
|
|||
"""
|
||||
return self._makefile(ext, args, kwargs)
|
||||
|
||||
def makeconftest(self, source):
|
||||
def makeconftest(self, source: str) -> Path:
|
||||
"""Write a contest.py file with 'source' as contents."""
|
||||
return self.makepyfile(conftest=source)
|
||||
|
||||
def makeini(self, source):
|
||||
def makeini(self, source: str) -> Path:
|
||||
"""Write a tox.ini file with 'source' as contents."""
|
||||
return self.makefile(".ini", tox=source)
|
||||
|
||||
def getinicfg(self, source) -> IniConfig:
|
||||
def getinicfg(self, source: str) -> IniConfig:
|
||||
"""Return the pytest section from the tox.ini config file."""
|
||||
p = self.makeini(source)
|
||||
return IniConfig(p)["pytest"]
|
||||
|
||||
def makepyprojecttoml(self, source):
|
||||
def makepyprojecttoml(self, source: str) -> Path:
|
||||
"""Write a pyproject.toml file with 'source' as contents.
|
||||
|
||||
.. versionadded:: 6.0
|
||||
"""
|
||||
return self.makefile(".toml", pyproject=source)
|
||||
|
||||
def makepyfile(self, *args, **kwargs):
|
||||
def makepyfile(self, *args, **kwargs) -> Path:
|
||||
r"""Shortcut for .makefile() with a .py extension.
|
||||
|
||||
Defaults to the test name with a '.py' extension, e.g test_foobar.py, overwriting
|
||||
|
@ -783,7 +817,7 @@ class Testdir:
|
|||
"""
|
||||
return self._makefile(".py", args, kwargs)
|
||||
|
||||
def maketxtfile(self, *args, **kwargs):
|
||||
def maketxtfile(self, *args, **kwargs) -> Path:
|
||||
r"""Shortcut for .makefile() with a .txt extension.
|
||||
|
||||
Defaults to the test name with a '.txt' extension, e.g test_foobar.txt, overwriting
|
||||
|
@ -803,74 +837,77 @@ class Testdir:
|
|||
"""
|
||||
return self._makefile(".txt", args, kwargs)
|
||||
|
||||
def syspathinsert(self, path=None) -> None:
|
||||
def syspathinsert(
|
||||
self, path: Optional[Union[str, "os.PathLike[str]"]] = None
|
||||
) -> None:
|
||||
"""Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`.
|
||||
|
||||
This is undone automatically when this object dies at the end of each
|
||||
test.
|
||||
"""
|
||||
if path is None:
|
||||
path = self.tmpdir
|
||||
path = self.path
|
||||
|
||||
self.monkeypatch.syspath_prepend(str(path))
|
||||
self._monkeypatch.syspath_prepend(str(path))
|
||||
|
||||
def mkdir(self, name) -> py.path.local:
|
||||
def mkdir(self, name: str) -> Path:
|
||||
"""Create a new (sub)directory."""
|
||||
return self.tmpdir.mkdir(name)
|
||||
p = self.path / name
|
||||
p.mkdir()
|
||||
return p
|
||||
|
||||
def mkpydir(self, name) -> py.path.local:
|
||||
"""Create a new Python package.
|
||||
def mkpydir(self, name: str) -> Path:
|
||||
"""Create a new python package.
|
||||
|
||||
This creates a (sub)directory with an empty ``__init__.py`` file so it
|
||||
gets recognised as a Python package.
|
||||
"""
|
||||
p = self.mkdir(name)
|
||||
p.ensure("__init__.py")
|
||||
p = self.path / name
|
||||
p.mkdir()
|
||||
p.joinpath("__init__.py").touch()
|
||||
return p
|
||||
|
||||
def copy_example(self, name=None) -> py.path.local:
|
||||
def copy_example(self, name: Optional[str] = None) -> Path:
|
||||
"""Copy file from project's directory into the testdir.
|
||||
|
||||
:param str name: The name of the file to copy.
|
||||
:returns: Path to the copied directory (inside ``self.tmpdir``).
|
||||
"""
|
||||
import warnings
|
||||
from _pytest.warning_types import PYTESTER_COPY_EXAMPLE
|
||||
:return: path to the copied directory (inside ``self.path``).
|
||||
|
||||
warnings.warn(PYTESTER_COPY_EXAMPLE, stacklevel=2)
|
||||
example_dir = self.request.config.getini("pytester_example_dir")
|
||||
"""
|
||||
example_dir = self._request.config.getini("pytester_example_dir")
|
||||
if example_dir is None:
|
||||
raise ValueError("pytester_example_dir is unset, can't copy examples")
|
||||
example_dir = self.request.config.rootdir.join(example_dir)
|
||||
example_dir = Path(str(self._request.config.rootdir)) / example_dir
|
||||
|
||||
for extra_element in self.request.node.iter_markers("pytester_example_path"):
|
||||
for extra_element in self._request.node.iter_markers("pytester_example_path"):
|
||||
assert extra_element.args
|
||||
example_dir = example_dir.join(*extra_element.args)
|
||||
example_dir = example_dir.joinpath(*extra_element.args)
|
||||
|
||||
if name is None:
|
||||
func_name = self._name
|
||||
maybe_dir = example_dir / func_name
|
||||
maybe_file = example_dir / (func_name + ".py")
|
||||
|
||||
if maybe_dir.isdir():
|
||||
if maybe_dir.is_dir():
|
||||
example_path = maybe_dir
|
||||
elif maybe_file.isfile():
|
||||
elif maybe_file.is_file():
|
||||
example_path = maybe_file
|
||||
else:
|
||||
raise LookupError(
|
||||
"{} cant be found as module or package in {}".format(
|
||||
func_name, example_dir.bestrelpath(self.request.config.rootdir)
|
||||
)
|
||||
f"{func_name} can't be found as module or package in {example_dir}"
|
||||
)
|
||||
else:
|
||||
example_path = example_dir.join(name)
|
||||
example_path = example_dir.joinpath(name)
|
||||
|
||||
if example_path.isdir() and not example_path.join("__init__.py").isfile():
|
||||
example_path.copy(self.tmpdir)
|
||||
return self.tmpdir
|
||||
elif example_path.isfile():
|
||||
result = self.tmpdir.join(example_path.basename)
|
||||
example_path.copy(result)
|
||||
if example_path.is_dir() and not example_path.joinpath("__init__.py").is_file():
|
||||
# TODO: py.path.local.copy can copy files to existing directories,
|
||||
# while with shutil.copytree the destination directory cannot exist,
|
||||
# we will need to roll our own in order to drop py.path.local completely
|
||||
py.path.local(example_path).copy(py.path.local(self.path))
|
||||
return self.path
|
||||
elif example_path.is_file():
|
||||
result = self.path.joinpath(example_path.name)
|
||||
shutil.copy(example_path, result)
|
||||
return result
|
||||
else:
|
||||
raise LookupError(
|
||||
|
@ -879,7 +916,9 @@ class Testdir:
|
|||
|
||||
Session = Session
|
||||
|
||||
def getnode(self, config: Config, arg):
|
||||
def getnode(
|
||||
self, config: Config, arg: Union[str, "os.PathLike[str]"]
|
||||
) -> Optional[Union[Collector, Item]]:
|
||||
"""Return the collection node of a file.
|
||||
|
||||
:param _pytest.config.Config config:
|
||||
|
@ -896,7 +935,7 @@ class Testdir:
|
|||
config.hook.pytest_sessionfinish(session=session, exitstatus=ExitCode.OK)
|
||||
return res
|
||||
|
||||
def getpathnode(self, path):
|
||||
def getpathnode(self, path: Union[str, "os.PathLike[str]"]):
|
||||
"""Return the collection node of a file.
|
||||
|
||||
This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
|
||||
|
@ -904,6 +943,7 @@ class Testdir:
|
|||
|
||||
:param py.path.local path: Path to the file.
|
||||
"""
|
||||
path = py.path.local(path)
|
||||
config = self.parseconfigure(path)
|
||||
session = Session.from_config(config)
|
||||
x = session.fspath.bestrelpath(path)
|
||||
|
@ -924,7 +964,7 @@ class Testdir:
|
|||
result.extend(session.genitems(colitem))
|
||||
return result
|
||||
|
||||
def runitem(self, source):
|
||||
def runitem(self, source: str) -> Any:
|
||||
"""Run the "test_func" Item.
|
||||
|
||||
The calling test instance (class containing the test method) must
|
||||
|
@ -935,11 +975,11 @@ class Testdir:
|
|||
# used from runner functional tests
|
||||
item = self.getitem(source)
|
||||
# the test class where we are called from wants to provide the runner
|
||||
testclassinstance = self.request.instance
|
||||
testclassinstance = self._request.instance
|
||||
runner = testclassinstance.getrunner()
|
||||
return runner(item)
|
||||
|
||||
def inline_runsource(self, source, *cmdlineargs) -> HookRecorder:
|
||||
def inline_runsource(self, source: str, *cmdlineargs) -> HookRecorder:
|
||||
"""Run a test module in process using ``pytest.main()``.
|
||||
|
||||
This run writes "source" into a temporary file and runs
|
||||
|
@ -968,7 +1008,10 @@ class Testdir:
|
|||
return items, rec
|
||||
|
||||
def inline_run(
|
||||
self, *args, plugins=(), no_reraise_ctrlc: bool = False
|
||||
self,
|
||||
*args: Union[str, "os.PathLike[str]"],
|
||||
plugins=(),
|
||||
no_reraise_ctrlc: bool = False,
|
||||
) -> HookRecorder:
|
||||
"""Run ``pytest.main()`` in-process, returning a HookRecorder.
|
||||
|
||||
|
@ -1016,7 +1059,7 @@ class Testdir:
|
|||
rec.append(self.make_hook_recorder(config.pluginmanager))
|
||||
|
||||
plugins.append(Collect())
|
||||
ret = pytest.main(list(args), plugins=plugins)
|
||||
ret = pytest.main([str(x) for x in args], plugins=plugins)
|
||||
if len(rec) == 1:
|
||||
reprec = rec.pop()
|
||||
else:
|
||||
|
@ -1024,7 +1067,7 @@ class Testdir:
|
|||
class reprec: # type: ignore
|
||||
pass
|
||||
|
||||
reprec.ret = ret
|
||||
reprec.ret = ret # type: ignore
|
||||
|
||||
# Typically we reraise keyboard interrupts from the child run
|
||||
# because it's our user requesting interruption of the testing.
|
||||
|
@ -1037,7 +1080,9 @@ class Testdir:
|
|||
for finalizer in finalizers:
|
||||
finalizer()
|
||||
|
||||
def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
|
||||
def runpytest_inprocess(
|
||||
self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
|
||||
) -> RunResult:
|
||||
"""Return result of running pytest in-process, providing a similar
|
||||
interface to what self.runpytest() provides."""
|
||||
syspathinsert = kwargs.pop("syspathinsert", False)
|
||||
|
@ -1079,26 +1124,30 @@ class Testdir:
|
|||
res.reprec = reprec # type: ignore
|
||||
return res
|
||||
|
||||
def runpytest(self, *args, **kwargs) -> RunResult:
|
||||
def runpytest(
|
||||
self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
|
||||
) -> RunResult:
|
||||
"""Run pytest inline or in a subprocess, depending on the command line
|
||||
option "--runpytest" and return a :py:class:`RunResult`."""
|
||||
args = self._ensure_basetemp(args)
|
||||
new_args = self._ensure_basetemp(args)
|
||||
if self._method == "inprocess":
|
||||
return self.runpytest_inprocess(*args, **kwargs)
|
||||
return self.runpytest_inprocess(*new_args, **kwargs)
|
||||
elif self._method == "subprocess":
|
||||
return self.runpytest_subprocess(*args, **kwargs)
|
||||
return self.runpytest_subprocess(*new_args, **kwargs)
|
||||
raise RuntimeError(f"Unrecognized runpytest option: {self._method}")
|
||||
|
||||
def _ensure_basetemp(self, args):
|
||||
args = list(args)
|
||||
for x in args:
|
||||
def _ensure_basetemp(
|
||||
self, args: Sequence[Union[str, "os.PathLike[str]"]]
|
||||
) -> List[Union[str, "os.PathLike[str]"]]:
|
||||
new_args = list(args)
|
||||
for x in new_args:
|
||||
if str(x).startswith("--basetemp"):
|
||||
break
|
||||
else:
|
||||
args.append("--basetemp=%s" % self.tmpdir.dirpath("basetemp"))
|
||||
return args
|
||||
new_args.append("--basetemp=%s" % self.path.parent.joinpath("basetemp"))
|
||||
return new_args
|
||||
|
||||
def parseconfig(self, *args) -> Config:
|
||||
def parseconfig(self, *args: Union[str, "os.PathLike[str]"]) -> Config:
|
||||
"""Return a new pytest Config instance from given commandline args.
|
||||
|
||||
This invokes the pytest bootstrapping code in _pytest.config to create
|
||||
|
@ -1109,18 +1158,19 @@ class Testdir:
|
|||
If :py:attr:`plugins` has been populated they should be plugin modules
|
||||
to be registered with the PluginManager.
|
||||
"""
|
||||
args = self._ensure_basetemp(args)
|
||||
|
||||
import _pytest.config
|
||||
|
||||
config = _pytest.config._prepareconfig(args, self.plugins) # type: ignore[arg-type]
|
||||
new_args = self._ensure_basetemp(args)
|
||||
new_args = [str(x) for x in new_args]
|
||||
|
||||
config = _pytest.config._prepareconfig(new_args, self.plugins) # type: ignore[arg-type]
|
||||
# we don't know what the test will do with this half-setup config
|
||||
# object and thus we make sure it gets unconfigured properly in any
|
||||
# case (otherwise capturing could still be active, for example)
|
||||
self.request.addfinalizer(config._ensure_unconfigure)
|
||||
self._request.addfinalizer(config._ensure_unconfigure)
|
||||
return config
|
||||
|
||||
def parseconfigure(self, *args) -> Config:
|
||||
def parseconfigure(self, *args: Union[str, "os.PathLike[str]"]) -> Config:
|
||||
"""Return a new pytest configured Config instance.
|
||||
|
||||
Returns a new :py:class:`_pytest.config.Config` instance like
|
||||
|
@ -1130,7 +1180,7 @@ class Testdir:
|
|||
config._do_configure()
|
||||
return config
|
||||
|
||||
def getitem(self, source, funcname: str = "test_func") -> Item:
|
||||
def getitem(self, source: str, funcname: str = "test_func") -> Item:
|
||||
"""Return the test item for a test function.
|
||||
|
||||
Writes the source to a python file and runs pytest's collection on
|
||||
|
@ -1150,7 +1200,7 @@ class Testdir:
|
|||
funcname, source, items
|
||||
)
|
||||
|
||||
def getitems(self, source) -> List[Item]:
|
||||
def getitems(self, source: str) -> List[Item]:
|
||||
"""Return all test items collected from the module.
|
||||
|
||||
Writes the source to a Python file and runs pytest's collection on
|
||||
|
@ -1159,7 +1209,9 @@ class Testdir:
|
|||
modcol = self.getmodulecol(source)
|
||||
return self.genitems([modcol])
|
||||
|
||||
def getmodulecol(self, source, configargs=(), withinit: bool = False):
|
||||
def getmodulecol(
|
||||
self, source: Union[str, Path], configargs=(), *, withinit: bool = False
|
||||
):
|
||||
"""Return the module collection node for ``source``.
|
||||
|
||||
Writes ``source`` to a file using :py:meth:`makepyfile` and then
|
||||
|
@ -1177,10 +1229,10 @@ class Testdir:
|
|||
directory to ensure it is a package.
|
||||
"""
|
||||
if isinstance(source, Path):
|
||||
path = self.tmpdir.join(str(source))
|
||||
path = self.path.joinpath(source)
|
||||
assert not withinit, "not supported for paths"
|
||||
else:
|
||||
kw = {self._name: Source(source).strip()}
|
||||
kw = {self._name: str(source)}
|
||||
path = self.makepyfile(**kw)
|
||||
if withinit:
|
||||
self.makepyfile(__init__="#")
|
||||
|
@ -1208,8 +1260,8 @@ class Testdir:
|
|||
def popen(
|
||||
self,
|
||||
cmdargs,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
stdout: Union[int, TextIO] = subprocess.PIPE,
|
||||
stderr: Union[int, TextIO] = subprocess.PIPE,
|
||||
stdin=CLOSE_STDIN,
|
||||
**kw,
|
||||
):
|
||||
|
@ -1244,14 +1296,18 @@ class Testdir:
|
|||
return popen
|
||||
|
||||
def run(
|
||||
self, *cmdargs, timeout: Optional[float] = None, stdin=CLOSE_STDIN
|
||||
self,
|
||||
*cmdargs: Union[str, "os.PathLike[str]"],
|
||||
timeout: Optional[float] = None,
|
||||
stdin=CLOSE_STDIN,
|
||||
) -> RunResult:
|
||||
"""Run a command with arguments.
|
||||
|
||||
Run a process using subprocess.Popen saving the stdout and stderr.
|
||||
|
||||
:param args:
|
||||
The sequence of arguments to pass to `subprocess.Popen()`.
|
||||
:param cmdargs:
|
||||
The sequence of arguments to pass to `subprocess.Popen()`, with path-like objects
|
||||
being converted to ``str`` automatically.
|
||||
:param timeout:
|
||||
The period in seconds after which to timeout and raise
|
||||
:py:class:`Testdir.TimeoutExpired`.
|
||||
|
@ -1266,15 +1322,14 @@ class Testdir:
|
|||
__tracebackhide__ = True
|
||||
|
||||
cmdargs = tuple(
|
||||
str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs
|
||||
os.fspath(arg) if isinstance(arg, os.PathLike) else arg for arg in cmdargs
|
||||
)
|
||||
p1 = self.tmpdir.join("stdout")
|
||||
p2 = self.tmpdir.join("stderr")
|
||||
p1 = self.path.joinpath("stdout")
|
||||
p2 = self.path.joinpath("stderr")
|
||||
print("running:", *cmdargs)
|
||||
print(" in:", py.path.local())
|
||||
f1 = open(str(p1), "w", encoding="utf8")
|
||||
f2 = open(str(p2), "w", encoding="utf8")
|
||||
try:
|
||||
print(" in:", Path.cwd())
|
||||
|
||||
with p1.open("w", encoding="utf8") as f1, p2.open("w", encoding="utf8") as f2:
|
||||
now = timing.time()
|
||||
popen = self.popen(
|
||||
cmdargs,
|
||||
|
@ -1305,23 +1360,16 @@ class Testdir:
|
|||
ret = popen.wait(timeout)
|
||||
except subprocess.TimeoutExpired:
|
||||
handle_timeout()
|
||||
finally:
|
||||
f1.close()
|
||||
f2.close()
|
||||
f1 = open(str(p1), encoding="utf8")
|
||||
f2 = open(str(p2), encoding="utf8")
|
||||
try:
|
||||
|
||||
with p1.open(encoding="utf8") as f1, p2.open(encoding="utf8") as f2:
|
||||
out = f1.read().splitlines()
|
||||
err = f2.read().splitlines()
|
||||
finally:
|
||||
f1.close()
|
||||
f2.close()
|
||||
|
||||
self._dump_lines(out, sys.stdout)
|
||||
self._dump_lines(err, sys.stderr)
|
||||
try:
|
||||
|
||||
with contextlib.suppress(ValueError):
|
||||
ret = ExitCode(ret)
|
||||
except ValueError:
|
||||
pass
|
||||
return RunResult(ret, out, err, timing.time() - now)
|
||||
|
||||
def _dump_lines(self, lines, fp):
|
||||
|
@ -1366,7 +1414,7 @@ class Testdir:
|
|||
:rtype: RunResult
|
||||
"""
|
||||
__tracebackhide__ = True
|
||||
p = make_numbered_dir(root=Path(str(self.tmpdir)), prefix="runpytest-")
|
||||
p = make_numbered_dir(root=self.path, prefix="runpytest-")
|
||||
args = ("--basetemp=%s" % p,) + args
|
||||
plugins = [x for x in self.plugins if isinstance(x, str)]
|
||||
if plugins:
|
||||
|
@ -1384,7 +1432,8 @@ class Testdir:
|
|||
|
||||
The pexpect child is returned.
|
||||
"""
|
||||
basetemp = self.tmpdir.mkdir("temp-pexpect")
|
||||
basetemp = self.path / "temp-pexpect"
|
||||
basetemp.mkdir()
|
||||
invoke = " ".join(map(str, self._getpytestargs()))
|
||||
cmd = f"{invoke} --basetemp={basetemp} {string}"
|
||||
return self.spawn(cmd, expect_timeout=expect_timeout)
|
||||
|
@ -1399,10 +1448,10 @@ class Testdir:
|
|||
pytest.skip("pypy-64 bit not supported")
|
||||
if not hasattr(pexpect, "spawn"):
|
||||
pytest.skip("pexpect.spawn not available")
|
||||
logfile = self.tmpdir.join("spawn.out").open("wb")
|
||||
logfile = self.path.joinpath("spawn.out").open("wb")
|
||||
|
||||
child = pexpect.spawn(cmd, logfile=logfile)
|
||||
self.request.addfinalizer(logfile.close)
|
||||
self._request.addfinalizer(logfile.close)
|
||||
child.timeout = expect_timeout
|
||||
return child
|
||||
|
||||
|
@ -1425,6 +1474,178 @@ class LineComp:
|
|||
LineMatcher(lines1).fnmatch_lines(lines2)
|
||||
|
||||
|
||||
@final
|
||||
@attr.s(repr=False, str=False)
|
||||
class Testdir:
|
||||
"""
|
||||
Similar to :class:`Pytester`, but this class works with legacy py.path.local objects instead.
|
||||
|
||||
All methods just forward to an internal :class:`Pytester` instance, converting results
|
||||
to `py.path.local` objects as necessary.
|
||||
"""
|
||||
|
||||
__test__ = False
|
||||
|
||||
CLOSE_STDIN = Pytester.CLOSE_STDIN
|
||||
TimeoutExpired = Pytester.TimeoutExpired
|
||||
Session = Pytester.Session
|
||||
|
||||
_pytester: Pytester = attr.ib()
|
||||
|
||||
@property
|
||||
def tmpdir(self) -> py.path.local:
|
||||
return py.path.local(self._pytester.path)
|
||||
|
||||
@property
|
||||
def test_tmproot(self) -> py.path.local:
|
||||
return py.path.local(self._pytester._test_tmproot)
|
||||
|
||||
@property
|
||||
def request(self):
|
||||
return self._pytester._request
|
||||
|
||||
@property
|
||||
def plugins(self):
|
||||
return self._pytester.plugins
|
||||
|
||||
@plugins.setter
|
||||
def plugins(self, plugins):
|
||||
self._pytester.plugins = plugins
|
||||
|
||||
@property
|
||||
def monkeypatch(self) -> MonkeyPatch:
|
||||
return self._pytester._monkeypatch
|
||||
|
||||
def make_hook_recorder(self, pluginmanager) -> HookRecorder:
|
||||
return self._pytester.make_hook_recorder(pluginmanager)
|
||||
|
||||
def chdir(self) -> None:
|
||||
return self._pytester.chdir()
|
||||
|
||||
def finalize(self) -> None:
|
||||
return self._pytester._finalize()
|
||||
|
||||
def makefile(self, ext, *args, **kwargs) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.makefile(ext, *args, **kwargs)))
|
||||
|
||||
def makeconftest(self, source) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.makeconftest(source)))
|
||||
|
||||
def makeini(self, source) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.makeini(source)))
|
||||
|
||||
def getinicfg(self, source) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.getinicfg(source)))
|
||||
|
||||
def makepyprojecttoml(self, source) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.makepyprojecttoml(source)))
|
||||
|
||||
def makepyfile(self, *args, **kwargs) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.makepyfile(*args, **kwargs)))
|
||||
|
||||
def maketxtfile(self, *args, **kwargs) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.maketxtfile(*args, **kwargs)))
|
||||
|
||||
def syspathinsert(self, path=None) -> None:
|
||||
return self._pytester.syspathinsert(path)
|
||||
|
||||
def mkdir(self, name) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.mkdir(name)))
|
||||
|
||||
def mkpydir(self, name) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.mkpydir(name)))
|
||||
|
||||
def copy_example(self, name=None) -> py.path.local:
|
||||
return py.path.local(str(self._pytester.copy_example(name)))
|
||||
|
||||
def getnode(self, config: Config, arg) -> Optional[Union[Item, Collector]]:
|
||||
return self._pytester.getnode(config, arg)
|
||||
|
||||
def getpathnode(self, path):
|
||||
return self._pytester.getpathnode(path)
|
||||
|
||||
def genitems(self, colitems: List[Union[Item, Collector]]) -> List[Item]:
|
||||
return self._pytester.genitems(colitems)
|
||||
|
||||
def runitem(self, source):
|
||||
return self._pytester.runitem(source)
|
||||
|
||||
def inline_runsource(self, source, *cmdlineargs):
|
||||
return self._pytester.inline_runsource(source, *cmdlineargs)
|
||||
|
||||
def inline_genitems(self, *args):
|
||||
return self._pytester.inline_genitems(*args)
|
||||
|
||||
def inline_run(self, *args, plugins=(), no_reraise_ctrlc: bool = False):
|
||||
return self._pytester.inline_run(
|
||||
*args, plugins=plugins, no_reraise_ctrlc=no_reraise_ctrlc
|
||||
)
|
||||
|
||||
def runpytest_inprocess(self, *args, **kwargs) -> RunResult:
|
||||
return self._pytester.runpytest_inprocess(*args, **kwargs)
|
||||
|
||||
def runpytest(self, *args, **kwargs) -> RunResult:
|
||||
return self._pytester.runpytest(*args, **kwargs)
|
||||
|
||||
def parseconfig(self, *args) -> Config:
|
||||
return self._pytester.parseconfig(*args)
|
||||
|
||||
def parseconfigure(self, *args) -> Config:
|
||||
return self._pytester.parseconfigure(*args)
|
||||
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
return self._pytester.getitem(source, funcname)
|
||||
|
||||
def getitems(self, source):
|
||||
return self._pytester.getitems(source)
|
||||
|
||||
def getmodulecol(self, source, configargs=(), withinit=False):
|
||||
return self._pytester.getmodulecol(
|
||||
source, configargs=configargs, withinit=withinit
|
||||
)
|
||||
|
||||
def collect_by_name(
|
||||
self, modcol: Module, name: str
|
||||
) -> Optional[Union[Item, Collector]]:
|
||||
return self._pytester.collect_by_name(modcol, name)
|
||||
|
||||
def popen(
|
||||
self,
|
||||
cmdargs,
|
||||
stdout: Union[int, TextIO] = subprocess.PIPE,
|
||||
stderr: Union[int, TextIO] = subprocess.PIPE,
|
||||
stdin=CLOSE_STDIN,
|
||||
**kw,
|
||||
):
|
||||
return self._pytester.popen(cmdargs, stdout, stderr, stdin, **kw)
|
||||
|
||||
def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult:
|
||||
return self._pytester.run(*cmdargs, timeout=timeout, stdin=stdin)
|
||||
|
||||
def runpython(self, script) -> RunResult:
|
||||
return self._pytester.runpython(script)
|
||||
|
||||
def runpython_c(self, command):
|
||||
return self._pytester.runpython_c(command)
|
||||
|
||||
def runpytest_subprocess(self, *args, timeout=None) -> RunResult:
|
||||
return self._pytester.runpytest_subprocess(*args, timeout=timeout)
|
||||
|
||||
def spawn_pytest(
|
||||
self, string: str, expect_timeout: float = 10.0
|
||||
) -> "pexpect.spawn":
|
||||
return self._pytester.spawn_pytest(string, expect_timeout=expect_timeout)
|
||||
|
||||
def spawn(self, cmd: str, expect_timeout: float = 10.0) -> "pexpect.spawn":
|
||||
return self._pytester.spawn(cmd, expect_timeout=expect_timeout)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<Testdir {self.tmpdir!r}>"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.tmpdir)
|
||||
|
||||
|
||||
class LineMatcher:
|
||||
"""Flexible matching of text.
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -9,7 +9,7 @@ import pytest
|
|||
from _pytest.compat import importlib_metadata
|
||||
from _pytest.config import ExitCode
|
||||
from _pytest.pathlib import symlink_or_skip
|
||||
from _pytest.pytester import Testdir
|
||||
from _pytest.pytester import Pytester
|
||||
|
||||
|
||||
def prepend_pythonpath(*dirs):
|
||||
|
@ -1276,14 +1276,14 @@ def test_tee_stdio_captures_and_live_prints(testdir):
|
|||
sys.platform == "win32",
|
||||
reason="Windows raises `OSError: [Errno 22] Invalid argument` instead",
|
||||
)
|
||||
def test_no_brokenpipeerror_message(testdir: Testdir) -> None:
|
||||
def test_no_brokenpipeerror_message(pytester: Pytester) -> None:
|
||||
"""Ensure that the broken pipe error message is supressed.
|
||||
|
||||
In some Python versions, it reaches sys.unraisablehook, in others
|
||||
a BrokenPipeError exception is propagated, but either way it prints
|
||||
to stderr on shutdown, so checking nothing is printed is enough.
|
||||
"""
|
||||
popen = testdir.popen((*testdir._getpytestargs(), "--help"))
|
||||
popen = pytester.popen((*pytester._getpytestargs(), "--help"))
|
||||
popen.stdout.close()
|
||||
ret = popen.wait()
|
||||
assert popen.stderr.read() == b""
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -801,9 +801,10 @@ def test_parse_summary_line_always_plural():
|
|||
|
||||
def test_makefile_joins_absolute_path(testdir: Testdir) -> None:
|
||||
absfile = testdir.tmpdir / "absfile"
|
||||
if sys.platform == "win32":
|
||||
with pytest.raises(OSError):
|
||||
testdir.makepyfile(**{str(absfile): ""})
|
||||
else:
|
||||
p1 = testdir.makepyfile(**{str(absfile): ""})
|
||||
assert str(p1) == (testdir.tmpdir / absfile) + ".py"
|
||||
p1 = testdir.makepyfile(**{str(absfile): ""})
|
||||
assert str(p1) == str(testdir.tmpdir / "absfile.py")
|
||||
|
||||
|
||||
def test_testtmproot(testdir):
|
||||
"""Check test_tmproot is a py.path attribute for backward compatibility."""
|
||||
assert testdir.test_tmproot.check(dir=1)
|
||||
|
|
Loading…
Reference in New Issue