Skip to content

Make normal fixtures work with "yield" #1586

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down
56 changes: 22 additions & 34 deletions _pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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()

Expand Down Expand Up @@ -2287,7 +2286,6 @@ def parsefactories(self, node_or_obj, nodeid=NOTSET, unittest=False):
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:
Expand Down Expand Up @@ -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
Expand All @@ -2367,7 +2357,6 @@ def __init__(self, fixturemanager, baseid, argname, func, scope, params,
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 = []
Expand Down Expand Up @@ -2428,8 +2417,7 @@ def execute(self, request):
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
Expand Down
4 changes: 2 additions & 2 deletions doc/en/example/costlysetup/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
@pytest.fixture("session")
def setup(request):
setup = CostlySetup()
request.addfinalizer(setup.finalize)
return setup
yield setup
setup.finalize()

class CostlySetup:
def __init__(self):
Expand Down
17 changes: 8 additions & 9 deletions doc/en/example/simple.rst
Original file line number Diff line number Diff line change
Expand Up @@ -648,15 +648,14 @@ here is a little example implemented via a local plugin::

@pytest.fixture
def something(request):
def fin():
# request.node is an "item" because we use the default
# "function" scope
if request.node.rep_setup.failed:
print ("setting up a test failed!", request.node.nodeid)
elif request.node.rep_setup.passed:
if request.node.rep_call.failed:
print ("executing test failed", request.node.nodeid)
request.addfinalizer(fin)
yield
# request.node is an "item" because we use the default
# "function" scope
if request.node.rep_setup.failed:
print ("setting up a test failed!", request.node.nodeid)
elif request.node.rep_setup.passed:
if request.node.rep_call.failed:
print ("executing test failed", request.node.nodeid)


if you then have failing tests::
Expand Down
112 changes: 69 additions & 43 deletions doc/en/fixture.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`:
Expand Down Expand Up @@ -247,9 +242,8 @@ 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

Expand All @@ -259,14 +253,12 @@ or multiple times::
@pytest.fixture(scope="module")
def smtp(request):
smtp = smtplib.SMTP("smtp.gmail.com")
def fin():
print ("teardown smtp")
smtp.close()
request.addfinalizer(fin)
return smtp # provide the fixture value
yield smtp # provide the fixture value
print("teardown smtp")
smtp.close()

The ``fin`` function will execute when the last test using
the fixture in the module has finished execution.
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::

Expand All @@ -282,14 +274,55 @@ 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::

Finalization/teardown with yield fixtures
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
# content of test_yield2.py

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>`.
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

import smtplib
import pytest

@pytest.fixture(scope="module")
def smtp(request):
smtp = smtplib.SMTP("smtp.gmail.com")
def fin():
print ("teardown smtp")
smtp.close()
request.addfinalizer(fin)
return smtp # provide the fixture value

The ``fin`` function will execute when the last test using
the fixture in the module has finished execution.

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

Expand All @@ -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():
print ("finalizing %s (%s)" % (smtp, server))
smtp.close()
request.addfinalizer(fin)
return smtp
yield smtp
print ("finalizing %s (%s)" % (smtp, server))
smtp.close()

We use the ``request.module`` attribute to optionally obtain an
``smtpserver`` attribute from the test module. If we just execute
Expand Down Expand Up @@ -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
Expand All @@ -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():
print ("finalizing %s" % smtp)
smtp.close()
request.addfinalizer(fin)
return smtp
yield smtp
print ("finalizing %s" % smtp)
smtp.close()

The main change is the declaration of ``params`` with
:py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values
Expand Down Expand Up @@ -586,19 +614,15 @@ to show the setup/teardown flow::
def modarg(request):
param = request.param
print (" SETUP modarg %s" % param)
def fin():
print (" TEARDOWN modarg %s" % param)
request.addfinalizer(fin)
return param
yield param
print (" TEARDOWN modarg %s" % param)

@pytest.fixture(scope="function", params=[1,2])
def otherarg(request):
param = request.param
print (" SETUP otherarg %s" % param)
def fin():
print (" TEARDOWN otherarg %s" % param)
request.addfinalizer(fin)
return param
yield param
print (" TEARDOWN otherarg %s" % param)

def test_0(otherarg):
print (" RUN test0 with otherarg %s" % otherarg)
Expand Down Expand Up @@ -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"]
Expand Down Expand Up @@ -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::

Expand Down
Loading