some docs and refined semantics for wrappers
This commit is contained in:
parent
c58770bfef
commit
8c91ffc701
|
@ -89,30 +89,34 @@ def add_method_wrapper(cls, wrapper_func):
|
|||
setattr(cls, name, wrap_exec)
|
||||
return lambda: setattr(cls, name, oldcall)
|
||||
|
||||
def raise_wrapfail(wrap_controller, msg):
|
||||
co = wrap_controller.gi_code
|
||||
raise RuntimeError("wrap_controller at %r %s:%d %s" %
|
||||
(co.co_name, co.co_filename, co.co_firstlineno, msg))
|
||||
|
||||
def wrapped_call(wrap_controller, func):
|
||||
""" Wrap calling to a function with a generator. The first yield
|
||||
will trigger calling the function and receive an according CallOutcome
|
||||
object representing an exception or a result. The generator then
|
||||
needs to finish (raise StopIteration) in order for the wrapped call
|
||||
to complete.
|
||||
""" Wrap calling to a function with a generator which needs to yield
|
||||
exactly once. The yield point will trigger calling the wrapped function
|
||||
and return its CallOutcome to the yield point. The generator then needs
|
||||
to finish (raise StopIteration) in order for the wrapped call to complete.
|
||||
"""
|
||||
try:
|
||||
next(wrap_controller) # first yield
|
||||
except StopIteration:
|
||||
return
|
||||
raise_wrapfail(wrap_controller, "did not yield")
|
||||
call_outcome = CallOutcome(func)
|
||||
try:
|
||||
wrap_controller.send(call_outcome)
|
||||
co = wrap_controller.gi_frame.f_code
|
||||
raise RuntimeError("wrap_controller for %r %s:%d has second yield" %
|
||||
(co.co_name, co.co_filename, co.co_firstlineno))
|
||||
raise_wrapfail(wrap_controller, "has second yield")
|
||||
except StopIteration:
|
||||
pass
|
||||
return call_outcome.get_result()
|
||||
|
||||
|
||||
class CallOutcome:
|
||||
""" Outcome of a function call, either an exception or a proper result.
|
||||
Calling the ``get_result`` method will return the result or reraise
|
||||
the exception raised when the function was called. """
|
||||
excinfo = None
|
||||
def __init__(self, func):
|
||||
try:
|
||||
|
|
|
@ -187,8 +187,8 @@ Plugin discovery order at tool startup
|
|||
invocation:
|
||||
|
||||
- if no test paths are specified use current dir as a test path
|
||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the first test path.
|
||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||
to the directory part of the first test path.
|
||||
|
||||
Note that pytest does not find ``conftest.py`` files in deeper nested
|
||||
sub directories at tool startup. It is usually a good idea to keep
|
||||
|
@ -431,6 +431,41 @@ declaring the hook functions directly in your plugin module, for example::
|
|||
This has the added benefit of allowing you to conditionally install hooks
|
||||
depending on which plugins are installed.
|
||||
|
||||
hookwrapper: executing around other hooks
|
||||
-------------------------------------------------
|
||||
|
||||
.. currentmodule:: _pytest.core
|
||||
|
||||
.. versionadded:: 2.7 (experimental)
|
||||
|
||||
pytest plugins can implement hook wrappers which which wrap the execution
|
||||
of other hook implementations. A hook wrapper is a generator function
|
||||
which yields exactly once. When pytest invokes hooks it first executes
|
||||
hook wrappers and passes the same arguments as to the regular hooks.
|
||||
|
||||
At the yield point of the hook wrapper pytest will execute the next hook
|
||||
implementations and return their result to the yield point in the form of
|
||||
a :py:class:`CallOutcome` instance which encapsulates a result or
|
||||
exception info. The yield point itself will thus typically not raise
|
||||
exceptions (unless there are bugs).
|
||||
|
||||
Here is an example definition of a hook wrapper::
|
||||
|
||||
import pytest
|
||||
|
||||
@pytest.mark.hookwrapper
|
||||
def pytest_pyfunc_call(pyfuncitem):
|
||||
# do whatever you want before the next hook executes
|
||||
outcome = yield
|
||||
# outcome.excinfo may be None or a (cls, val, tb) tuple
|
||||
res = outcome.get_result() # will raise if outcome was exception
|
||||
# postprocess result
|
||||
|
||||
Note that hook wrappers don't return results themselves, they merely
|
||||
perform tracing or other side effects around the actual hook implementations.
|
||||
If the result of the underlying hook is a mutable object, they may modify
|
||||
that result, however.
|
||||
|
||||
|
||||
Reference of objects involved in hooks
|
||||
===========================================================
|
||||
|
@ -470,3 +505,6 @@ Reference of objects involved in hooks
|
|||
.. autoclass:: _pytest.runner.TestReport()
|
||||
:members:
|
||||
|
||||
.. autoclass:: _pytest.core.CallOutcome()
|
||||
:members:
|
||||
|
||||
|
|
|
@ -783,6 +783,22 @@ class TestWrapMethod:
|
|||
assert A().f() == "A.f"
|
||||
assert l == []
|
||||
|
||||
def test_no_yield(self):
|
||||
class A:
|
||||
def method(self):
|
||||
return
|
||||
|
||||
def method(self):
|
||||
if 0:
|
||||
yield
|
||||
|
||||
add_method_wrapper(A, method)
|
||||
with pytest.raises(RuntimeError) as excinfo:
|
||||
A().method()
|
||||
|
||||
assert "method" in str(excinfo.value)
|
||||
assert "did not yield" in str(excinfo.value)
|
||||
|
||||
def test_method_raises(self):
|
||||
class A:
|
||||
def error(self, val):
|
||||
|
|
Loading…
Reference in New Issue