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)
|
setattr(cls, name, wrap_exec)
|
||||||
return lambda: setattr(cls, name, oldcall)
|
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):
|
def wrapped_call(wrap_controller, func):
|
||||||
""" Wrap calling to a function with a generator. The first yield
|
""" Wrap calling to a function with a generator which needs to yield
|
||||||
will trigger calling the function and receive an according CallOutcome
|
exactly once. The yield point will trigger calling the wrapped function
|
||||||
object representing an exception or a result. The generator then
|
and return its CallOutcome to the yield point. The generator then needs
|
||||||
needs to finish (raise StopIteration) in order for the wrapped call
|
to finish (raise StopIteration) in order for the wrapped call to complete.
|
||||||
to complete.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
next(wrap_controller) # first yield
|
next(wrap_controller) # first yield
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
return
|
raise_wrapfail(wrap_controller, "did not yield")
|
||||||
call_outcome = CallOutcome(func)
|
call_outcome = CallOutcome(func)
|
||||||
try:
|
try:
|
||||||
wrap_controller.send(call_outcome)
|
wrap_controller.send(call_outcome)
|
||||||
co = wrap_controller.gi_frame.f_code
|
raise_wrapfail(wrap_controller, "has second yield")
|
||||||
raise RuntimeError("wrap_controller for %r %s:%d has second yield" %
|
|
||||||
(co.co_name, co.co_filename, co.co_firstlineno))
|
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
pass
|
pass
|
||||||
return call_outcome.get_result()
|
return call_outcome.get_result()
|
||||||
|
|
||||||
|
|
||||||
class CallOutcome:
|
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
|
excinfo = None
|
||||||
def __init__(self, func):
|
def __init__(self, func):
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -187,8 +187,8 @@ Plugin discovery order at tool startup
|
||||||
invocation:
|
invocation:
|
||||||
|
|
||||||
- if no test paths are specified use current dir as a test path
|
- if no test paths are specified use current dir as a test path
|
||||||
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
- if exists, load ``conftest.py`` and ``test*/conftest.py`` relative
|
||||||
to the directory part of the first test path.
|
to the directory part of the first test path.
|
||||||
|
|
||||||
Note that pytest does not find ``conftest.py`` files in deeper nested
|
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
|
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
|
This has the added benefit of allowing you to conditionally install hooks
|
||||||
depending on which plugins are installed.
|
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
|
Reference of objects involved in hooks
|
||||||
===========================================================
|
===========================================================
|
||||||
|
@ -470,3 +505,6 @@ Reference of objects involved in hooks
|
||||||
.. autoclass:: _pytest.runner.TestReport()
|
.. autoclass:: _pytest.runner.TestReport()
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
.. autoclass:: _pytest.core.CallOutcome()
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
|
@ -783,6 +783,22 @@ class TestWrapMethod:
|
||||||
assert A().f() == "A.f"
|
assert A().f() == "A.f"
|
||||||
assert l == []
|
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):
|
def test_method_raises(self):
|
||||||
class A:
|
class A:
|
||||||
def error(self, val):
|
def error(self, val):
|
||||||
|
|
Loading…
Reference in New Issue