some docs and refined semantics for wrappers

This commit is contained in:
holger krekel 2014-10-09 12:21:01 +02:00
parent c58770bfef
commit 8c91ffc701
3 changed files with 69 additions and 11 deletions

View File

@ -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:

View File

@ -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:

View File

@ -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):