Merge pull request #1586 from nicoddemus/issue-1461-merge-yield-fixture
Make normal fixtures work with "yield"
This commit is contained in:
		
						commit
						feeee2803e
					
				|  | @ -36,6 +36,12 @@ | ||||||
| 
 | 
 | ||||||
| **Changes** | **Changes** | ||||||
| 
 | 
 | ||||||
|  | * Fixtures marked with ``@pytest.fixture`` can now use ``yield`` statements exactly like | ||||||
|  |   those marked with the ``@pytest.yield_fixture`` decorator. This change renders | ||||||
|  |   ``@pytest.yield_fixture`` deprecated and makes ``@pytest.fixture`` with ``yield`` statements | ||||||
|  |   the preferred way to write teardown code (`#1461`_). | ||||||
|  |   Thanks `@csaftoiu`_ for bringing this to attention and `@nicoddemus`_ for the PR. | ||||||
|  | 
 | ||||||
| * Fix (`#1351`_): | * Fix (`#1351`_): | ||||||
|   explicitly passed parametrize ids do not get escaped to ascii. |   explicitly passed parametrize ids do not get escaped to ascii. | ||||||
|   Thanks `@ceridwen`_ for the PR. |   Thanks `@ceridwen`_ for the PR. | ||||||
|  | @ -58,6 +64,7 @@ | ||||||
| * | * | ||||||
| 
 | 
 | ||||||
| .. _@milliams: https://github.com/milliams | .. _@milliams: https://github.com/milliams | ||||||
|  | .. _@csaftoiu: https://github.com/csaftoiu | ||||||
| .. _@novas0x2a: https://github.com/novas0x2a | .. _@novas0x2a: https://github.com/novas0x2a | ||||||
| .. _@kalekundert: https://github.com/kalekundert | .. _@kalekundert: https://github.com/kalekundert | ||||||
| .. _@tareqalayan: https://github.com/tareqalayan | .. _@tareqalayan: https://github.com/tareqalayan | ||||||
|  | @ -72,6 +79,7 @@ | ||||||
| .. _#1441: https://github.com/pytest-dev/pytest/pull/1441 | .. _#1441: https://github.com/pytest-dev/pytest/pull/1441 | ||||||
| .. _#1454: https://github.com/pytest-dev/pytest/pull/1454 | .. _#1454: https://github.com/pytest-dev/pytest/pull/1454 | ||||||
| .. _#1351: https://github.com/pytest-dev/pytest/issues/1351 | .. _#1351: https://github.com/pytest-dev/pytest/issues/1351 | ||||||
|  | .. _#1461: https://github.com/pytest-dev/pytest/pull/1461 | ||||||
| .. _#1468: https://github.com/pytest-dev/pytest/pull/1468 | .. _#1468: https://github.com/pytest-dev/pytest/pull/1468 | ||||||
| .. _#1474: https://github.com/pytest-dev/pytest/pull/1474 | .. _#1474: https://github.com/pytest-dev/pytest/pull/1474 | ||||||
| .. _#1502: https://github.com/pytest-dev/pytest/pull/1502 | .. _#1502: https://github.com/pytest-dev/pytest/pull/1502 | ||||||
|  |  | ||||||
|  | @ -116,12 +116,10 @@ def safe_getattr(object, name, default): | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class FixtureFunctionMarker: | class FixtureFunctionMarker: | ||||||
|     def __init__(self, scope, params, |     def __init__(self, scope, params, autouse=False, ids=None, name=None): | ||||||
|                  autouse=False, yieldctx=False, ids=None, name=None): |  | ||||||
|         self.scope = scope |         self.scope = scope | ||||||
|         self.params = params |         self.params = params | ||||||
|         self.autouse = autouse |         self.autouse = autouse | ||||||
|         self.yieldctx = yieldctx |  | ||||||
|         self.ids = ids |         self.ids = ids | ||||||
|         self.name = name |         self.name = name | ||||||
| 
 | 
 | ||||||
|  | @ -166,6 +164,10 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None): | ||||||
|                to resolve this is to name the decorated function |                to resolve this is to name the decorated function | ||||||
|                ``fixture_<fixturename>`` and then use |                ``fixture_<fixturename>`` and then use | ||||||
|                ``@pytest.fixture(name='<fixturename>')``. |                ``@pytest.fixture(name='<fixturename>')``. | ||||||
|  | 
 | ||||||
|  |     Fixtures can optionally provide their values to test functions using a ``yield`` statement, | ||||||
|  |     instead of ``return``. In this case, the code block after the ``yield`` statement is executed | ||||||
|  |     as teardown code regardless of the test outcome. A fixture function must yield exactly once. | ||||||
|     """ |     """ | ||||||
|     if callable(scope) and params is None and autouse == False: |     if callable(scope) and params is None and autouse == False: | ||||||
|         # direct decoration |         # direct decoration | ||||||
|  | @ -175,22 +177,19 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None): | ||||||
|         params = list(params) |         params = list(params) | ||||||
|     return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) |     return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) | ||||||
| 
 | 
 | ||||||
