Merge pull request #1586 from nicoddemus/issue-1461-merge-yield-fixture

Make normal fixtures work with "yield"
This commit is contained in:
Ronny Pfannschmidt 2016-06-14 10:31:09 +02:00 committed by GitHub
commit feeee2803e
7 changed files with 138 additions and 230 deletions

View File

@ -36,6 +36,12 @@
**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`_):
explicitly passed parametrize ids do not get escaped to ascii.
Thanks `@ceridwen`_ for the PR.
@ -58,6 +64,7 @@
*
.. _@milliams: https://github.com/milliams
.. _@csaftoiu: https://github.com/csaftoiu
.. _@novas0x2a: https://github.com/novas0x2a
.. _@kalekundert: https://github.com/kalekundert
.. _@tareqalayan: https://github.com/tareqalayan
@ -72,6 +79,7 @@
.. _#1441: https://github.com/pytest-dev/pytest/pull/1441
.. _#1454: https://github.com/pytest-dev/pytest/pull/1454
.. _#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
.. _#1474: https://github.com/pytest-dev/pytest/pull/1474
.. _#1502: https://github.com/pytest-dev/pytest/pull/1502

View File

@ -116,12 +116,10 @@ def safe_getattr(object, name, default):
class FixtureFunctionMarker:
def __init__(self, scope, params,
autouse=False, yieldctx=False, ids=None, name=None):
def __init__(self, scope, params, autouse=False, ids=None, name=None):
self.scope = scope
self.params = params
self.autouse = autouse
self.yieldctx = yieldctx
self.ids = ids
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
``fixture_<fixturename>`` and then use
``@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:
# direct decoration
@ -175,22 +177,19 @@ def fixture(scope="function", params=None, autouse=False, ids=None, name=None):
params = list(params)
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
expects a fixture function to use a ``yield`` instead of a ``return``
statement to provide a fixture. See
http://pytest.org/en/latest/yieldfixture.html for more info.
def yield_fixture(scope="function", params=None, autouse=False, ids=None, name=None):
""" (return a) decorator to mark a yield-fixture factory function.
.. 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
return FixtureFunctionMarker(
"function", params, autouse, yieldctx=True)(scope)
"function", params, autouse, ids=ids, name=name)(scope)
else:
return FixtureFunctionMarker(scope, params, autouse,
yieldctx=True, ids=ids)
return FixtureFunctionMarker(scope, params, autouse, ids=ids, name=name)
defaultfuncargprefixmarker = fixture()
@ -2287,7 +2286,6 @@ class FixtureManager:
assert not name.startswith(self._argprefix)
fixturedef = FixtureDef(self, nodeid, name, obj,
marker.scope, marker.params,
yieldctx=marker.yieldctx,
unittest=unittest, ids=marker.ids)
faclist = self._arg2fixturedefs.setdefault(name, [])
if fixturedef.has_location:
@ -2325,38 +2323,30 @@ def fail_fixturefunc(fixturefunc, msg):
pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location,
pytrace=False)
def call_fixture_func(fixturefunc, request, kwargs, yieldctx):
def call_fixture_func(fixturefunc, request, kwargs):
yieldctx = is_generator(fixturefunc)
if yieldctx:
if not is_generator(fixturefunc):
fail_fixturefunc(fixturefunc,
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()
it = fixturefunc(**kwargs)
res = next(it)
def teardown():
try:
next()
next(it)
except StopIteration:
pass
else:
fail_fixturefunc(fixturefunc,
"yield_fixture function has more than one 'yield'")
request.addfinalizer(teardown)
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)
return res
class FixtureDef:
""" A container for a factory definition. """
def __init__(self, fixturemanager, baseid, argname, func, scope, params,
yieldctx, unittest=False, ids=None):
unittest=False, ids=None):
self._fixturemanager = fixturemanager
self.baseid = baseid or ''
self.has_location = baseid is not None
@ -2367,7 +2357,6 @@ class FixtureDef:
self.params = params
startindex = unittest and 1 or None
self.argnames = getfuncargnames(func, startindex=startindex)
self.yieldctx = yieldctx
self.unittest = unittest
self.ids = ids
self._finalizer = []
@ -2428,8 +2417,7 @@ class FixtureDef:
fixturefunc = fixturefunc.__get__(request.instance)
try:
result = call_fixture_func(fixturefunc, request, kwargs,
self.yieldctx)
result = call_fixture_func(fixturefunc, request, kwargs)
except Exception:
self.cached_result = (None, my_cache_key, sys.exc_info())
raise

View File

@ -4,8 +4,8 @@ import pytest
@pytest.fixture("session")
def setup(request):
setup = CostlySetup()
request.addfinalizer(setup.finalize)
return setup
yield setup
setup.finalize()
class CostlySetup:
def __init__(self):

View File

@ -648,7 +648,7 @@ here is a little example implemented via a local plugin::
@pytest.fixture
def something(request):
def fin():
yield
# request.node is an "item" because we use the default
# "function" scope
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:
if request.node.rep_call.failed:
print ("executing test failed", request.node.nodeid)
request.addfinalizer(fin)
if you then have failing tests::

View File

@ -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
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`:
.. _`funcarg mechanism`:
@ -247,9 +242,67 @@ Fixture finalization / executing teardown code
-------------------------------------------------------------
pytest supports execution of fixture specific finalization code
when the fixture goes out of scope. By accepting a ``request`` object
into your fixture function you can call its ``request.addfinalizer`` one
or multiple times::
when the fixture goes out of scope. By using a ``yield`` statement instead of ``return``, all
the code after the *yield* statement serves as the teardown code.::
# 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
@ -268,28 +321,8 @@ or multiple times::
The ``fin`` function will execute when the last test using
the fixture in the module has finished execution.
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.
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>`.
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.
.. _`request-context`:
@ -309,12 +342,9 @@ read an optional server URL from the test module which uses our fixture::
def smtp(request):
server = getattr(request.module, "smtpserver", "smtp.gmail.com")
smtp = smtplib.SMTP(server)
def fin():
yield smtp
print ("finalizing %s (%s)" % (smtp, server))
smtp.close()
request.addfinalizer(fin)
return smtp
We use the ``request.module`` attribute to optionally obtain an
``smtpserver`` attribute from the test module. If we just execute
@ -351,7 +381,7 @@ from the module namespace.
.. _`fixture-parametrize`:
Parametrizing a fixture
Parametrizing fixtures
-----------------------------------------------------------------
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"])
def smtp(request):
smtp = smtplib.SMTP(request.param)
def fin():
yield smtp
print ("finalizing %s" % smtp)
smtp.close()
request.addfinalizer(fin)
return smtp
The main change is the declaration of ``params`` with
:py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values
@ -586,19 +614,15 @@ to show the setup/teardown flow::
def modarg(request):
param = request.param
print (" SETUP modarg %s" % param)
def fin():
yield param
print (" TEARDOWN modarg %s" % param)
request.addfinalizer(fin)
return param
@pytest.fixture(scope="function", params=[1,2])
def otherarg(request):
param = request.param
print (" SETUP otherarg %s" % param)
def fin():
yield param
print (" TEARDOWN otherarg %s" % param)
request.addfinalizer(fin)
return param
def test_0(otherarg):
print (" RUN test0 with otherarg %s" % otherarg)
@ -777,7 +801,8 @@ self-contained implementation of this idea::
@pytest.fixture(autouse=True)
def transact(self, request, db):
db.begin(request.function.__name__)
request.addfinalizer(db.rollback)
yield
db.rollback()
def test_method1(self, db):
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``::
# content of conftest.py
@pytest.fixture()
@pytest.fixture
def transact(self, request, db):
db.begin()
request.addfinalizer(db.rollback)
yield
db.rollback()
and then e.g. have a TestClass using it by declaring the need::

View File

@ -1,100 +1,17 @@
.. _yieldfixture:
Fixture functions using "yield" / context manager integration
"yield_fixture" functions
---------------------------------------------------------------
.. deprecated:: 2.10
.. 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
of a ``return`` statement to provide a fixture value while otherwise
fully supporting all other fixture features.
Marking functions as ``yield_fixture`` is still supported, but deprecated and should not
be used in new code.
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.

View File

@ -2597,11 +2597,13 @@ class TestShowFixtures:
''')
@pytest.mark.parametrize('flavor', ['fixture', 'yield_fixture'])
class TestContextManagerFixtureFuncs:
def test_simple(self, testdir):
def test_simple(self, testdir, flavor):
testdir.makepyfile("""
import pytest
@pytest.yield_fixture
@pytest.{flavor}
def arg1():
print ("setup")
yield 1
@ -2611,7 +2613,7 @@ class TestContextManagerFixtureFuncs:
def test_2(arg1):
print ("test2 %s" % arg1)
assert 0
""")
""".format(flavor=flavor))
result = testdir.runpytest("-s")
result.stdout.fnmatch_lines("""
*setup*
@ -2622,10 +2624,10 @@ class TestContextManagerFixtureFuncs:
*teardown*
""")
def test_scoped(self, testdir):
def test_scoped(self, testdir, flavor):
testdir.makepyfile("""
import pytest
@pytest.yield_fixture(scope="module")
@pytest.{flavor}(scope="module")
def arg1():
print ("setup")
yield 1
@ -2634,7 +2636,7 @@ class TestContextManagerFixtureFuncs:
print ("test1 %s" % arg1)
def test_2(arg1):
print ("test2 %s" % arg1)
""")
""".format(flavor=flavor))
result = testdir.runpytest("-s")
result.stdout.fnmatch_lines("""
*setup*
@ -2643,94 +2645,62 @@ class TestContextManagerFixtureFuncs:
*teardown*
""")
def test_setup_exception(self, testdir):
def test_setup_exception(self, testdir, flavor):
testdir.makepyfile("""
import pytest
@pytest.yield_fixture(scope="module")
@pytest.{flavor}(scope="module")
def arg1():
pytest.fail("setup")
yield 1
def test_1(arg1):
pass
""")
""".format(flavor=flavor))
result = testdir.runpytest("-s")
result.stdout.fnmatch_lines("""
*pytest.fail*setup*
*1 error*
""")
def test_teardown_exception(self, testdir):
def test_teardown_exception(self, testdir, flavor):
testdir.makepyfile("""
import pytest
@pytest.yield_fixture(scope="module")
@pytest.{flavor}(scope="module")
def arg1():
yield 1
pytest.fail("teardown")
def test_1(arg1):
pass
""")
""".format(flavor=flavor))
result = testdir.runpytest("-s")
result.stdout.fnmatch_lines("""
*pytest.fail*teardown*
*1 passed*1 error*
""")
def test_yields_more_than_one(self, testdir):
def test_yields_more_than_one(self, testdir, flavor):
testdir.makepyfile("""
import pytest
@pytest.yield_fixture(scope="module")
@pytest.{flavor}(scope="module")
def arg1():
yield 1
yield 2
def test_1(arg1):
pass
""")
""".format(flavor=flavor))
result = testdir.runpytest("-s")
result.stdout.fnmatch_lines("""
*fixture function*
*test_yields*:2*
""")
def test_no_yield(self, testdir):
def test_custom_name(self, testdir, flavor):
testdir.makepyfile("""
import pytest
@pytest.yield_fixture(scope="module")
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')
@pytest.{flavor}(name='meow')
def arg1():
return 'mew'
def test_1(meow):
print(meow)
""")
""".format(flavor=flavor))
result = testdir.runpytest("-s")
result.stdout.fnmatch_lines("*mew*")