Make transitive Pytester types public
Export `HookRecorder`, `RecordedHookCall` (originally `ParsedCall`), `RunResult`, `LineMatcher`. These types are reachable through `Pytester` and so should be public themselves for typing and other purposes. The name `ParsedCall` I think is too generic under the `pytest` namespace, so rename it to `RecordedHookCall`. The `HookRecorder`'s constructor is made private -- it should only be constructed by `Pytester`. `LineMatcher` and `RunResult` are exported as is - no private and no rename, since they're being used. All of the classes are made final as they are not designed for subclassing.
This commit is contained in:
parent
259cff59f5
commit
11e36c8493
|
@ -8,5 +8,6 @@ Directly constructing the following classes is now deprecated:
|
|||
- ``_pytest._code.ExceptionInfo``
|
||||
- ``_pytest.config.argparsing.Parser``
|
||||
- ``_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.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.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.
|
||||
|
||||
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()
|
||||
:members:
|
||||
|
||||
.. autoclass:: _pytest.pytester.RunResult()
|
||||
.. autoclass:: pytest.RunResult()
|
||||
:members:
|
||||
|
||||
.. autoclass:: _pytest.pytester.LineMatcher()
|
||||
.. autoclass:: pytest.LineMatcher()
|
||||
:members:
|
||||
:special-members: __str__
|
||||
|
||||
.. autoclass:: _pytest.pytester.HookRecorder()
|
||||
.. autoclass:: pytest.HookRecorder()
|
||||
:members:
|
||||
|
||||
.. autoclass:: pytest.RecordedHookCall()
|
||||
:members:
|
||||
|
||||
.. fixture:: testdir
|
||||
|
|
|
@ -214,7 +214,20 @@ def get_public_names(values: Iterable[str]) -> List[str]:
|
|||
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:
|
||||
self.__dict__.update(kwargs)
|
||||
self._name = name
|
||||
|
@ -222,7 +235,7 @@ class ParsedCall:
|
|||
def __repr__(self) -> str:
|
||||
d = self.__dict__.copy()
|
||||
del d["_name"]
|
||||
return f"<ParsedCall {self._name!r}(**{d!r})>"
|
||||
return f"<RecordedHookCall {self._name!r}(**{d!r})>"
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# The class has undetermined attributes, this tells mypy about it.
|
||||
|
@ -230,20 +243,27 @@ class ParsedCall:
|
|||
...
|
||||
|
||||
|
||||
@final
|
||||
class HookRecorder:
|
||||
"""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
|
||||
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.calls: List[ParsedCall] = []
|
||||
self.calls: List[RecordedHookCall] = []
|
||||
self.ret: Optional[Union[int, ExitCode]] = 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:
|
||||
pass
|
||||
|
@ -253,7 +273,8 @@ class HookRecorder:
|
|||
def finish_recording(self) -> None:
|
||||
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):
|
||||
names = names.split()
|
||||
return [call for call in self.calls if call._name in names]
|
||||
|
@ -279,7 +300,7 @@ class HookRecorder:
|
|||
else:
|
||||
fail(f"could not find {name!r} check {check!r}")
|
||||
|
||||
def popcall(self, name: str) -> ParsedCall:
|
||||
def popcall(self, name: str) -> RecordedHookCall:
|
||||
__tracebackhide__ = True
|
||||
for i, call in enumerate(self.calls):
|
||||
if call._name == name:
|
||||
|
@ -289,7 +310,7 @@ class HookRecorder:
|
|||
lines.extend([" %s" % x for x in self.calls])
|
||||
fail("\n".join(lines))
|
||||
|
||||
def getcall(self, name: str) -> ParsedCall:
|
||||
def getcall(self, name: str) -> RecordedHookCall:
|
||||
values = self.getcalls(name)
|
||||
assert len(values) == 1, (name, values)
|
||||
return values[0]
|
||||
|
@ -507,8 +528,9 @@ rex_session_duration = re.compile(r"\d+\.\d\ds")
|
|||
rex_outcome = re.compile(r"(\d+) (\w+)")
|
||||
|
||||
|
||||
@final
|
||||
class RunResult:
|
||||
"""The result of running a command."""
|
||||
"""The result of running a command from :class:`~pytest.Pytester`."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -527,13 +549,13 @@ class RunResult:
|
|||
self.errlines = errlines
|
||||
"""List of lines captured from stderr."""
|
||||
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
|
||||
:func:`stdout.fnmatch_lines() <LineMatcher.fnmatch_lines()>` method.
|
||||
Use e.g. :func:`str(stdout) <pytest.LineMatcher.__str__()>` to reconstruct stdout, or the commonly used
|
||||
:func:`stdout.fnmatch_lines() <pytest.LineMatcher.fnmatch_lines()>` method.
|
||||
"""
|
||||
self.stderr = LineMatcher(errlines)
|
||||
""":class:`LineMatcher` of stderr."""
|
||||
""":class:`~pytest.LineMatcher` of stderr."""
|
||||
self.duration = duration
|
||||
"""Duration in seconds."""
|
||||
|
||||
|
@ -741,7 +763,7 @@ class Pytester:
|
|||
|
||||
def make_hook_recorder(self, pluginmanager: PytestPluginManager) -> HookRecorder:
|
||||
"""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)
|
||||
return reprec
|
||||
|
||||
|
@ -1021,10 +1043,7 @@ class Pytester:
|
|||
for the result.
|
||||
|
||||
:param source: The source code of the test module.
|
||||
|
||||
:param cmdlineargs: Any extra command line arguments to use.
|
||||
|
||||
:returns: :py:class:`HookRecorder` instance of the result.
|
||||
"""
|
||||
p = self.makepyfile(source)
|
||||
values = list(cmdlineargs) + [p]
|
||||
|
@ -1062,8 +1081,6 @@ class Pytester:
|
|||
:param no_reraise_ctrlc:
|
||||
Typically we reraise keyboard interrupts from the child run. If
|
||||
True, the KeyboardInterrupt exception is captured.
|
||||
|
||||
:returns: A :py:class:`HookRecorder` instance.
|
||||
"""
|
||||
# (maybe a cpython bug?) the importlib cache sometimes isn't updated
|
||||
# properly between file creation and inline_run (especially if imports
|
||||
|
@ -1162,7 +1179,7 @@ class Pytester:
|
|||
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`."""
|
||||
option "--runpytest" and return a :py:class:`~pytest.RunResult`."""
|
||||
new_args = self._ensure_basetemp(args)
|
||||
if self._method == "inprocess":
|
||||
return self.runpytest_inprocess(*new_args, **kwargs)
|
||||
|
@ -1504,7 +1521,7 @@ class LineComp:
|
|||
def assert_contains_lines(self, lines2: Sequence[str]) -> None:
|
||||
"""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
|
||||
val = self.stringio.getvalue()
|
||||
|
@ -1731,6 +1748,7 @@ class Testdir:
|
|||
return str(self.tmpdir)
|
||||
|
||||
|
||||
@final
|
||||
class LineMatcher:
|
||||
"""Flexible matching of text.
|
||||
|
||||
|
|
|
@ -41,7 +41,11 @@ from _pytest.outcomes import fail
|
|||
from _pytest.outcomes import importorskip
|
||||
from _pytest.outcomes import skip
|
||||
from _pytest.outcomes import xfail
|
||||
from _pytest.pytester import HookRecorder
|
||||
from _pytest.pytester import LineMatcher
|
||||
from _pytest.pytester import Pytester
|
||||
from _pytest.pytester import RecordedHookCall
|
||||
from _pytest.pytester import RunResult
|
||||
from _pytest.pytester import Testdir
|
||||
from _pytest.python import Class
|
||||
from _pytest.python import Function
|
||||
|
@ -98,10 +102,12 @@ __all__ = [
|
|||
"freeze_includes",
|
||||
"Function",
|
||||
"hookimpl",
|
||||
"HookRecorder",
|
||||
"hookspec",
|
||||
"importorskip",
|
||||
"Instance",
|
||||
"Item",
|
||||
"LineMatcher",
|
||||
"LogCaptureFixture",
|
||||
"main",
|
||||
"mark",
|
||||
|
@ -129,7 +135,9 @@ __all__ = [
|
|||
"PytestUnraisableExceptionWarning",
|
||||
"PytestWarning",
|
||||
"raises",
|
||||
"RecordedHookCall",
|
||||
"register_assert_rewrite",
|
||||
"RunResult",
|
||||
"Session",
|
||||
"set_trace",
|
||||
"skip",
|
||||
|
|
|
@ -192,7 +192,7 @@ def make_holder():
|
|||
def test_hookrecorder_basic(holder) -> None:
|
||||
pm = PytestPluginManager()
|
||||
pm.add_hookspecs(holder)
|
||||
rec = HookRecorder(pm)
|
||||
rec = HookRecorder(pm, _ispytest=True)
|
||||
pm.hook.pytest_xyz(arg=123)
|
||||
call = rec.popcall("pytest_xyz")
|
||||
assert call.arg == 123
|
||||
|
|
Loading…
Reference in New Issue