| def yield_fixture(scope="function", params=None, autouse=False, ids=None): |  | ||||||
|     """ (return a) decorator to mark a yield-fixture factory function |  | ||||||
|     (EXPERIMENTAL). |  | ||||||
| 
 | 
 | ||||||
|     This takes the same arguments as :py:func:`pytest.fixture` but | def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=None): | ||||||
|     expects a fixture function to use a ``yield`` instead of a ``return`` |     """ (return a) decorator to mark a yield-fixture factory function. | ||||||
|     statement to provide a fixture.  See | 
 | ||||||
|     http://pytest.org/en/latest/yieldfixture.html for more info. |     .. deprecated:: 1.10 | ||||||
|  |         Use :py:func:`pytest.fixture` directly instead. | ||||||
|     """ |     """ | ||||||
|     if callable(scope) and params is None and autouse == False: |     if callable(scope) and params is None and not autouse: | ||||||
|         # direct decoration |         # direct decoration | ||||||
|         return FixtureFunctionMarker( |         return FixtureFunctionMarker( | ||||||
|                 "function", params, autouse, yieldctx=True)(scope) |                 "function", params, autouse, ids=ids, name=name)(scope) | ||||||
|     else: |     else: | ||||||
|         return FixtureFunctionMarker(scope, params, autouse, |         return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name) | ||||||
|                                      yieldctx=True, ids=ids) |  | ||||||
| 
 | 
 | ||||||
| defaultfuncargprefixmarker = fixture() | defaultfuncargprefixmarker = fixture() | ||||||
| 
 | 
 | ||||||
|  | @ -2287,7 +2286,6 @@ class FixtureManager: | ||||||
|                 assert not name.startswith(self._argprefix) |                 assert not name.startswith(self._argprefix) | ||||||
|             fixturedef = FixtureDef(self, nodeid, name, obj, |             fixturedef = FixtureDef(self, nodeid, name, obj, | ||||||
|                                     marker.scope, marker.params, |                                     marker.scope, marker.params, | ||||||
|                                     yieldctx=marker.yieldctx, |  | ||||||
|                                     unittest=unittest, ids=marker.ids) |                                     unittest=unittest, ids=marker.ids) | ||||||
|             faclist = self._arg2fixturedefs.setdefault(name, []) |             faclist = self._arg2fixturedefs.setdefault(name, []) | ||||||
|             if fixturedef.has_location: |             if fixturedef.has_location: | ||||||
|  | @ -2325,38 +2323,30 @@ def fail_fixturefunc(fixturefunc, msg): | ||||||
|     pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, |     pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location, | ||||||
|                 pytrace=False) |                 pytrace=False) | ||||||
| 
 | 
 | ||||||
| def call_fixture_func(fixturefunc, request, kwargs, yieldctx): | def call_fixture_func(fixturefunc, request, kwargs): | ||||||
|  |     yieldctx = is_generator(fixturefunc) | ||||||
|     if yieldctx: |     if yieldctx: | ||||||
|         if not is_generator(fixturefunc): |         it = fixturefunc(**kwargs) | ||||||
|             fail_fixturefunc(fixturefunc, |         res = next(it) | ||||||
|                 msg="yield_fixture requires yield statement in function") | 
 | ||||||
|         iter = fixturefunc(**kwargs) |  | ||||||
|         next = getattr(iter, "__next__", None) |  | ||||||
|         if next is None: |  | ||||||
|             next = getattr(iter, "next") |  | ||||||
|         res = next() |  | ||||||
|         def teardown(): |         def teardown(): | ||||||
|             try: |             try: | ||||||
|                 next() |                 next(it) | ||||||
|             except StopIteration: |             except StopIteration: | ||||||
|                 pass |                 pass | ||||||
|             else: |             else: | ||||||
|                 fail_fixturefunc(fixturefunc, |                 fail_fixturefunc(fixturefunc, | ||||||
|                     "yield_fixture function has more than one 'yield'") |                     "yield_fixture function has more than one 'yield'") | ||||||
|  | 
 | ||||||
|         request.addfinalizer(teardown) |         request.addfinalizer(teardown) | ||||||
|     else: |     else: | ||||||
|         if is_generator(fixturefunc): |  | ||||||
|             fail_fixturefunc(fixturefunc, |  | ||||||
|                 msg="pytest.fixture functions cannot use ``yield``. " |  | ||||||
|                     "Instead write and return an inner function/generator " |  | ||||||
|                     "and let the consumer call and iterate over it.") |  | ||||||
|         res = fixturefunc(**kwargs) |         res = fixturefunc(**kwargs) | ||||||
|     return res |     return res | ||||||
| 
 | 
 | ||||||
