Merge pull request #9230 from bluetech/pytester-transitive-public
Export pytester transitive types
This commit is contained in:
commit
3c5c5feb04
|
@ -8,5 +8,6 @@ Directly constructing the following classes is now deprecated:
|
||||||
- ``_pytest._code.ExceptionInfo``
|
- ``_pytest._code.ExceptionInfo``
|
||||||
- ``_pytest.config.argparsing.Parser``
|
- ``_pytest.config.argparsing.Parser``
|
||||||
- ``_pytest.config.argparsing.OptionGroup``
|
- ``_pytest.config.argparsing.OptionGroup``
|
||||||
|
- ``_pytest.pytester.HookRecorder``
|
||||||
|
|
||||||
These have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.0.0.
|
These constructors have always been considered private, but now issue a deprecation warning, which may become a hard error in pytest 8.
|
||||||
|
|
|
@ -12,8 +12,12 @@ The newly-exported types are:
|
||||||
- ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo <pytest.ExceptionInfo>` type returned from :func:`pytest.raises` and passed to various hooks.
|
- ``pytest.ExceptionInfo`` for the :class:`ExceptionInfo <pytest.ExceptionInfo>` type returned from :func:`pytest.raises` and passed to various hooks.
|
||||||
- ``pytest.Parser`` for the :class:`Parser <pytest.Parser>` type passed to the :func:`pytest_addoption <pytest.hookspec.pytest_addoption>` hook.
|
- ``pytest.Parser`` for the :class:`Parser <pytest.Parser>` type passed to the :func:`pytest_addoption <pytest.hookspec.pytest_addoption>` hook.
|
||||||
- ``pytest.OptionGroup`` for the :class:`OptionGroup <pytest.OptionGroup>` type returned from the :func:`parser.addgroup <pytest.Parser.getgroup>` method.
|
- ``pytest.OptionGroup`` for the :class:`OptionGroup <pytest.OptionGroup>` type returned from the :func:`parser.addgroup <pytest.Parser.getgroup>` method.
|
||||||
|
- ``pytest.HookRecorder`` for the :class:`HookRecorder <pytest.HookRecorder>` type returned from :class:`~pytest.Pytester`.
|
||||||
|
- ``pytest.RecordedHookCall`` for the :class:`RecordedHookCall <pytest.HookRecorder>` type returned from :class:`~pytest.HookRecorder`.
|
||||||
|
- ``pytest.RunResult`` for the :class:`RunResult <pytest.RunResult>` type returned from :class:`~pytest.Pytester`.
|
||||||
|
- ``pytest.LineMatcher`` for the :class:`LineMatcher <pytest.RunResult>` type used in :class:`~pytest.RunResult` and others.
|
||||||
|
|
||||||
Constructing them directly is not supported; they are only meant for use in type annotations.
|
Constructing most of them directly is not supported; they are only meant for use in type annotations.
|
||||||
Doing so will emit a deprecation warning, and may become a hard-error in pytest 8.0.
|
Doing so will emit a deprecation warning, and may become a hard-error in pytest 8.0.
|
||||||
|
|
||||||
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.
|
Subclassing them is also not supported. This is not currently enforced at runtime, but is detected by type-checkers such as mypy.
|
||||||
|
|
|
@ -558,14 +558,17 @@ To use it, include in your topmost ``conftest.py`` file:
|
||||||
.. autoclass:: pytest.Pytester()
|
.. autoclass:: pytest.Pytester()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: _pytest.pytester.RunResult()
|
.. autoclass:: pytest.RunResult()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. autoclass:: _pytest.pytester.LineMatcher()
|
.. autoclass:: pytest.LineMatcher()
|
||||||
:members:
|
:members:
|
||||||
:special-members: __str__
|
:special-members: __str__
|
||||||
|
|
||||||
.. autoclass:: _pytest.pytester.HookRecorder()
|
.. autoclass:: pytest.HookRecorder()
|
||||||
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: pytest.RecordedHookCall()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
.. fixture:: testdir
|
.. fixture:: testdir
|
||||||
|
|
|
@ -214,7 +214,20 @@ def get_public_names(values: Iterable[str]) -> List[str]:
|
||||||
return [x for x in values if x[0] != "_"]
|
return [x for x in values if x[0] != "_"]
|
||||||
|
|
||||||
|
|
||||||
class ParsedCall:
|
@final
|
||||||
|
class RecordedHookCall:
|
||||||
|
"""A recorded call to a hook.
|
||||||
|
|
||||||
|
The arguments to the hook call are set as attributes.
|
||||||
|
For example:
|
||||||
|
|
||||||
|
.. code-block:: python
|
||||||
|
|
||||||
|
calls = hook_recorder.getcalls("pytest_runtest_setup")
|
||||||
|
# Suppose pytest_runtest_setup was called once with `item=an_item`.
|
||||||
|
assert calls[0].item is an_item
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, name: str, kwargs) -> None:
|
def __init__(self, name: str, kwargs) -> None:
|
||||||
self.__dict__.update(kwargs)
|
self.__dict__.update(kwargs)
|
||||||
self._name = name
|
self._name = name
|
||||||
|
@ -222,7 +235,7 @@ class ParsedCall:
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
d = self.__dict__.copy()
|
d = self.__dict__.copy()
|
||||||
del d["_name"]
|
del d["_name"]
|
||||||
return f"<ParsedCall {self._name!r}(**{d!r})>"
|
return f"<RecordedHookCall {self._name!r}(**{d!r})>"
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
# The class has undetermined attributes, this tells mypy about it.
|
# The class has undetermined attributes, this tells mypy about it.
|
||||||
|
@ -230,20 +243,27 @@ class ParsedCall:
|
||||||
...
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
class HookRecorder:
|
class HookRecorder:
|
||||||
"""Record all hooks called in a plugin manager.
|
"""Record all hooks called in a plugin manager.
|
||||||
|
|
||||||
|
Hook recorders are created by :class:`Pytester`.
|
||||||
|
|
||||||
This wraps all the hook calls in the plugin manager, recording each call
|
This wraps all the hook calls in the plugin manager, recording each call
|
||||||
before propagating the normal calls.
|
before propagating the normal calls.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, pluginmanager: PytestPluginManager) -> None:
|
def __init__(
|
||||||
|
self, pluginmanager: PytestPluginManager, *, _ispytest: bool = False
|
||||||
|
) -> None:
|
||||||
|
check_ispytest(_ispytest)
|
||||||
|
|
||||||
self._pluginmanager = pluginmanager
|
self._pluginmanager = pluginmanager
|
||||||
self.calls: List[ParsedCall] = []
|
self.calls: List[RecordedHookCall] = []
|
||||||
self.ret: Optional[Union[int, ExitCode]] = None
|
self.ret: Optional[Union[int, ExitCode]] = None
|
||||||
|
|
||||||
def before(hook_name: str, hook_impls, kwargs) -> None:
|
def before(hook_name: str, hook_impls, kwargs) -> None:
|
||||||
self.calls.append(ParsedCall(hook_name, kwargs))
|
self.calls.append(RecordedHookCall(hook_name, kwargs))
|
||||||
|
|
||||||
def after(outcome, hook_name: str, hook_impls, kwargs) -> None:
|
def after(outcome, hook_name: str, hook_impls, kwargs) -> None:
|
||||||
pass
|
pass
|
||||||
|
@ -253,7 +273,8 @@ class HookRecorder:
|
||||||
def finish_recording(self) -> None:
|
def finish_recording(self) -> None:
|
||||||
self._undo_wrapping()
|
self._undo_wrapping()
|
||||||
|
|
||||||
def getcalls(self, names: Union[str, Iterable[str]]) -> List[ParsedCall]:
|
def getcalls(self, names: Union[str, Iterable[str]]) -> List[RecordedHookCall]:
|
||||||
|
"""Get all recorded calls to hooks with the given names (or name)."""
|
||||||
if isinstance(names, str):
|
if isinstance(names, str):
|
||||||
names = names.split()
|
names = names.split()
|
||||||
return [call for call in self.calls if call._name in names]
|
return [call for call in self.calls if call._name in names]
|
||||||
|
@ -279,7 +300,7 @@ class HookRecorder:
|
||||||
else:
|
else:
|
||||||
fail(f"could not find {name!r} check {check!r}")
|
fail(f"could not find {name!r} check {check!r}")
|
||||||
|
|
||||||
def popcall(self, name: str) -> ParsedCall:
|
def popcall(self, name: str) -> RecordedHookCall:
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
for i, call in enumerate(self.calls):
|
for i, call in enumerate(self.calls):
|
||||||
if call._name == name:
|
if call._name == name:
|
||||||
|
@ -289,7 +310,7 @@ class HookRecorder:
|
||||||
lines.extend([" %s" % x for x in self.calls])
|
lines.extend([" %s" % x for x in self.calls])
|
||||||
fail("\n".join(lines))
|
fail("\n".join(lines))
|
||||||
|
|
||||||
def getcall(self, name: str) -> ParsedCall:
|
def getcall(self, name: str) -> RecordedHookCall:
|
||||||
values = self.getcalls(name)
|
values = self.getcalls(name)
|
||||||
assert len(values) == 1, (name, values)
|
assert len(values) == 1, (name, values)
|
||||||
return values[0]
|
return values[0]
|
||||||
|
@ -507,8 +528,9 @@ rex_session_duration = re.compile(r"\d+\.\d\ds")
|
||||||
rex_outcome = re.compile(r"(\d+) (\w+)")
|
rex_outcome = re.compile(r"(\d+) (\w+)")
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
class RunResult:
|
class RunResult:
|
||||||
"""The result of running a command."""
|
"""The result of running a command from :class:`~pytest.Pytester`."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
|
@ -527,13 +549,13 @@ class RunResult:
|
||||||
self.errlines = errlines
|
self.errlines = errlines
|
||||||
"""List of lines captured from stderr."""
|
"""List of lines captured from stderr."""
|
||||||
self.stdout = LineMatcher(outlines)
|
self.stdout = LineMatcher(outlines)
|
||||||
""":class:`LineMatcher` of stdout.
|
""":class:`~pytest.LineMatcher` of stdout.
|
||||||
|
|
||||||
Use e.g. :func:`str(stdout) <LineMatcher.__str__()>` to reconstruct stdout, or the commonly used
|
Use e.g. :func:`str(stdout) <pytest.LineMatcher.__str__()>` to reconstruct stdout, or the commonly used
|
||||||
:func:`stdout.fnmatch_lines() <LineMatcher.fnmatch_lines()>` method.
|
:func:`stdout.fnmatch_lines() <pytest.LineMatcher.fnmatch_lines()>` method.
|
||||||
"""
|
"""
|
||||||
self.stderr = LineMatcher(errlines)
|
self.stderr = LineMatcher(errlines)
|
||||||
""":class:`LineMatcher` of stderr."""
|
""":class:`~pytest.LineMatcher` of stderr."""
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
"""Duration in seconds."""
|
"""Duration in seconds."""
|
||||||
|
|
||||||
|
@ -741,7 +763,7 @@ class Pytester:
|
||||||
|
|
||||||
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, _ispytest=True)
|
||||||
self._request.addfinalizer(reprec.finish_recording)
|
self._request.addfinalizer(reprec.finish_recording)
|
||||||
return reprec
|
return reprec
|
||||||
|
|
||||||
|
@ -950,8 +972,6 @@ class Pytester:
|
||||||
f'example "{example_path}" is not found as a file or directory'
|
f'example "{example_path}" is not found as a file or directory'
|
||||||
)
|
)
|
||||||
|
|
||||||
Session = Session
|
|
||||||
|
|
||||||
def getnode(
|
def getnode(
|
||||||
self, config: Config, arg: Union[str, "os.PathLike[str]"]
|
self, config: Config, arg: Union[str, "os.PathLike[str]"]
|
||||||
) -> Optional[Union[Collector, Item]]:
|
) -> Optional[Union[Collector, Item]]:
|
||||||
|
@ -1023,10 +1043,7 @@ class Pytester:
|
||||||
for the result.
|
for the result.
|
||||||
|
|
||||||
:param source: The source code of the test module.
|
:param source: The source code of the test module.
|
||||||
|
|
||||||
:param cmdlineargs: Any extra command line arguments to use.
|
:param cmdlineargs: Any extra command line arguments to use.
|
||||||
|
|
||||||
:returns: :py:class:`HookRecorder` instance of the result.
|
|
||||||
"""
|
"""
|
||||||
p = self.makepyfile(source)
|
p = self.makepyfile(source)
|
||||||
values = list(cmdlineargs) + [p]
|
values = list(cmdlineargs) + [p]
|
||||||
|
@ -1064,8 +1081,6 @@ class Pytester:
|
||||||
:param no_reraise_ctrlc:
|
:param no_reraise_ctrlc:
|
||||||
Typically we reraise keyboard interrupts from the child run. If
|
Typically we reraise keyboard interrupts from the child run. If
|
||||||
True, the KeyboardInterrupt exception is captured.
|
True, the KeyboardInterrupt exception is captured.
|
||||||
|
|
||||||
:returns: A :py:class:`HookRecorder` instance.
|
|
||||||
"""
|
"""
|
||||||
# (maybe a cpython bug?) the importlib cache sometimes isn't updated
|
# (maybe a cpython bug?) the importlib cache sometimes isn't updated
|
||||||
# properly between file creation and inline_run (especially if imports
|
# properly between file creation and inline_run (especially if imports
|
||||||
|
@ -1164,7 +1179,7 @@ class Pytester:
|
||||||
self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
|
self, *args: Union[str, "os.PathLike[str]"], **kwargs: Any
|
||||||
) -> RunResult:
|
) -> 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:`~pytest.RunResult`."""
|
||||||
new_args = self._ensure_basetemp(args)
|
new_args = self._ensure_basetemp(args)
|
||||||
if self._method == "inprocess":
|
if self._method == "inprocess":
|
||||||
return self.runpytest_inprocess(*new_args, **kwargs)
|
return self.runpytest_inprocess(*new_args, **kwargs)
|
||||||
|
@ -1506,7 +1521,7 @@ class LineComp:
|
||||||
def assert_contains_lines(self, lines2: Sequence[str]) -> None:
|
def assert_contains_lines(self, lines2: Sequence[str]) -> None:
|
||||||
"""Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value.
|
"""Assert that ``lines2`` are contained (linearly) in :attr:`stringio`'s value.
|
||||||
|
|
||||||
Lines are matched using :func:`LineMatcher.fnmatch_lines`.
|
Lines are matched using :func:`LineMatcher.fnmatch_lines <pytest.LineMatcher.fnmatch_lines>`.
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
val = self.stringio.getvalue()
|
val = self.stringio.getvalue()
|
||||||
|
@ -1529,7 +1544,6 @@ class Testdir:
|
||||||
|
|
||||||
CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN
|
CLOSE_STDIN: "Final" = Pytester.CLOSE_STDIN
|
||||||
TimeoutExpired: "Final" = Pytester.TimeoutExpired
|
TimeoutExpired: "Final" = Pytester.TimeoutExpired
|
||||||
Session: "Final" = Pytester.Session
|
|
||||||
|
|
||||||
def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
|
def __init__(self, pytester: Pytester, *, _ispytest: bool = False) -> None:
|
||||||
check_ispytest(_ispytest)
|
check_ispytest(_ispytest)
|
||||||
|
@ -1734,6 +1748,7 @@ class Testdir:
|
||||||
return str(self.tmpdir)
|
return str(self.tmpdir)
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
class LineMatcher:
|
class LineMatcher:
|
||||||
"""Flexible matching of text.
|
"""Flexible matching of text.
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,11 @@ from _pytest.outcomes import fail
|
||||||
from _pytest.outcomes import importorskip
|
from _pytest.outcomes import importorskip
|
||||||
from _pytest.outcomes import skip
|
from _pytest.outcomes import skip
|
||||||
from _pytest.outcomes import xfail
|
from _pytest.outcomes import xfail
|
||||||
|
from _pytest.pytester import HookRecorder
|
||||||
|
from _pytest.pytester import LineMatcher
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
|
from _pytest.pytester import RecordedHookCall
|
||||||
|
from _pytest.pytester import RunResult
|
||||||
from _pytest.pytester import Testdir
|
from _pytest.pytester import Testdir
|
||||||
from _pytest.python import Class
|
from _pytest.python import Class
|
||||||
from _pytest.python import Function
|
from _pytest.python import Function
|
||||||
|
@ -98,10 +102,12 @@ __all__ = [
|
||||||
"freeze_includes",
|
"freeze_includes",
|
||||||
"Function",
|
"Function",
|
||||||
"hookimpl",
|
"hookimpl",
|
||||||
|
"HookRecorder",
|
||||||
"hookspec",
|
"hookspec",
|
||||||
"importorskip",
|
"importorskip",
|
||||||
"Instance",
|
"Instance",
|
||||||
"Item",
|
"Item",
|
||||||
|
"LineMatcher",
|
||||||
"LogCaptureFixture",
|
"LogCaptureFixture",
|
||||||
"main",
|
"main",
|
||||||
"mark",
|
"mark",
|
||||||
|
@ -129,7 +135,9 @@ __all__ = [
|
||||||
"PytestUnraisableExceptionWarning",
|
"PytestUnraisableExceptionWarning",
|
||||||
"PytestWarning",
|
"PytestWarning",
|
||||||
"raises",
|
"raises",
|
||||||
|
"RecordedHookCall",
|
||||||
"register_assert_rewrite",
|
"register_assert_rewrite",
|
||||||
|
"RunResult",
|
||||||
"Session",
|
"Session",
|
||||||
"set_trace",
|
"set_trace",
|
||||||
"skip",
|
"skip",
|
||||||
|
|
|
@ -7,6 +7,7 @@ from typing import Dict
|
||||||
import _pytest._code
|
import _pytest._code
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest.config import ExitCode
|
from _pytest.config import ExitCode
|
||||||
|
from _pytest.main import Session
|
||||||
from _pytest.monkeypatch import MonkeyPatch
|
from _pytest.monkeypatch import MonkeyPatch
|
||||||
from _pytest.nodes import Collector
|
from _pytest.nodes import Collector
|
||||||
from _pytest.pytester import Pytester
|
from _pytest.pytester import Pytester
|
||||||
|
@ -294,7 +295,7 @@ class TestFunction:
|
||||||
from _pytest.fixtures import FixtureManager
|
from _pytest.fixtures import FixtureManager
|
||||||
|
|
||||||
config = pytester.parseconfigure()
|
config = pytester.parseconfigure()
|
||||||
session = pytester.Session.from_config(config)
|
session = Session.from_config(config)
|
||||||
session._fixturemanager = FixtureManager(session)
|
session._fixturemanager = FixtureManager(session)
|
||||||
|
|
||||||
return pytest.Function.from_parent(parent=session, **kwargs)
|
return pytest.Function.from_parent(parent=session, **kwargs)
|
||||||
|
|
|
@ -192,7 +192,7 @@ def make_holder():
|
||||||
def test_hookrecorder_basic(holder) -> None:
|
def test_hookrecorder_basic(holder) -> None:
|
||||||
pm = PytestPluginManager()
|
pm = PytestPluginManager()
|
||||||
pm.add_hookspecs(holder)
|
pm.add_hookspecs(holder)
|
||||||
rec = HookRecorder(pm)
|
rec = HookRecorder(pm, _ispytest=True)
|
||||||
pm.hook.pytest_xyz(arg=123)
|
pm.hook.pytest_xyz(arg=123)
|
||||||
call = rec.popcall("pytest_xyz")
|
call = rec.popcall("pytest_xyz")
|
||||||
assert call.arg == 123
|
assert call.arg == 123
|
||||||
|
|
Loading…
Reference in New Issue