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