| class FixtureDef: | class FixtureDef: | ||||||
|     """ A container for a factory definition. """ |     """ A container for a factory definition. """ | ||||||
|     def __init__(self, fixturemanager, baseid, argname, func, scope, params, |     def __init__(self, fixturemanager, baseid, argname, func, scope, params, | ||||||
|                  yieldctx, unittest=False, ids=None): |                  unittest=False, ids=None): | ||||||
|         self._fixturemanager = fixturemanager |         self._fixturemanager = fixturemanager | ||||||
|         self.baseid = baseid or '' |         self.baseid = baseid or '' | ||||||
|         self.has_location = baseid is not None |         self.has_location = baseid is not None | ||||||
|  | @ -2367,7 +2357,6 @@ class FixtureDef: | ||||||
|         self.params = params |         self.params = params | ||||||
|         startindex = unittest and 1 or None |         startindex = unittest and 1 or None | ||||||
|         self.argnames = getfuncargnames(func, startindex=startindex) |         self.argnames = getfuncargnames(func, startindex=startindex) | ||||||
|         self.yieldctx = yieldctx |  | ||||||
|         self.unittest = unittest |         self.unittest = unittest | ||||||
|         self.ids = ids |         self.ids = ids | ||||||
|         self._finalizer = [] |         self._finalizer = [] | ||||||
|  | @ -2428,8 +2417,7 @@ class FixtureDef: | ||||||
|                     fixturefunc = fixturefunc.__get__(request.instance) |                     fixturefunc = fixturefunc.__get__(request.instance) | ||||||
| 
 | 
 | ||||||
|         try: |         try: | ||||||
|             result = call_fixture_func(fixturefunc, request, kwargs, |             result = call_fixture_func(fixturefunc, request, kwargs) | ||||||
|                                        self.yieldctx) |  | ||||||
|         except Exception: |         except Exception: | ||||||
|             self.cached_result = (None, my_cache_key, sys.exc_info()) |             self.cached_result = (None, my_cache_key, sys.exc_info()) | ||||||
|             raise |             raise | ||||||
|  |  | ||||||
|  | @ -4,8 +4,8 @@ import pytest | ||||||
| @pytest.fixture("session") | @pytest.fixture("session") | ||||||
| def setup(request): | def setup(request): | ||||||
|     setup = CostlySetup() |     setup = CostlySetup() | ||||||
|     request.addfinalizer(setup.finalize) |     yield setup | ||||||
|     return setup |     setup.finalize() | ||||||
| 
 | 
 | ||||||
| class CostlySetup: | class CostlySetup: | ||||||
|     def __init__(self): |     def __init__(self): | ||||||
|  |  | ||||||
|  | @ -648,7 +648,7 @@ here is a little example implemented via a local plugin:: | ||||||
| 
 | 
 | ||||||
|     @pytest.fixture |     @pytest.fixture | ||||||
|     def something(request): |     def something(request): | ||||||
|         def fin(): |         yield | ||||||
|         # request.node is an "item" because we use the default |         # request.node is an "item" because we use the default | ||||||
|         # "function" scope |         # "function" scope | ||||||
|         if request.node.rep_setup.failed: |         if request.node.rep_setup.failed: | ||||||
|  | @ -656,7 +656,6 @@ here is a little example implemented via a local plugin:: | ||||||
|         elif request.node.rep_setup.passed: |         elif request.node.rep_setup.passed: | ||||||
|             if request.node.rep_call.failed: |             if request.node.rep_call.failed: | ||||||
|                 print ("executing test failed", request.node.nodeid) |                 print ("executing test failed", request.node.nodeid) | ||||||
|         request.addfinalizer(fin) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| if you then have failing tests:: | if you then have failing tests:: | ||||||
|  |  | ||||||
|  | @ -34,11 +34,6 @@ both styles, moving incrementally from classic to new style, as you | ||||||
| prefer.  You can also start out from existing :ref:`unittest.TestCase | prefer.  You can also start out from existing :ref:`unittest.TestCase | ||||||
| style <unittest.TestCase>` or :ref:`nose based <nosestyle>` projects. | style <unittest.TestCase>` or :ref:`nose based <nosestyle>` projects. | ||||||
| 
 | 
 | ||||||
| .. note:: |  | ||||||
| 
 |  | ||||||
|     pytest-2.4 introduced an additional :ref:`yield fixture mechanism |  | ||||||
|     <yieldfixture>` for easier context manager integration and more linear |  | ||||||
|     writing of teardown code. |  | ||||||
| 
 | 
 | ||||||
| .. _`funcargs`: | .. _`funcargs`: | ||||||
| .. _`funcarg mechanism`: | .. _`funcarg mechanism`: | ||||||
|  | @ -247,9 +242,67 @@ Fixture finalization / executing teardown code | ||||||
| ------------------------------------------------------------- | ------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| pytest supports execution of fixture specific finalization code | pytest supports execution of fixture specific finalization code | ||||||
| when the fixture goes out of scope.  By accepting a ``request`` object | when the fixture goes out of scope.  By using a ``yield`` statement instead of ``return``, all | ||||||
| into your fixture function you can call its ``request.addfinalizer`` one | the code after the *yield* statement serves as the teardown code.:: | ||||||
| or multiple times:: | 
 | ||||||
