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
pytest-commit@python.org
https://mail.python.org/mailman/listinfo/pytest-commit

Reply via email to