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