|  |     # content of conftest.py | ||||||
|  | 
 | ||||||
|  |     import smtplib | ||||||
|  |     import pytest | ||||||
|  | 
 | ||||||
|  |     @pytest.fixture(scope="module") | ||||||
|  |     def smtp(request): | ||||||
|  |         smtp = smtplib.SMTP("smtp.gmail.com") | ||||||
|  |         yield smtp  # provide the fixture value | ||||||
|  |         print("teardown smtp") | ||||||
|  |         smtp.close() | ||||||
|  | 
 | ||||||
|  | The ``print`` and ``smtp.close()`` statements will execute when the last test using | ||||||
|  | the fixture in the module has finished execution, regardless of the exception status of the tests. | ||||||
|  | 
 | ||||||
|  | Let's execute it:: | ||||||
|  | 
 | ||||||
|  |     $ py.test -s -q --tb=no | ||||||
|  |     FFteardown smtp | ||||||
|  |      | ||||||
|  |     2 failed in 0.12 seconds | ||||||
|  | 
 | ||||||
|  | We see that the ``smtp`` instance is finalized after the two | ||||||
|  | tests finished execution.  Note that if we decorated our fixture | ||||||
|  | function with ``scope='function'`` then fixture setup and cleanup would | ||||||
|  | occur around each single test.  In either case the test | ||||||
|  | module itself does not need to change or know about these details | ||||||
|  | of fixture setup. | ||||||
|  | 
 | ||||||
|  | Note that we can also seamlessly use the ``yield`` syntax with ``with`` statements:: | ||||||
|  | 
 | ||||||
|  |     # content of test_yield2.py | ||||||
|  | 
 | ||||||
|  |     import pytest | ||||||
|  | 
 | ||||||
|  |     @pytest.fixture | ||||||
|  |     def passwd(): | ||||||
|  |         with open("/etc/passwd") as f: | ||||||
|  |             yield f.readlines() | ||||||
|  | 
 | ||||||
|  |     def test_has_lines(passwd): | ||||||
|  |         assert len(passwd) >= 1 | ||||||
|  | 
 | ||||||
|  | The file ``f`` will be closed after the test finished execution | ||||||
|  | because the Python ``file`` object supports finalization when | ||||||
|  | the ``with`` statement ends. | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .. note:: | ||||||
|  |     Prior to version 2.10, in order to use a ``yield`` statement to execute teardown code one | ||||||
|  |     had to mark a fixture using the ``yield_fixture`` marker. From 2.10 onward, normal | ||||||
|  |     fixtures can use ``yield`` directly so the ``yield_fixture`` decorator is no longer needed | ||||||
|  |     and considered deprecated. | ||||||
|  | 
 | ||||||
|  | .. note:: | ||||||
|  |     As historical note, another way to write teardown code is | ||||||
|  |     by accepting a ``request`` object into your fixture function and can call its | ||||||
|  |     ``request.addfinalizer`` one or multiple times:: | ||||||
| 
 | 
 | ||||||
|         # content of conftest.py |         # content of conftest.py | ||||||
| 
 | 
 | ||||||
|  | @ -268,28 +321,8 @@ or multiple times:: | ||||||
|     The ``fin`` function will execute when the last test using |     The ``fin`` function will execute when the last test using | ||||||
|     the fixture in the module has finished execution. |     the fixture in the module has finished execution. | ||||||
| 
 | 
 | ||||||
| Let's execute it:: |     This method is still fully supported, but ``yield`` is recommended from 2.10 onward because | ||||||
| 
 |     it is considered simpler and better describes the natural code flow. | ||||||
|     $ py.test -s -q --tb=no |  | ||||||
|     FFteardown smtp |  | ||||||
|      |  | ||||||
|     2 failed in 0.12 seconds |  | ||||||
| 
 |  | ||||||
| We see that the ``smtp`` instance is finalized after the two |  | ||||||
| tests finished execution.  Note that if we decorated our fixture |  | ||||||
| function with ``scope='function'`` then fixture setup and cleanup would |  | ||||||
| occur around each single test.  In either case the test |  | ||||||
| module itself does not need to change or know about these details |  | ||||||
| of fixture setup. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| Finalization/teardown with yield fixtures |  | ||||||
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ |  | ||||||
| 
 |  | ||||||
| Another alternative to the *request.addfinalizer()* method is to use *yield |  | ||||||
| fixtures*. All the code after the *yield* statement serves as the teardown |  | ||||||
| code. See the :ref:`yield fixture documentation <yieldfixture>`. |  | ||||||
| 
 |  | ||||||
| 
 | 
 | ||||||
| .. _`request-context`: | .. _`request-context`: | ||||||
| 
 | 
 | ||||||
