1 new commit in pytest:
https://bitbucket.org/hpk42/pytest/commits/e4ca07d8ab69/
Changeset: e4ca07d8ab69
User: hpk42
Date: 2013-09-26 12:57:21
Summary: introduce yieldctx=True in the @pytest.fixture decorator. Refactor
tests and docs.
Affected #: 3 files
diff -r 5ac2e164e7b41350274ce40f0495f82817e3c68c -r
e4ca07d8ab69b50c6a13d04cbff8417d5a2ad0b0 _pytest/python.py
--- a/_pytest/python.py
+++ b/_pytest/python.py
@@ -26,19 +26,21 @@
class FixtureFunctionMarker:
- def __init__(self, scope, params, autouse=False):
+ def __init__(self, scope, params, autouse=False, yieldctx=False):
self.scope = scope
self.params = params
self.autouse = autouse
+ self.yieldctx = yieldctx
def __call__(self, function):
if inspect.isclass(function):
- raise ValueError("class fixtures not supported (may be in the
future)")
+ raise ValueError(
+ "class fixtures not supported (may be in the future)")
function._pytestfixturefunction = self
return function
-def fixture(scope="function", params=None, autouse=False):
+def fixture(scope="function", params=None, autouse=False, yieldctx=False):
""" (return a) decorator to mark a fixture factory function.
This decorator can be used (with or or without parameters) to define
@@ -59,12 +61,17 @@
:arg autouse: if True, the fixture func is activated for all tests that
can see it. If False (the default) then an explicit
reference is needed to activate the fixture.
+
+ :arg yieldctx: if True, the fixture function yields a fixture value.
+ Code after such a ``yield`` statement is treated as
+ teardown code.
"""
if callable(scope) and params is None and autouse == False:
# direct decoration
- return FixtureFunctionMarker("function", params, autouse)(scope)
+ return FixtureFunctionMarker(
+ "function", params, autouse, yieldctx)(scope)
else:
- return FixtureFunctionMarker(scope, params, autouse=autouse)
+ return FixtureFunctionMarker(scope, params, autouse, yieldctx)
defaultfuncargprefixmarker = fixture()
@@ -1616,6 +1623,7 @@
assert not name.startswith(self._argprefix)
fixturedef = FixtureDef(self, nodeid, name, obj,
marker.scope, marker.params,
+ marker.yieldctx,
unittest=unittest)
faclist = self._arg2fixturedefs.setdefault(name, [])
if not fixturedef.has_location:
@@ -1656,8 +1664,18 @@
except ValueError:
pass
-def call_fixture_func(fixturefunc, request, kwargs):
- if is_generator(fixturefunc):
+def fail_fixturefunc(fixturefunc, msg):
+ fs, lineno = getfslineno(fixturefunc)
+ location = "%s:%s" % (fs, lineno+1)
+ source = py.code.Source(fixturefunc)
+ pytest.fail(msg + ":\n\n" + str(source.indent()) + "\n" + location,
+ pytrace=False)
+
+def call_fixture_func(fixturefunc, request, kwargs, yieldctx):
+ if yieldctx:
+ if not is_generator(fixturefunc):
+ fail_fixturefunc(fixturefunc,
+ msg="yieldctx=True requires yield statement")
iter = fixturefunc(**kwargs)
next = getattr(iter, "__next__", None)
if next is None:
@@ -1669,11 +1687,8 @@
except StopIteration:
pass
else:
- fs, lineno = getfslineno(fixturefunc)
- location = "%s:%s" % (fs, lineno+1)
- pytest.fail(
- "fixture function %s has more than one 'yield': \n%s" %
- (fixturefunc.__name__, location), pytrace=False)
+ fail_fixturefunc(fixturefunc,
+ "fixture function has more than one 'yield'")
request.addfinalizer(teardown)
else:
res = fixturefunc(**kwargs)
@@ -1682,7 +1697,7 @@
class FixtureDef:
""" A container for a factory definition. """
def __init__(self, fixturemanager, baseid, argname, func, scope, params,
- unittest=False):
+ yieldctx, unittest=False):
self._fixturemanager = fixturemanager
self.baseid = baseid or ''
self.has_location = baseid is not None
@@ -1693,6 +1708,7 @@
self.params = params
startindex = unittest and 1 or None
self.argnames = getfuncargnames(func, startindex=startindex)
+ self.yieldctx = yieldctx
self.unittest = unittest
self._finalizer = []
@@ -1730,7 +1746,8 @@
fixturefunc = fixturefunc.__get__(request.instance)
except AttributeError:
pass
- result = call_fixture_func(fixturefunc, request, kwargs)
+ result = call_fixture_func(fixturefunc, request, kwargs,
+ self.yieldctx)
assert not hasattr(self, "cached_result")
self.cached_result = result
return result
diff -r 5ac2e164e7b41350274ce40f0495f82817e3c68c -r
e4ca07d8ab69b50c6a13d04cbff8417d5a2ad0b0 doc/en/fixture.txt
--- a/doc/en/fixture.txt
+++ b/doc/en/fixture.txt
@@ -40,7 +40,7 @@
.. _`@pytest.fixture`:
.. _`pytest.fixture`:
-Fixtures as Function arguments (funcargs)
+Fixtures as Function arguments
-----------------------------------------
Test functions can receive fixture objects by naming them as an input
@@ -70,7 +70,8 @@
$ py.test test_smtpsimple.py
=========================== test session starts
============================
- platform linux2 -- Python 2.7.3 -- pytest-2.3.5
+ platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev12
+ plugins: xdist, pep8, cov, cache, capturelog, instafail
collected 1 items
test_smtpsimple.py F
@@ -78,7 +79,7 @@
================================= FAILURES
=================================
________________________________ test_ehlo
_________________________________
- smtp = <smtplib.SMTP instance at 0x226cc20>
+ smtp = <smtplib.SMTP instance at 0x1ac66c8>
def test_ehlo(smtp):
response, msg = smtp.ehlo()
@@ -88,7 +89,7 @@
E assert 0
test_smtpsimple.py:12: AssertionError
- ========================= 1 failed in 0.20 seconds
=========================
+ ========================= 1 failed in 0.17 seconds
=========================
In the failure traceback we see that the test function was called with a
``smtp`` argument, the ``smtplib.SMTP()`` instance created by the fixture
@@ -123,7 +124,7 @@
but is not anymore advertised as the primary means of declaring fixture
functions.
-Funcargs a prime example of dependency injection
+"Funcargs" a prime example of dependency injection
---------------------------------------------------
When injecting fixtures to test functions, pytest-2.0 introduced the
@@ -142,7 +143,7 @@
.. _smtpshared:
-Working with a module-shared fixture
+Sharing a fixture across tests in a module (or class/session)
-----------------------------------------------------------------
.. regendoc:wipe
@@ -188,7 +189,8 @@
$ py.test test_module.py
=========================== test session starts
============================
- platform linux2 -- Python 2.7.3 -- pytest-2.3.5
+ platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev12
+ plugins: xdist, pep8, cov, cache, capturelog, instafail
collected 2 items
test_module.py FF
@@ -196,7 +198,7 @@
================================= FAILURES
=================================
________________________________ test_ehlo
_________________________________
- smtp = <smtplib.SMTP instance at 0x18a6368>
+ smtp = <smtplib.SMTP instance at 0x15b2d88>
def test_ehlo(smtp):
response = smtp.ehlo()
@@ -208,7 +210,7 @@
test_module.py:6: AssertionError
________________________________ test_noop
_________________________________
- smtp = <smtplib.SMTP instance at 0x18a6368>
+ smtp = <smtplib.SMTP instance at 0x15b2d88>
def test_noop(smtp):
response = smtp.noop()
@@ -217,7 +219,7 @@
E assert 0
test_module.py:11: AssertionError
- ========================= 2 failed in 0.26 seconds
=========================
+ ========================= 2 failed in 0.16 seconds
=========================
You see the two ``assert 0`` failing and more importantly you can also see
that the same (module-scoped) ``smtp`` object was passed into the two
@@ -233,62 +235,17 @@
# the returned fixture value will be shared for
# all tests needing it
-.. _`contextfixtures`:
+.. _`finalization`:
-fixture finalization / teardowns
+fixture finalization / executing teardown code
-------------------------------------------------------------
-pytest supports two styles of fixture finalization:
+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::
-- (new in pytest-2.4) by writing a contextmanager fixture
- generator where a fixture value is "yielded" and the remainder
- of the function serves as the teardown code. This integrates
- very well with existing context managers.
-
-- by making a fixture function accept a ``request`` argument
- with which it can call ``request.addfinalizer(teardownfunction)``
- to register a teardown callback function.
-
-Both methods are strictly equivalent from pytest's view and will
-remain supported in the future.
-
-Because a number of people prefer the new contextmanager style
-we describe it first::
-
- # content of test_ctxfixture.py
-
- import smtplib
- import pytest
-
- @pytest.fixture(scope="module")
- def smtp():
- smtp = smtplib.SMTP("merlinux.eu")
- yield smtp # provide the fixture value
- print ("teardown smtp")
- smtp.close()
-
-pytest detects that you are using a ``yield`` in your fixture function,
-turns it into a generator and:
-
-a) iterates once into it for producing the value
-b) iterates a second time for tearing the fixture down, expecting
- a StopIteration (which is produced automatically from the Python
- runtime when the generator returns).
-
-.. note::
-
- The teardown will execute independently of the status of test functions.
- You do not need to write the teardown code into a ``try-finally`` clause
- like you would usually do with ``contextlib.contextmanager`` decorated
- functions.
-
- If the fixture generator yields a second value pytest will report
- an error. Yielding cannot be used for parametrization. We'll describe
- ways to implement parametrization further below.
-
-Prior to pytest-2.4 you always needed to register a finalizer by accepting
-a ``request`` object into your fixture function and calling
-``request.addfinalizer`` with a teardown function::
+ # content of conftest.py
import smtplib
import pytest
@@ -299,24 +256,38 @@
def fin():
print ("teardown smtp")
smtp.close()
+ request.addfinalizer(fin)
return smtp # provide the fixture value
-This method of registering a finalizer reads more indirect
-than the new contextmanager style syntax because ``fin``
-is a callback function.
+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
+ FF
+ 2 failed in 0.16 seconds
+ teardown smtp
+
+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 pytest-2.4 introduced an alternative `yield-context <yieldctx>`_
+mechanism which allows to interact nicely with context managers.
.. _`request-context`:
-Fixtures can interact with the requesting test context
+Fixtures can introspect the requesting test context
-------------------------------------------------------------
-pytest provides a builtin :py:class:`request <FixtureRequest>` object,
-which fixture functions can use to introspect the function, class or module
-for which they are invoked.
-
+Fixture function can accept the :py:class:`request <FixtureRequest>` object
+to introspect the "requesting" test function, class or module context.
Further extending the previous ``smtp`` fixture example, let's
-read an optional server URL from the module namespace::
+read an optional server URL from the test module which uses our fixture::
# content of conftest.py
import pytest
@@ -326,22 +297,21 @@
def smtp(request):
server = getattr(request.module, "smtpserver", "merlinux.eu")
smtp = smtplib.SMTP(server)
- yield smtp # provide the fixture
- print ("finalizing %s" % smtp)
- smtp.close()
+
+ def fin():
+ print ("finalizing %s (%s)" % (smtp, server))
+ smtp.close()
+
+ return smtp
-The finalizing part after the ``yield smtp`` statement will execute
-when the last test using the ``smtp`` fixture has executed::
+We use the ``request.module`` attribute to optionally obtain an
+``smtpserver`` attribute from the test module. If we just execute
+again, nothing much has changed::
$ py.test -s -q --tb=no
FF
- finalizing <smtplib.SMTP instance at 0x1e10248>
-
-We see that the ``smtp`` instance is finalized after the two
-tests which use it finished executin. If we rather specify
-``scope='function'`` then fixture setup and cleanup occurs
-around each single test. Note that in either case the test
-module itself does not need to change!
+ 2 failed in 0.17 seconds
+ teardown smtp
Let's quickly create another test module that actually sets the
server URL in its module namespace::
@@ -361,12 +331,11 @@
______________________________ test_showhelo
_______________________________
test_anothersmtp.py:5: in test_showhelo
> assert 0, smtp.helo()
- E AssertionError: (250, 'mail.python.org')
+ E AssertionError: (250, 'hq.merlinux.eu')
voila! The ``smtp`` fixture function picked up our mail server name
from the module namespace.
-
.. _`fixture-parametrize`:
Parametrizing a fixture
@@ -392,9 +361,11 @@
params=["merlinux.eu", "mail.python.org"])
def smtp(request):
smtp = smtplib.SMTP(request.param)
- yield smtp
- print ("finalizing %s" % smtp)
- smtp.close()
+ def fin():
+ 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
@@ -407,7 +378,7 @@
================================= FAILURES
=================================
__________________________ test_ehlo[merlinux.eu]
__________________________
- smtp = <smtplib.SMTP instance at 0x1b38a28>
+ smtp = <smtplib.SMTP instance at 0x1d1b680>
def test_ehlo(smtp):
response = smtp.ehlo()
@@ -419,7 +390,7 @@
test_module.py:6: AssertionError
__________________________ test_noop[merlinux.eu]
__________________________
- smtp = <smtplib.SMTP instance at 0x1b38a28>
+ smtp = <smtplib.SMTP instance at 0x1d1b680>
def test_noop(smtp):
response = smtp.noop()
@@ -430,7 +401,7 @@
test_module.py:11: AssertionError
________________________ test_ehlo[mail.python.org]
________________________
- smtp = <smtplib.SMTP instance at 0x1b496c8>
+ smtp = <smtplib.SMTP instance at 0x1d237e8>
def test_ehlo(smtp):
response = smtp.ehlo()
@@ -441,7 +412,7 @@
test_module.py:5: AssertionError
________________________ test_noop[mail.python.org]
________________________
- smtp = <smtplib.SMTP instance at 0x1b496c8>
+ smtp = <smtplib.SMTP instance at 0x1d237e8>
def test_noop(smtp):
response = smtp.noop()
@@ -450,6 +421,7 @@
E assert 0
test_module.py:11: AssertionError
+ 4 failed in 6.04 seconds
We see that our two test functions each ran twice, against the different
``smtp`` instances. Note also, that with the ``mail.python.org``
@@ -489,13 +461,15 @@
$ py.test -v test_appsetup.py
=========================== test session starts
============================
- platform linux2 -- Python 2.7.3 -- pytest-2.3.5 --
/home/hpk/p/pytest/.tox/regen/bin/python
+ platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev12 --
/home/hpk/venv/0/bin/python
+ cachedir: /tmp/doc-exec-120/.cache
+ plugins: xdist, pep8, cov, cache, capturelog, instafail
collecting ... collected 2 items
+ test_appsetup.py:12: test_smtp_exists[mail.python.org] PASSED
test_appsetup.py:12: test_smtp_exists[merlinux.eu] PASSED
- test_appsetup.py:12: test_smtp_exists[mail.python.org] PASSED
- ========================= 2 passed in 5.38 seconds
=========================
+ ========================= 2 passed in 6.98 seconds
=========================
Due to the parametrization of ``smtp`` the test will run twice with two
different ``App`` instances and respective smtp servers. There is no
@@ -534,8 +508,9 @@
def modarg(request):
param = request.param
print "create", param
- yield param
- print ("fin %s" % param)
+ def fin():
+ print ("fin %s" % param)
+ return param
@pytest.fixture(scope="function", params=[1,2])
def otherarg(request):
@@ -552,31 +527,31 @@
$ py.test -v -s test_module.py
=========================== test session starts
============================
- platform linux2 -- Python 2.7.3 -- pytest-2.3.5 --
/home/hpk/p/pytest/.tox/regen/bin/python
+ platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev12 --
/home/hpk/venv/0/bin/python
+ cachedir: /tmp/doc-exec-120/.cache
+ plugins: xdist, pep8, cov, cache, capturelog, instafail
collecting ... collected 8 items
- test_module.py:16: test_0[1] PASSED
- test_module.py:16: test_0[2] PASSED
- test_module.py:18: test_1[mod1] PASSED
- test_module.py:20: test_2[1-mod1] PASSED
- test_module.py:20: test_2[2-mod1] PASSED
- test_module.py:18: test_1[mod2] PASSED
- test_module.py:20: test_2[1-mod2] PASSED
- test_module.py:20: test_2[2-mod2] PASSED
+ test_module.py:15: test_0[1] PASSED
+ test_module.py:15: test_0[2] PASSED
+ test_module.py:17: test_1[mod1] PASSED
+ test_module.py:19: test_2[1-mod1] PASSED
+ test_module.py:19: test_2[2-mod1] PASSED
+ test_module.py:17: test_1[mod2] PASSED
+ test_module.py:19: test_2[1-mod2] PASSED
+ test_module.py:19: test_2[2-mod2] PASSED
- ========================= 8 passed in 0.01 seconds
=========================
+ ========================= 8 passed in 0.02 seconds
=========================
test0 1
test0 2
create mod1
test1 mod1
test2 1 mod1
test2 2 mod1
- fin mod1
create mod2
test1 mod2
test2 1 mod2
test2 2 mod2
- fin mod2
You can see that the parametrized module-scoped ``modarg`` resource caused
an ordering of test execution that lead to the fewest possible "active"
resources. The finalizer for the ``mod1`` parametrized resource was executed
@@ -632,6 +607,7 @@
$ py.test -q
..
+ 2 passed in 0.02 seconds
You can specify multiple fixtures like this::
@@ -702,6 +678,7 @@
$ py.test -q
..
+ 2 passed in 0.02 seconds
Here is how autouse fixtures work in other scopes:
@@ -750,3 +727,62 @@
fixtures functions starts at test classes, then test modules, then
``conftest.py`` files and finally builtin and third party plugins.
+
+.. _yieldctx:
+
+Fixture functions using "yield" / context manager integration
+---------------------------------------------------------------
+
+.. versionadded:: 2.4
+
+pytest-2.4 allows fixture functions to use a ``yield`` instead
+of a ``return`` statement to provide a fixture value. Let's
+look at a quick example before discussing advantages::
+
+ # content of conftest.py
+
+ import smtplib
+ import pytest
+
+ @pytest.fixture(scope="module", yieldctx=True)
+ def smtp():
+ smtp = smtplib.SMTP("merlinux.eu")
+ yield smtp # provide the fixture value
+ print ("teardown smtp after a yield")
+ smtp.close()
+
+In contrast to the `finalization`_ example, our fixture
+function uses a single ``yield`` to provide the ``smtp`` fixture
+value. The code after the ``yield`` statement serves as the
+teardown code, avoiding the indirection of registering a
+teardown function. More importantly, it also allows to
+seemlessly re-use existing context managers, for example::
+
+ @pytest.fixture(yieldctx=True)
+ def somefixture():
+ with open("somefile") as f:
+ yield f.readlines()
+
+The file ``f`` will be closed once ``somefixture`` goes out of scope.
+It is possible to achieve the same result by using a ``request.addfinalizer``
+call but it is more boilerplate and not very obvious unless
+you know about the exact ``__enter__|__exit__`` protocol of with-style
+context managers.
+
+For some background, here is the protocol pytest follows for when
+``yieldctx=True`` is specified in the fixture decorator:
+
+a) iterate once into the generator for producing the value
+b) iterate a second time for tearing the fixture down, expecting
+ a StopIteration (which is produced automatically from the Python
+ runtime when the generator returns).
+
+The teardown will always execute, independently of the outcome of
+test functions. You do **not need** to write the teardown code into a
+``try-finally`` clause like you would usually do with
+:py:func:`contextlib.contextmanager` decorated functions.
+
+If the fixture generator yields a second value pytest will report
+an error. Yielding cannot be used for parametrization, rather
+see `fixture-parametrize`_.
+
diff -r 5ac2e164e7b41350274ce40f0495f82817e3c68c -r
e4ca07d8ab69b50c6a13d04cbff8417d5a2ad0b0 testing/python/fixture.py
--- a/testing/python/fixture.py
+++ b/testing/python/fixture.py
@@ -1980,7 +1980,7 @@
def test_simple(self, testdir):
testdir.makepyfile("""
import pytest
- @pytest.fixture
+ @pytest.fixture(yieldctx=True)
def arg1():
print ("setup")
yield 1
@@ -2004,7 +2004,7 @@
def test_scoped(self, testdir):
testdir.makepyfile("""
import pytest
- @pytest.fixture(scope="module")
+ @pytest.fixture(scope="module", yieldctx=True)
def arg1():
print ("setup")
yield 1
@@ -2025,7 +2025,7 @@
def test_setup_exception(self, testdir):
testdir.makepyfile("""
import pytest
- @pytest.fixture(scope="module")
+ @pytest.fixture(scope="module", yieldctx=True)
def arg1():
pytest.fail("setup")
yield 1
@@ -2041,7 +2041,7 @@
def test_teardown_exception(self, testdir):
testdir.makepyfile("""
import pytest
- @pytest.fixture(scope="module")
+ @pytest.fixture(scope="module", yieldctx=True)
def arg1():
yield 1
pytest.fail("teardown")
@@ -2054,11 +2054,10 @@
*1 passed*1 error*
""")
-
def test_yields_more_than_one(self, testdir):
testdir.makepyfile("""
import pytest
- @pytest.fixture(scope="module")
+ @pytest.fixture(scope="module", yieldctx=True)
def arg1():
yield 1
yield 2
@@ -2072,3 +2071,20 @@
""")
+ def test_no_yield(self, testdir):
+ testdir.makepyfile("""
+ import pytest
+ @pytest.fixture(scope="module", yieldctx=True)
+ def arg1():
+ return 1
+ def test_1(arg1):
+ pass
+ """)
+ result = testdir.runpytest("-s")
+ result.stdout.fnmatch_lines("""
+ *yieldctx*requires*yield*
+ *yieldctx=True*
+ *def arg1*
+ """)
+
+
Repository URL: https://bitbucket.org/hpk42/pytest/
--
This is a commit notification from bitbucket.org. You are receiving
this because you have the service enabled, addressing the recipient of
this email.
_______________________________________________
pytest-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pytest-commit