|  | @ -309,12 +342,9 @@ read an optional server URL from the test module which uses our fixture:: | ||||||
|     def smtp(request): |     def smtp(request): | ||||||
|         server = getattr(request.module, "smtpserver", "smtp.gmail.com") |         server = getattr(request.module, "smtpserver", "smtp.gmail.com") | ||||||
|         smtp = smtplib.SMTP(server) |         smtp = smtplib.SMTP(server) | ||||||
| 
 |         yield smtp | ||||||
|         def fin(): |  | ||||||
|         print ("finalizing %s (%s)" % (smtp, server)) |         print ("finalizing %s (%s)" % (smtp, server)) | ||||||
|         smtp.close() |         smtp.close() | ||||||
|         request.addfinalizer(fin) |  | ||||||
|         return smtp |  | ||||||
| 
 | 
 | ||||||
| We use the ``request.module`` attribute to optionally obtain an | We use the ``request.module`` attribute to optionally obtain an | ||||||
| ``smtpserver`` attribute from the test module.  If we just execute | ``smtpserver`` attribute from the test module.  If we just execute | ||||||
|  | @ -351,7 +381,7 @@ from the module namespace. | ||||||
| 
 | 
 | ||||||
| .. _`fixture-parametrize`: | .. _`fixture-parametrize`: | ||||||
| 
 | 
 | ||||||
| Parametrizing a fixture | Parametrizing fixtures | ||||||
| ----------------------------------------------------------------- | ----------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
| Fixture functions can be parametrized in which case they will be called | Fixture functions can be parametrized in which case they will be called | ||||||
|  | @ -374,11 +404,9 @@ through the special :py:class:`request <FixtureRequest>` object:: | ||||||
|                     params=["smtp.gmail.com", "mail.python.org"]) |                     params=["smtp.gmail.com", "mail.python.org"]) | ||||||
|     def smtp(request): |     def smtp(request): | ||||||
|         smtp = smtplib.SMTP(request.param) |         smtp = smtplib.SMTP(request.param) | ||||||
|         def fin(): |         yield smtp | ||||||
|         print ("finalizing %s" % smtp) |         print ("finalizing %s" % smtp) | ||||||
|         smtp.close() |         smtp.close() | ||||||
|         request.addfinalizer(fin) |  | ||||||
|         return smtp |  | ||||||
| 
 | 
 | ||||||
| The main change is the declaration of ``params`` with | The main change is the declaration of ``params`` with | ||||||
| :py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values | :py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values | ||||||
|  | @ -586,19 +614,15 @@ to show the setup/teardown flow:: | ||||||
|     def modarg(request): |     def modarg(request): | ||||||
|         param = request.param |         param = request.param | ||||||
|         print ("  SETUP modarg %s" % param) |         print ("  SETUP modarg %s" % param) | ||||||
|         def fin(): |         yield param | ||||||
|         print ("  TEARDOWN modarg %s" % param) |         print ("  TEARDOWN modarg %s" % param) | ||||||
|         request.addfinalizer(fin) |  | ||||||
|         return param |  | ||||||
| 
 | 
 | ||||||
|     @pytest.fixture(scope="function", params=[1,2]) |     @pytest.fixture(scope="function", params=[1,2]) | ||||||
|     def otherarg(request): |     def otherarg(request): | ||||||
|         param = request.param |         param = request.param | ||||||
|         print ("  SETUP otherarg %s" % param) |         print ("  SETUP otherarg %s" % param) | ||||||
|         def fin(): |         yield param | ||||||
|         print ("  TEARDOWN otherarg %s" % param) |         print ("  TEARDOWN otherarg %s" % param) | ||||||
|         request.addfinalizer(fin) |  | ||||||
|         return param |  | ||||||
| 
 | 
 | ||||||
|     def test_0(otherarg): |     def test_0(otherarg): | ||||||
|         print ("  RUN test0 with otherarg %s" % otherarg) |         print ("  RUN test0 with otherarg %s" % otherarg) | ||||||
|  | @ -777,7 +801,8 @@ self-contained implementation of this idea:: | ||||||
|         @pytest.fixture(autouse=True) |         @pytest.fixture(autouse=True) | ||||||
|         def transact(self, request, db): |         def transact(self, request, db): | ||||||
|             db.begin(request.function.__name__) |             db.begin(request.function.__name__) | ||||||
|             request.addfinalizer(db.rollback) |             yield | ||||||
|  |             db.rollback() | ||||||
| 
 | 
 | ||||||
|         def test_method1(self, db): |         def test_method1(self, db): | ||||||
|             assert db.intransaction == ["test_method1"] |             assert db.intransaction == ["test_method1"] | ||||||
|  | @ -817,10 +842,11 @@ active.  The canonical way to do that is to put the transact definition | ||||||
| into a conftest.py file **without** using ``autouse``:: | into a conftest.py file **without** using ``autouse``:: | ||||||
| 
 | 
 | ||||||
|     # content of conftest.py |     # content of conftest.py | ||||||
|     @pytest.fixture() |     @pytest.fixture | ||||||
|     def transact(self, request, db): |     def transact(self, request, db): | ||||||
|         db.begin() |         db.begin() | ||||||
|         request.addfinalizer(db.rollback) |         yield | ||||||
|  |         db.rollback() | ||||||
| 
 | 
 | ||||||
| and then e.g. have a TestClass using it by declaring the need:: | and then e.g. have a TestClass using it by declaring the need:: | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -1,100 +1,17 @@ | ||||||
| .. _yieldfixture: | .. _yieldfixture: | ||||||
| 
 | 
 | ||||||
| Fixture functions using "yield" / context manager integration | "yield_fixture" functions | ||||||
| --------------------------------------------------------------- | --------------------------------------------------------------- | ||||||
| 
 | 
 | ||||||
|  | .. deprecated:: 2.10 | ||||||
|  | 
 | ||||||
| .. versionadded:: 2.4 | .. versionadded:: 2.4 | ||||||
| 
 | 
 | ||||||
| .. regendoc:wipe | .. important:: | ||||||
|  |     Since pytest-2.10, fixtures using the normal ``fixture`` decorator can use a ``yield`` | ||||||
|  |     statement to provide fixture values and execute teardown code, exactly like ``yield_fixture`` | ||||||
|  |     in previous versions. | ||||||
| 
 | 
 | ||||||
| pytest-2.4 allows fixture functions to seamlessly use a ``yield`` instead  |     Marking functions as ``yield_fixture`` is still supported, but deprecated and should not | ||||||
| of a ``return`` statement to provide a fixture value while otherwise |     be used in new code. | ||||||
| fully supporting all other fixture features. |  | ||||||
| 
 | 
 | ||||||
| Let's look at a simple standalone-example using the ``yield`` syntax:: |  | ||||||
| 
 |  | ||||||
|     # content of test_yield.py |  | ||||||
|      |  | ||||||
|     import pytest |  | ||||||
| 
 |  | ||||||
|     @pytest.yield_fixture |  | ||||||
|     def passwd(): |  | ||||||
|         print ("\nsetup before yield") |  | ||||||
|         f = open("/etc/passwd") |  | ||||||
|         yield f.readlines() |  | ||||||
|         print ("teardown after yield") |  | ||||||
|         f.close() |  | ||||||
| 
 |  | ||||||
|     def test_has_lines(passwd): |  | ||||||
|         print ("test called") |  | ||||||
|         assert passwd |  | ||||||
| 
 |  | ||||||
| In contrast to :ref:`finalization through registering callbacks |  | ||||||
| <finalization>`, our fixture function used a ``yield`` |  | ||||||
| statement to provide the lines of the ``/etc/passwd`` file.   |  | ||||||
| The code after the ``yield`` statement serves as the teardown code,  |  | ||||||
| avoiding the indirection of registering a teardown callback function.    |  | ||||||
| 
 |  | ||||||
| Let's run it with output capturing disabled:: |  | ||||||
| 
 |  | ||||||
|     $ py.test -q -s test_yield.py |  | ||||||
|      |  | ||||||
|     setup before yield |  | ||||||
|     test called |  | ||||||
|     .teardown after yield |  | ||||||
|      |  | ||||||
|     1 passed in 0.12 seconds |  | ||||||
| 
 |  | ||||||
| We can also seamlessly use the new syntax with ``with`` statements. |  | ||||||
| Let's simplify the above ``passwd`` fixture:: |  | ||||||
| 
 |  | ||||||
|     # content of test_yield2.py |  | ||||||
|      |  | ||||||
|     import pytest |  | ||||||
| 
 |  | ||||||
|     @pytest.yield_fixture |  | ||||||
|     def passwd(): |  | ||||||
|         with open("/etc/passwd") as f: |  | ||||||
|             yield f.readlines() |  | ||||||
| 
 |  | ||||||
|     def test_has_lines(passwd): |  | ||||||
|         assert len(passwd) >= 1 |  | ||||||
| 
 |  | ||||||
| The file ``f`` will be closed after the test finished execution |  | ||||||
| because the Python ``file`` object supports finalization when |  | ||||||
| the ``with`` statement ends.  |  | ||||||
| 
 |  | ||||||
| Note that the yield fixture form supports all other fixture |  | ||||||
| features such as ``scope``, ``params``, etc., thus changing existing |  | ||||||
| fixture functions to use ``yield`` is straightforward. |  | ||||||
| 
 |  | ||||||
| .. note:: |  | ||||||
| 
 |  | ||||||
|     While the ``yield`` syntax is similar to what |  | ||||||
|     :py:func:`contextlib.contextmanager` decorated functions |  | ||||||
|     provide, with pytest fixture functions the part after the |  | ||||||
|     "yield" will always be invoked, independently from the |  | ||||||
|     exception status of the test function which uses the fixture. |  | ||||||
|     This behaviour makes sense if you consider that many different |  | ||||||
|     test functions might use a module or session scoped fixture. |  | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| Discussion and future considerations / feedback |  | ||||||
| ++++++++++++++++++++++++++++++++++++++++++++++++++++ |  | ||||||
| 
 |  | ||||||
| There are some topics that are worth mentioning: |  | ||||||
| 
 |  | ||||||
| - usually ``yield`` is used for producing multiple values. |  | ||||||
|   But fixture functions can only yield exactly one value. |  | ||||||
|   Yielding a second fixture value will get you an error. |  | ||||||
|   It's possible we can evolve pytest to allow for producing |  | ||||||
|   multiple values as an alternative to current parametrization. |  | ||||||
|   For now, you can just use the normal |  | ||||||
|   :ref:`fixture parametrization <fixture-parametrize>` |  | ||||||
|   mechanisms together with ``yield``-style fixtures. |  | ||||||
| 
 |  | ||||||
| - lastly ``yield`` introduces more than one way to write |  | ||||||
|   fixture functions, so what's the obvious way to a newcomer? |  | ||||||
| 
 |  | ||||||
| If you want to feedback or participate in discussion of the above |  | ||||||
| topics, please join our :ref:`contact channels`, you are most welcome. |  | ||||||
|  |  | ||||||
|  | @ -2597,11 +2597,13 @@ class TestShowFixtures: | ||||||
|         ''') |         ''') | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @pytest.mark.parametrize('flavor', ['fixture', 'yield_fixture']) | ||||||
| class TestContextManagerFixtureFuncs: | class TestContextManagerFixtureFuncs: | ||||||
|     def test_simple(self, testdir): | 
 | ||||||
|  |     def test_simple(self, testdir, flavor): | ||||||
|         testdir.makepyfile(""" |         testdir.makepyfile(""" | ||||||
|             import pytest |             import pytest | ||||||
|             @pytest.yield_fixture |             @pytest.{flavor} | ||||||
|             def arg1(): |             def arg1(): | ||||||
|                 print ("setup") |                 print ("setup") | ||||||
|                 yield 1 |                 yield 1 | ||||||
|  | @ -2611,7 +2613,7 @@ class TestContextManagerFixtureFuncs: | ||||||
|             def test_2(arg1): |             def test_2(arg1): | ||||||
|                 print ("test2 %s" % arg1) |                 print ("test2 %s" % arg1) | ||||||
|                 assert 0 |                 assert 0 | ||||||
|         """) |         """.format(flavor=flavor)) | ||||||
|         result = testdir.runpytest("-s") |         result = testdir.runpytest("-s") | ||||||
|         result.stdout.fnmatch_lines(""" |         result.stdout.fnmatch_lines(""" | ||||||
|             *setup* |             *setup* | ||||||
|  | @ -2622,10 +2624,10 @@ class TestContextManagerFixtureFuncs: | ||||||
|             *teardown* |             *teardown* | ||||||
|         """) |         """) | ||||||
| 
 | 
 | ||||||
|     def test_scoped(self, testdir): |     def test_scoped(self, testdir, flavor): | ||||||
|         testdir.makepyfile(""" |         testdir.makepyfile(""" | ||||||
|             import pytest |             import pytest | ||||||
|             @pytest.yield_fixture(scope="module") |             @pytest.{flavor}(scope="module") | ||||||
|             def arg1(): |             def arg1(): | ||||||
|                 print ("setup") |                 print ("setup") | ||||||
|                 yield 1 |                 yield 1 | ||||||
|  | @ -2634,7 +2636,7 @@ class TestContextManagerFixtureFuncs: | ||||||
|                 print ("test1 %s" % arg1) |                 print ("test1 %s" % arg1) | ||||||
|             def test_2(arg1): |             def test_2(arg1): | ||||||
|                 print ("test2 %s" % arg1) |                 print ("test2 %s" % arg1) | ||||||
|         """) |         """.format(flavor=flavor)) | ||||||
|         result = testdir.runpytest("-s") |         result = testdir.runpytest("-s") | ||||||
|         result.stdout.fnmatch_lines(""" |         result.stdout.fnmatch_lines(""" | ||||||
|             *setup* |             *setup* | ||||||
|  | @ -2643,94 +2645,62 @@ class TestContextManagerFixtureFuncs: | ||||||
|             *teardown* |             *teardown* | ||||||
|         """) |         """) | ||||||
| 
 | 
 | ||||||
|     def test_setup_exception(self, testdir): |     def test_setup_exception(self, testdir, flavor): | ||||||
|         testdir.makepyfile(""" |         testdir.makepyfile(""" | ||||||
|             import pytest |             import pytest | ||||||
|             @pytest.yield_fixture(scope="module") |             @pytest.{flavor}(scope="module") | ||||||
|             def arg1(): |             def arg1(): | ||||||
|                 pytest.fail("setup") |                 pytest.fail("setup") | ||||||
|                 yield 1 |                 yield 1 | ||||||
|             def test_1(arg1): |             def test_1(arg1): | ||||||
|                 pass |                 pass | ||||||
|         """) |         """.format(flavor=flavor)) | ||||||
|         result = testdir.runpytest("-s") |         result = testdir.runpytest("-s") | ||||||
|         result.stdout.fnmatch_lines(""" |         result.stdout.fnmatch_lines(""" | ||||||
|             *pytest.fail*setup* |             *pytest.fail*setup* | ||||||
|             *1 error* |             *1 error* | ||||||
|         """) |         """) | ||||||
| 
 | 
 | ||||||
|     def test_teardown_exception(self, testdir): |     def test_teardown_exception(self, testdir, flavor): | ||||||
|         testdir.makepyfile(""" |         testdir.makepyfile(""" | ||||||
|             import pytest |             import pytest | ||||||
|             @pytest.yield_fixture(scope="module") |             @pytest.{flavor}(scope="module") | ||||||
|             def arg1(): |             def arg1(): | ||||||
|                 yield 1 |                 yield 1 | ||||||
|                 pytest.fail("teardown") |                 pytest.fail("teardown") | ||||||
|             def test_1(arg1): |             def test_1(arg1): | ||||||
|                 pass |                 pass | ||||||
|         """) |         """.format(flavor=flavor)) | ||||||
|         result = testdir.runpytest("-s") |         result = testdir.runpytest("-s") | ||||||
|         result.stdout.fnmatch_lines(""" |         result.stdout.fnmatch_lines(""" | ||||||
|             *pytest.fail*teardown* |             *pytest.fail*teardown* | ||||||
|             *1 passed*1 error* |             *1 passed*1 error* | ||||||
|         """) |         """) | ||||||
| 
 | 
 | ||||||
|     def test_yields_more_than_one(self, testdir): |     def test_yields_more_than_one(self, testdir, flavor): | ||||||
|         testdir.makepyfile(""" |         testdir.makepyfile(""" | ||||||
|             import pytest |             import pytest | ||||||
|             @pytest.yield_fixture(scope="module") |             @pytest.{flavor}(scope="module") | ||||||
|             def arg1(): |             def arg1(): | ||||||
|                 yield 1 |                 yield 1 | ||||||
|                 yield 2 |                 yield 2 | ||||||
|             def test_1(arg1): |             def test_1(arg1): | ||||||
|                 pass |                 pass | ||||||
|         """) |         """.format(flavor=flavor)) | ||||||
|         result = testdir.runpytest("-s") |         result = testdir.runpytest("-s") | ||||||
|         result.stdout.fnmatch_lines(""" |         result.stdout.fnmatch_lines(""" | ||||||
|             *fixture function* |             *fixture function* | ||||||
|             *test_yields*:2* |             *test_yields*:2* | ||||||
|         """) |         """) | ||||||
| 
 | 
 | ||||||
| 
 |     def test_custom_name(self, testdir, flavor): | ||||||
|     def test_no_yield(self, testdir): |  | ||||||
|         testdir.makepyfile(""" |         testdir.makepyfile(""" | ||||||
|             import pytest |             import pytest | ||||||
|             @pytest.yield_fixture(scope="module") |             @pytest.{flavor}(name='meow') | ||||||
|             def arg1(): |  | ||||||
|                 return 1 |  | ||||||
|             def test_1(arg1): |  | ||||||
|                 pass |  | ||||||
|         """) |  | ||||||
|         result = testdir.runpytest("-s") |  | ||||||
|         result.stdout.fnmatch_lines(""" |  | ||||||
|             *yield_fixture*requires*yield* |  | ||||||
|             *yield_fixture* |  | ||||||
|             *def arg1* |  | ||||||
|         """) |  | ||||||
| 
 |  | ||||||
|     def test_yield_not_allowed_in_non_yield(self, testdir): |  | ||||||
|         testdir.makepyfile(""" |  | ||||||
|             import pytest |  | ||||||
|             @pytest.fixture(scope="module") |  | ||||||
|             def arg1(): |  | ||||||
|                 yield 1 |  | ||||||
|             def test_1(arg1): |  | ||||||
|                 pass |  | ||||||
|         """) |  | ||||||
|         result = testdir.runpytest("-s") |  | ||||||
|         result.stdout.fnmatch_lines(""" |  | ||||||
|             *fixture*cannot use*yield* |  | ||||||
|             *def arg1* |  | ||||||
|         """) |  | ||||||
| 
 |  | ||||||
|     def test_custom_name(self, testdir): |  | ||||||
|         testdir.makepyfile(""" |  | ||||||
|             import pytest |  | ||||||
|             @pytest.fixture(name='meow') |  | ||||||
|             def arg1(): |             def arg1(): | ||||||
|                 return 'mew' |                 return 'mew' | ||||||
|             def test_1(meow): |             def test_1(meow): | ||||||
|                 print(meow) |                 print(meow) | ||||||
|         """) |         """.format(flavor=flavor)) | ||||||
|         result = testdir.runpytest("-s") |         result = testdir.runpytest("-s") | ||||||
|         result.stdout.fnmatch_lines("*mew*") |         result.stdout.fnmatch_lines("*mew*") | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue