From commits-noreply at bitbucket.org Mon Jul 2 13:24:54 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Mon, 02 Jul 2012 11:24:54 -0000 Subject: [py-svn] commit/pytest: 2 new changesets Message-ID: <20120702112454.12029.49588@bitbucket01.managed.contegix.com> 2 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/c19b0167b115/ changeset: c19b0167b115 user: hpk42 date: 2012-07-02 13:13:48 summary: fix issue165 - fix broken links in documentation, also point to stackoverflow from FAQ and contact page affected #: 7 files diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c19b0167b11505167d7cdaf481f1358181292de5 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Changes between 2.2.4 and 2.2.5.dev ----------------------------------- +- fix issue165 - broken links in documentation - catch unicode-issues when writing failure representations to terminal to prevent the whole session from crashing - fix xfail/skip confusion: a skip-mark or an imperative pytest.skip @@ -290,7 +291,7 @@ - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/plugins.html#cmdunregister + command line, see http://pytest.org/latest/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c19b0167b11505167d7cdaf481f1358181292de5 doc/en/announce/release-2.0.1.txt --- a/doc/en/announce/release-2.0.1.txt +++ b/doc/en/announce/release-2.0.1.txt @@ -57,7 +57,7 @@ - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/plugins.html#cmdunregister + command line, see http://pytest.org/latest/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c19b0167b11505167d7cdaf481f1358181292de5 doc/en/contact.txt --- a/doc/en/contact.txt +++ b/doc/en/contact.txt @@ -9,6 +9,10 @@ 2.0 and above). You may also peek at the `old issue tracker`_ but please don't submit bugs there anymore. +- `pytest on stackoverflow.com `_ + to post questions with the tag ``pytest``. New Questions will usually + be seen by pytest users or developers. + - `Testing In Python`_: a mailing list for Python testing tools and discussion. - `py-dev developers list`_ pytest specific announcements and discussions. diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c19b0167b11505167d7cdaf481f1358181292de5 doc/en/example/parametrize.txt --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.txt @@ -122,7 +122,7 @@ A quick port of "testscenarios" ------------------------------------ -.. _`test scenarios`: http://bazaar.launchpad.net/~lifeless/testscenarios/trunk/annotate/head%3A/doc/example.py +.. _`test scenarios`: http://pypi.python.org/pypi/testscenarios/ Here is a quick port to run tests configured with `test scenarios`_, an add-on from Robert Collins for the standard unittest framework. We diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c19b0167b11505167d7cdaf481f1358181292de5 doc/en/faq.txt --- a/doc/en/faq.txt +++ b/doc/en/faq.txt @@ -3,8 +3,9 @@ .. note:: - If you don't find an answer here, checkout the :ref:`contact channels` - to get help. + If you don't find an answer here, you may checkout + `pytest Q&A at Stackoverflow `_ + or other :ref:`contact channels` to get help. On naming, nosetests, licensing and magic ------------------------------------------------ @@ -36,7 +37,7 @@ ``manage.py test`` and allows to use all pytest features_ most of which are not available from Django directly. -.. _features: test/features.html +.. _features: features.html What's this "magic" with py.test? (historic notes) @@ -93,7 +94,7 @@ Function arguments, parametrized tests and setup ------------------------------------------------------- -.. _funcargs: test/funcargs.html +.. _funcargs: funcargs.html Is using funcarg- versus xUnit setup a style question? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -107,9 +108,9 @@ because the support code can register setup/teardown functions in a managed class/module/function scope. -.. _monkeypatch: test/plugin/monkeypatch.html -.. _tmpdir: test/plugin/tmpdir.html -.. _capture: test/plugin/capture.html +.. _monkeypatch: monkeypatch.html +.. _tmpdir: tmpdir.html +.. _capture: capture.html .. _`why pytest_pyfuncarg__ methods?`: diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c19b0167b11505167d7cdaf481f1358181292de5 doc/en/projects.txt --- a/doc/en/projects.txt +++ b/doc/en/projects.txt @@ -32,7 +32,7 @@ * `XIST `_ extensible HTML/XML generator * `tiddlyweb `_ optionally headless, extensible RESTful datastore * `fancycompleter `_ for colorful tab-completion -* `Paludis `_ tools for Gentoo Paludis package manager +* `Paludis `_ tools for Gentoo Paludis package manager * `Gerald `_ schema comparison tool * `abjad `_ Python API for Formalized Score control * `bu `_ a microscopic build system diff -r 5bcd225c3b5eb502209b537546ad0f83e40de032 -r c19b0167b11505167d7cdaf481f1358181292de5 doc/en/usage.txt --- a/doc/en/usage.txt +++ b/doc/en/usage.txt @@ -130,7 +130,7 @@ and look at the content at the ``path`` location. Such files are used e.g. by the `PyPy-test`_ web page to show test results over several revisions. -.. _`PyPy-test`: http://codespeak.net:8099/summary +.. _`PyPy-test`: http://buildbot.pypy.org/summary Sending test report to pocoo pastebin service @@ -148,7 +148,7 @@ py.test --pastebin=all -Currently only pasting to the http://paste.pocoo.org service is implemented. +Currently only pasting to the http://bpaste.net service is implemented. Calling pytest from Python code ---------------------------------------------------- https://bitbucket.org/hpk42/pytest/changeset/d9a574e890f0/ changeset: d9a574e890f0 user: hpk42 date: 2012-07-02 13:23:41 summary: merge affected #: 7 files diff -r 6d5db6fbe405aacc5b828ec1f461f7699fa25664 -r d9a574e890f028e8fd520807cf1e7a88bcbc300f CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Changes between 2.2.4 and 2.3.0.dev ----------------------------------- +- fix issue165 - fix broken doc links and mention stackoverflow for FAQ - fix issue139 - merge FuncargRequest and Item API such that funcarg-functionality is now directly available on the "item" object passed to the various pytest_runtest hooks. This allows more diff -r 6d5db6fbe405aacc5b828ec1f461f7699fa25664 -r d9a574e890f028e8fd520807cf1e7a88bcbc300f doc/en/announce/release-2.0.1.txt --- a/doc/en/announce/release-2.0.1.txt +++ b/doc/en/announce/release-2.0.1.txt @@ -57,7 +57,7 @@ - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/plugins.html#cmdunregister + command line, see http://pytest.org/latest/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff -r 6d5db6fbe405aacc5b828ec1f461f7699fa25664 -r d9a574e890f028e8fd520807cf1e7a88bcbc300f doc/en/contact.txt --- a/doc/en/contact.txt +++ b/doc/en/contact.txt @@ -9,6 +9,10 @@ 2.0 and above). You may also peek at the `old issue tracker`_ but please don't submit bugs there anymore. +- `pytest on stackoverflow.com `_ + to post questions with the tag ``pytest``. New Questions will usually + be seen by pytest users or developers. + - `Testing In Python`_: a mailing list for Python testing tools and discussion. - `py-dev developers list`_ pytest specific announcements and discussions. diff -r 6d5db6fbe405aacc5b828ec1f461f7699fa25664 -r d9a574e890f028e8fd520807cf1e7a88bcbc300f doc/en/example/parametrize.txt --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.txt @@ -122,7 +122,7 @@ A quick port of "testscenarios" ------------------------------------ -.. _`test scenarios`: http://bazaar.launchpad.net/~lifeless/testscenarios/trunk/annotate/head%3A/doc/example.py +.. _`test scenarios`: http://pypi.python.org/pypi/testscenarios/ Here is a quick port to run tests configured with `test scenarios`_, an add-on from Robert Collins for the standard unittest framework. We diff -r 6d5db6fbe405aacc5b828ec1f461f7699fa25664 -r d9a574e890f028e8fd520807cf1e7a88bcbc300f doc/en/faq.txt --- a/doc/en/faq.txt +++ b/doc/en/faq.txt @@ -3,8 +3,9 @@ .. note:: - If you don't find an answer here, checkout the :ref:`contact channels` - to get help. + If you don't find an answer here, you may checkout + `pytest Q&A at Stackoverflow `_ + or other :ref:`contact channels` to get help. On naming, nosetests, licensing and magic ------------------------------------------------ @@ -36,7 +37,7 @@ ``manage.py test`` and allows to use all pytest features_ most of which are not available from Django directly. -.. _features: test/features.html +.. _features: features.html What's this "magic" with py.test? (historic notes) @@ -93,7 +94,7 @@ Function arguments, parametrized tests and setup ------------------------------------------------------- -.. _funcargs: test/funcargs.html +.. _funcargs: funcargs.html Is using funcarg- versus xUnit setup a style question? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -107,9 +108,9 @@ because the support code can register setup/teardown functions in a managed class/module/function scope. -.. _monkeypatch: test/plugin/monkeypatch.html -.. _tmpdir: test/plugin/tmpdir.html -.. _capture: test/plugin/capture.html +.. _monkeypatch: monkeypatch.html +.. _tmpdir: tmpdir.html +.. _capture: capture.html .. _`why pytest_pyfuncarg__ methods?`: diff -r 6d5db6fbe405aacc5b828ec1f461f7699fa25664 -r d9a574e890f028e8fd520807cf1e7a88bcbc300f doc/en/projects.txt --- a/doc/en/projects.txt +++ b/doc/en/projects.txt @@ -32,7 +32,7 @@ * `XIST `_ extensible HTML/XML generator * `tiddlyweb `_ optionally headless, extensible RESTful datastore * `fancycompleter `_ for colorful tab-completion -* `Paludis `_ tools for Gentoo Paludis package manager +* `Paludis `_ tools for Gentoo Paludis package manager * `Gerald `_ schema comparison tool * `abjad `_ Python API for Formalized Score control * `bu `_ a microscopic build system diff -r 6d5db6fbe405aacc5b828ec1f461f7699fa25664 -r d9a574e890f028e8fd520807cf1e7a88bcbc300f doc/en/usage.txt --- a/doc/en/usage.txt +++ b/doc/en/usage.txt @@ -130,7 +130,7 @@ and look at the content at the ``path`` location. Such files are used e.g. by the `PyPy-test`_ web page to show test results over several revisions. -.. _`PyPy-test`: http://codespeak.net:8099/summary +.. _`PyPy-test`: http://buildbot.pypy.org/summary Sending test report to pocoo pastebin service @@ -148,7 +148,7 @@ py.test --pastebin=all -Currently only pasting to the http://paste.pocoo.org service is implemented. +Currently only pasting to the http://bpaste.net service is implemented. Calling pytest from Python code ---------------------------------------------------- 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. From commits-noreply at bitbucket.org Sat Jul 7 07:41:15 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 07 Jul 2012 05:41:15 -0000 Subject: [py-svn] commit/pytest: hpk42: exit with errno instead of always signalling success, thanks John Anderson Message-ID: <20120707054115.20214.38715@bitbucket05.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/3a18ce85ae81/ changeset: 3a18ce85ae81 user: hpk42 date: 2012-07-07 07:40:51 summary: exit with errno instead of always signalling success, thanks John Anderson affected #: 2 files diff -r d9a574e890f028e8fd520807cf1e7a88bcbc300f -r 3a18ce85ae81adb382992d7f00865092c3965326 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ Changes between 2.2.4 and 2.3.0.dev ----------------------------------- +- fix "python setup.py test" example to cause a proper "errno" return - fix issue165 - fix broken doc links and mention stackoverflow for FAQ - fix issue139 - merge FuncargRequest and Item API such that funcarg-functionality is now directly available on the "item" diff -r d9a574e890f028e8fd520807cf1e7a88bcbc300f -r 3a18ce85ae81adb382992d7f00865092c3965326 doc/en/goodpractises.txt --- a/doc/en/goodpractises.txt +++ b/doc/en/goodpractises.txt @@ -103,6 +103,7 @@ to support running a pytest from test requirements:: from setuptools.command.test import test as TestCommand + import sys class PyTest(TestCommand): def finalize_options(self): @@ -112,7 +113,8 @@ def run_tests(self): #import here, cause outside the eggs aren't loaded import pytest - pytest.main(self.test_args) + errno = pytest.main(self.test_args) + sys.exit(errno) setup( #..., 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. From commits-noreply at bitbucket.org Sat Jul 7 13:21:54 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 07 Jul 2012 11:21:54 -0000 Subject: [py-svn] commit/pytest: RonnyPfannschmidt: merge Message-ID: <20120707112154.20213.14252@bitbucket05.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/d50ce2daf212/ changeset: d50ce2daf212 user: RonnyPfannschmidt date: 2012-07-07 13:21:45 summary: merge affected #: 8 files diff -r 2e06f917619d045818b48651714c1ea5d59056e4 -r d50ce2daf212727b324e43b26b838e78612102ed CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ Changes between 2.2.4 and 2.3.0.dev ----------------------------------- +- fix "python setup.py test" example to cause a proper "errno" return +- fix issue165 - fix broken doc links and mention stackoverflow for FAQ - fix issue139 - merge FuncargRequest and Item API such that funcarg-functionality is now directly available on the "item" object passed to the various pytest_runtest hooks. This allows more diff -r 2e06f917619d045818b48651714c1ea5d59056e4 -r d50ce2daf212727b324e43b26b838e78612102ed doc/en/announce/release-2.0.1.txt --- a/doc/en/announce/release-2.0.1.txt +++ b/doc/en/announce/release-2.0.1.txt @@ -57,7 +57,7 @@ - refinements to "collecting" output on non-ttys - refine internal plugin registration and --traceconfig output - introduce a mechanism to prevent/unregister plugins from the - command line, see http://pytest.org/plugins.html#cmdunregister + command line, see http://pytest.org/latest/plugins.html#cmdunregister - activate resultlog plugin by default - fix regression wrt yielded tests which due to the collection-before-running semantics were not diff -r 2e06f917619d045818b48651714c1ea5d59056e4 -r d50ce2daf212727b324e43b26b838e78612102ed doc/en/contact.txt --- a/doc/en/contact.txt +++ b/doc/en/contact.txt @@ -9,6 +9,10 @@ 2.0 and above). You may also peek at the `old issue tracker`_ but please don't submit bugs there anymore. +- `pytest on stackoverflow.com `_ + to post questions with the tag ``pytest``. New Questions will usually + be seen by pytest users or developers. + - `Testing In Python`_: a mailing list for Python testing tools and discussion. - `py-dev developers list`_ pytest specific announcements and discussions. diff -r 2e06f917619d045818b48651714c1ea5d59056e4 -r d50ce2daf212727b324e43b26b838e78612102ed doc/en/example/parametrize.txt --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.txt @@ -122,7 +122,7 @@ A quick port of "testscenarios" ------------------------------------ -.. _`test scenarios`: http://bazaar.launchpad.net/~lifeless/testscenarios/trunk/annotate/head%3A/doc/example.py +.. _`test scenarios`: http://pypi.python.org/pypi/testscenarios/ Here is a quick port to run tests configured with `test scenarios`_, an add-on from Robert Collins for the standard unittest framework. We diff -r 2e06f917619d045818b48651714c1ea5d59056e4 -r d50ce2daf212727b324e43b26b838e78612102ed doc/en/faq.txt --- a/doc/en/faq.txt +++ b/doc/en/faq.txt @@ -3,8 +3,9 @@ .. note:: - If you don't find an answer here, checkout the :ref:`contact channels` - to get help. + If you don't find an answer here, you may checkout + `pytest Q&A at Stackoverflow `_ + or other :ref:`contact channels` to get help. On naming, nosetests, licensing and magic ------------------------------------------------ @@ -36,7 +37,7 @@ ``manage.py test`` and allows to use all pytest features_ most of which are not available from Django directly. -.. _features: test/features.html +.. _features: features.html What's this "magic" with py.test? (historic notes) @@ -93,7 +94,7 @@ Function arguments, parametrized tests and setup ------------------------------------------------------- -.. _funcargs: test/funcargs.html +.. _funcargs: funcargs.html Is using funcarg- versus xUnit setup a style question? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -107,9 +108,9 @@ because the support code can register setup/teardown functions in a managed class/module/function scope. -.. _monkeypatch: test/plugin/monkeypatch.html -.. _tmpdir: test/plugin/tmpdir.html -.. _capture: test/plugin/capture.html +.. _monkeypatch: monkeypatch.html +.. _tmpdir: tmpdir.html +.. _capture: capture.html .. _`why pytest_pyfuncarg__ methods?`: diff -r 2e06f917619d045818b48651714c1ea5d59056e4 -r d50ce2daf212727b324e43b26b838e78612102ed doc/en/goodpractises.txt --- a/doc/en/goodpractises.txt +++ b/doc/en/goodpractises.txt @@ -103,6 +103,7 @@ to support running a pytest from test requirements:: from setuptools.command.test import test as TestCommand + import sys class PyTest(TestCommand): def finalize_options(self): @@ -112,7 +113,8 @@ def run_tests(self): #import here, cause outside the eggs aren't loaded import pytest - pytest.main(self.test_args) + errno = pytest.main(self.test_args) + sys.exit(errno) setup( #..., diff -r 2e06f917619d045818b48651714c1ea5d59056e4 -r d50ce2daf212727b324e43b26b838e78612102ed doc/en/projects.txt --- a/doc/en/projects.txt +++ b/doc/en/projects.txt @@ -32,7 +32,7 @@ * `XIST `_ extensible HTML/XML generator * `tiddlyweb `_ optionally headless, extensible RESTful datastore * `fancycompleter `_ for colorful tab-completion -* `Paludis `_ tools for Gentoo Paludis package manager +* `Paludis `_ tools for Gentoo Paludis package manager * `Gerald `_ schema comparison tool * `abjad `_ Python API for Formalized Score control * `bu `_ a microscopic build system diff -r 2e06f917619d045818b48651714c1ea5d59056e4 -r d50ce2daf212727b324e43b26b838e78612102ed doc/en/usage.txt --- a/doc/en/usage.txt +++ b/doc/en/usage.txt @@ -130,7 +130,7 @@ and look at the content at the ``path`` location. Such files are used e.g. by the `PyPy-test`_ web page to show test results over several revisions. -.. _`PyPy-test`: http://codespeak.net:8099/summary +.. _`PyPy-test`: http://buildbot.pypy.org/summary Sending test report to pocoo pastebin service @@ -148,7 +148,7 @@ py.test --pastebin=all -Currently only pasting to the http://paste.pocoo.org service is implemented. +Currently only pasting to the http://bpaste.net service is implemented. Calling pytest from Python code ---------------------------------------------------- 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. From commits-noreply at bitbucket.org Sat Jul 7 16:10:15 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 07 Jul 2012 14:10:15 -0000 Subject: [py-svn] commit/pytest: flub: Fix extension of of cached re-written file Message-ID: <20120707141015.10505.25055@bitbucket15.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/1e9f8d37d4bf/ changeset: 1e9f8d37d4bf user: flub date: 2012-07-07 16:09:53 summary: Fix extension of of cached re-written file With PYTHONOPTIMIZE set this had the extension of "o" instead of ".pyo". Fixes issue #168. affected #: 2 files diff -r d50ce2daf212727b324e43b26b838e78612102ed -r 1e9f8d37d4bf6f51aa651546ed37c5579a30fec7 _pytest/assertion/rewrite.py --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -34,7 +34,7 @@ PYTEST_TAG = "%s-%s%s-PYTEST" % (impl, ver[0], ver[1]) del ver, impl -PYC_EXT = ".py" + "c" if __debug__ else "o" +PYC_EXT = ".py" + ("c" if __debug__ else "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2) diff -r d50ce2daf212727b324e43b26b838e78612102ed -r 1e9f8d37d4bf6f51aa651546ed37c5579a30fec7 testing/test_assertrewrite.py --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1,3 +1,4 @@ +import os import sys import zipfile import py @@ -352,6 +353,7 @@ @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo(self, testdir, monkeypatch): + import _pytest.assertion.rewrite testdir.makepyfile(""" import pytest def test_optimized(): @@ -362,8 +364,12 @@ tmp = "--basetemp=%s" % p monkeypatch.setenv("PYTHONOPTIMIZE", "2") assert testdir.runpybin("py.test", tmp).ret == 0 + fname = 'test_pyc_vs_pyo.%s.pyo' % _pytest.assertion.rewrite.PYTEST_TAG + assert fname in os.listdir('__pycache__') monkeypatch.undo() assert testdir.runpybin("py.test", tmp).ret == 1 + fname = 'test_pyc_vs_pyo.%s.pyc' % _pytest.assertion.rewrite.PYTEST_TAG + assert fname in os.listdir('__pycache__') def test_package(self, testdir): pkg = testdir.tmpdir.join("pkg") 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. From commits-noreply at bitbucket.org Sat Jul 7 17:01:49 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 07 Jul 2012 15:01:49 -0000 Subject: [py-svn] commit/pytest: gutworth: cleanup test a bit Message-ID: <20120707150149.5634.17494@bitbucket02.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/d691d36de2cc/ changeset: d691d36de2cc user: gutworth date: 2012-07-07 17:01:44 summary: cleanup test a bit affected #: 1 file diff -r 1e9f8d37d4bf6f51aa651546ed37c5579a30fec7 -r d691d36de2cc36ea05e02731d39e4a0461b6b1ab testing/test_assertrewrite.py --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -10,7 +10,7 @@ pytest.skip("assert rewrite does currently not work on jython") from _pytest.assertion import util -from _pytest.assertion.rewrite import rewrite_asserts +from _pytest.assertion.rewrite import rewrite_asserts, PYTEST_TAG def setup_module(mod): @@ -353,7 +353,6 @@ @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo(self, testdir, monkeypatch): - import _pytest.assertion.rewrite testdir.makepyfile(""" import pytest def test_optimized(): @@ -364,12 +363,11 @@ tmp = "--basetemp=%s" % p monkeypatch.setenv("PYTHONOPTIMIZE", "2") assert testdir.runpybin("py.test", tmp).ret == 0 - fname = 'test_pyc_vs_pyo.%s.pyo' % _pytest.assertion.rewrite.PYTEST_TAG - assert fname in os.listdir('__pycache__') + tagged = "test_pyc_vs_pyo." + PYTEST_TAG + assert tagged + ".pyo" in os.listdir("__pycache__") monkeypatch.undo() assert testdir.runpybin("py.test", tmp).ret == 1 - fname = 'test_pyc_vs_pyo.%s.pyc' % _pytest.assertion.rewrite.PYTEST_TAG - assert fname in os.listdir('__pycache__') + assert tagged + ".pyc" in os.listdir("__pycache__") def test_package(self, testdir): pkg = testdir.tmpdir.join("pkg") 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. From commits-noreply at bitbucket.org Tue Jul 10 09:39:58 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Tue, 10 Jul 2012 07:39:58 -0000 Subject: [py-svn] commit/pytest-cache: RonnyPfannschmidt: test and fix issue 4 Message-ID: <20120710073958.29422.362@bitbucket12.managed.contegix.com> 1 new commit in pytest-cache: https://bitbucket.org/hpk42/pytest-cache/changeset/bc61694438e9/ changeset: bc61694438e9 user: RonnyPfannschmidt date: 2012-07-10 09:39:40 summary: test and fix issue 4 consider setup time skip of a previous fail no longer a fail affected #: 3 files diff -r 27cda0c4b038562cbe7d281e2323539cf7487710 -r bc61694438e9cdd3ce0296b8379f8f82b694aae5 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,7 @@ + +- fix issue 4: consider setup time skip of a previous failure + as no longer failing + 0.9 ---------------------------------------------- diff -r 27cda0c4b038562cbe7d281e2323539cf7487710 -r bc61694438e9cdd3ce0296b8379f8f82b694aae5 pytest_cache.py --- a/pytest_cache.py +++ b/pytest_cache.py @@ -130,7 +130,9 @@ if report.failed: self.lastfailed.add(report.nodeid) else: - if report.when == "call": + call = report.when == "call" + skipped_setup = report.when == 'setup' and report.skipped + if call or skipped_setup: try: self.lastfailed.remove(report.nodeid) except KeyError: diff -r 27cda0c4b038562cbe7d281e2323539cf7487710 -r bc61694438e9cdd3ce0296b8379f8f82b694aae5 test_cache.py --- a/test_cache.py +++ b/test_cache.py @@ -160,3 +160,68 @@ result.stdout.fnmatch_lines([ "*2 failed*", ]) + + @pytest.mark.skipif("sys.version_info < (2,6)") + def test_lastfailed_failure_to_skip_is_passsed(self, testdir, monkeypatch): + monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", 1) + p1 = testdir.makepyfile(""" + import os + import pytest + def skipat(when): + if os.environ.get("SKIPAT") == when: + pytest.skip(when) + + def setup_module(mod): + skipat('setup') + + def test_configured_skip(): + skipat('test') + assert 0 + """) + p2 = testdir.tmpdir.join('test_fail.py').write(py.code.Source(""" + def test_keepfail(): + assert 0 + """)) + + result = testdir.runpytest("--lf") + result.stdout.fnmatch_lines([ + "*2 failed*", + ]) + + monkeypatch.setenv("SKIPAT", 'test') + + result = testdir.runpytest("--lf") + result.stdout.fnmatch_lines([ + "*1 skipped*", + ]) + + result = testdir.runpytest("--lf") + result.stdout.fnmatch_lines([ + "*1 deselected*", + ]) + + monkeypatch.delenv('SKIPAT') + + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*2 failed*", + ]) + + monkeypatch.setenv("SKIPAT", 'setup') + + result = testdir.runpytest("--lf") + result.stdout.fnmatch_lines([ + "*1 skipped*", + ]) + #XXX: check cache + result = testdir.runpytest("--lf") + result.stdout.fnmatch_lines([ + "*1 deselected*", + ]) + + monkeypatch.delenv('SKIPAT') + + result = testdir.runpytest("--lf") + result.stdout.fnmatch_lines([ + "*1 failed*", + ]) Repository URL: https://bitbucket.org/hpk42/pytest-cache/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Jul 10 09:51:12 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Tue, 10 Jul 2012 07:51:12 -0000 Subject: [py-svn] commit/pytest-cache: 3 new changesets Message-ID: <20120710075112.1675.29694@bitbucket03.managed.contegix.com> 3 new commits in pytest-cache: https://bitbucket.org/hpk42/pytest-cache/changeset/c3794cc95172/ changeset: c3794cc95172 user: RonnyPfannschmidt date: 2012-07-10 09:48:46 summary: use set.discard for removing lastfailed set items affected #: 1 file diff -r bc61694438e9cdd3ce0296b8379f8f82b694aae5 -r c3794cc951724285664c7a5f315d9d133723de0d pytest_cache.py --- a/pytest_cache.py +++ b/pytest_cache.py @@ -133,10 +133,7 @@ call = report.when == "call" skipped_setup = report.when == 'setup' and report.skipped if call or skipped_setup: - try: - self.lastfailed.remove(report.nodeid) - except KeyError: - pass + self.lastfailed.discard(report.nodeid) def pytest_collection_modifyitems(self, session, config, items): if self.config.getvalue("lf") and self.lastfailed: https://bitbucket.org/hpk42/pytest-cache/changeset/bec5cc83091b/ changeset: bec5cc83091b user: RonnyPfannschmidt date: 2012-07-10 09:49:16 summary: use sort in cache show, so the output gets deterministic affected #: 1 file diff -r c3794cc951724285664c7a5f315d9d133723de0d -r bec5cc83091bc5fb32df87f82873ba36c1aecbc2 pytest_cache.py --- a/pytest_cache.py +++ b/pytest_cache.py @@ -165,7 +165,7 @@ basedir = config.cache._cachedir vdir = basedir.join("v") tw.sep("-", "cache values") - for valpath in vdir.visit(lambda x: x.check(file=1)): + for valpath in vdir.visit(lambda x: x.check(file=1), sort=True): key = valpath.relto(vdir) val = config.cache.get(key, dummy) if val is dummy: @@ -179,9 +179,9 @@ tw.line(" " + line) ddir = basedir.join("d") - if ddir.check(dir=1) and ddir.listdir(): + if ddir.check(dir=1) and ddir.listdir(sort=True): tw.sep("-", "cache directories") - for p in basedir.join("d").visit(): + for p in basedir.join("d").visit(sort=True): #if p.check(dir=1): # print("%s/" % p.relto(basedir)) if p.check(file=1): https://bitbucket.org/hpk42/pytest-cache/changeset/470d240e9674/ changeset: 470d240e9674 user: RonnyPfannschmidt date: 2012-07-10 09:51:01 summary: pep8 fix affected #: 1 file diff -r bec5cc83091bc5fb32df87f82873ba36c1aecbc2 -r 470d240e9674529a38718110fbd25d5511809892 test_cache.py --- a/test_cache.py +++ b/test_cache.py @@ -201,14 +201,14 @@ ]) monkeypatch.delenv('SKIPAT') - + result = testdir.runpytest() result.stdout.fnmatch_lines([ "*2 failed*", ]) - + monkeypatch.setenv("SKIPAT", 'setup') - + result = testdir.runpytest("--lf") result.stdout.fnmatch_lines([ "*1 skipped*", @@ -220,7 +220,7 @@ ]) monkeypatch.delenv('SKIPAT') - + result = testdir.runpytest("--lf") result.stdout.fnmatch_lines([ "*1 failed*", Repository URL: https://bitbucket.org/hpk42/pytest-cache/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Jul 10 09:52:26 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Tue, 10 Jul 2012 07:52:26 -0000 Subject: [py-svn] commit/pytest-cache: RonnyPfannschmidt: update changelog Message-ID: <20120710075226.26328.90416@bitbucket02.managed.contegix.com> 1 new commit in pytest-cache: https://bitbucket.org/hpk42/pytest-cache/changeset/0694bcab3d0c/ changeset: 0694bcab3d0c user: RonnyPfannschmidt date: 2012-07-10 09:52:05 summary: update changelog affected #: 1 file diff -r 470d240e9674529a38718110fbd25d5511809892 -r 0694bcab3d0c2f066b8c656dce20e8bb519192b6 CHANGELOG --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,7 @@ - fix issue 4: consider setup time skip of a previous failure as no longer failing +- ensure --cache output is sorted 0.9 ---------------------------------------------- Repository URL: https://bitbucket.org/hpk42/pytest-cache/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Jul 10 10:32:43 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Tue, 10 Jul 2012 08:32:43 -0000 Subject: [py-svn] commit/pytest-codecheckers: 3 new changesets Message-ID: <20120710083243.28893.82929@bitbucket15.managed.contegix.com> 3 new commits in pytest-codecheckers: https://bitbucket.org/RonnyPfannschmidt/pytest-codecheckers/changeset/09df4ad0629e/ changeset: 09df4ad0629e user: RonnyPfannschmidt date: 2012-06-21 14:37:32 summary: clean up flakes tests affected #: 1 file diff -r 069dd9d1aead5de036a5c315be204e774272bbb4 -r 09df4ad0629ebc157292529870a2ece54334e5d5 tests/test_pyflakes.py --- a/tests/test_pyflakes.py +++ b/tests/test_pyflakes.py @@ -11,8 +11,7 @@ def b(): abc ''') - #XXX: bad hack cause i fail to disable the pep8 checker - out = testdir.runpytest('--tb=short', '-k', 'flakes', '-v') + out = testdir.runpytest('--tb=short', '-v') out.stdout.fnmatch_lines([ '*abc*', '*1 failed*', https://bitbucket.org/RonnyPfannschmidt/pytest-codecheckers/changeset/4e1175ad4c19/ changeset: 4e1175ad4c19 user: RonnyPfannschmidt date: 2012-06-21 14:56:10 summary: tests for pep8 affected #: 1 file diff -r 09df4ad0629ebc157292529870a2ece54334e5d5 -r 4e1175ad4c19e795acfce9b9093392dd663e9af1 tests/test_pep8.py --- /dev/null +++ b/tests/test_pep8.py @@ -0,0 +1,34 @@ +def pytest_funcarg__testdir(request): + testdir = request.getfuncargvalue('testdir') + testdir.makeini('[pytest]\ncodechecks = pep8') + return testdir + + +def test_badcode(testdir): + testdir.makepyfile(''' + def a(): + pass + def b(): + pass''') + out = testdir.runpytest('--tb=short', '-v') + out.stdout.fnmatch_lines([ + '*lines*', + '*1 failed*', + ]) + + +def test_goodcode(testdir): + p = testdir.makepyfile(''' + def a(): + pass + + + def b(): + pass + + ''') + p.write(p.read() + '\n') + out = testdir.runpytest('--tb=short', '-v') + out.stdout.fnmatch_lines([ + '*1 passed*', + ]) https://bitbucket.org/RonnyPfannschmidt/pytest-codecheckers/changeset/78f7abc82494/ changeset: 78f7abc82494 user: RonnyPfannschmidt date: 2012-07-10 10:32:30 summary: back to no checks by default affected #: 2 files diff -r 4e1175ad4c19e795acfce9b9093392dd663e9af1 -r 78f7abc82494da686ecf5009e06047d25b6e1722 README --- a/README +++ b/README @@ -8,3 +8,5 @@ Changelog ----------- + +- dont do codechecsk by default anymore diff -r 4e1175ad4c19e795acfce9b9093392dd663e9af1 -r 78f7abc82494da686ecf5009e06047d25b6e1722 codecheckers/plugin.py --- a/codecheckers/plugin.py +++ b/codecheckers/plugin.py @@ -56,5 +56,5 @@ def pytest_addoption(parser): parser.addini('codechecks', type='args', help='listings of the codechecks to use', - default=['pep8', 'pyflakes']) + default=[]) parser.addoption('--no-codechecks', action='store_true') Repository URL: https://bitbucket.org/RonnyPfannschmidt/pytest-codecheckers/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Jul 12 18:25:07 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Thu, 12 Jul 2012 16:25:07 -0000 Subject: [py-svn] commit/pytest: johtso: Fix typo in terminal help text Message-ID: <20120712162507.2369.11598@bitbucket16.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/d075ea394e78/ changeset: d075ea394e78 user: johtso date: 2012-07-12 18:00:48 summary: Fix typo in terminal help text affected #: 1 file diff -r d691d36de2cc36ea05e02731d39e4a0461b6b1ab -r d075ea394e78c26c6377347b576b568ffd12cf15 _pytest/terminal.py --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -12,7 +12,7 @@ group._addoption('-v', '--verbose', action="count", dest="verbose", default=0, help="increase verbosity."), group._addoption('-q', '--quiet', action="count", - dest="quiet", default=0, help="decreate verbosity."), + dest="quiet", default=0, help="decrease verbosity."), group._addoption('-r', action="store", dest="reportchars", default=None, metavar="chars", help="show extra test summary info as specified by chars (f)ailed, " 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. From commits-noreply at bitbucket.org Sat Jul 14 12:07:35 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Sat, 14 Jul 2012 10:07:35 -0000 Subject: [py-svn] commit/pytest: hpk42: add a little example on how to group test execution by parametrized resource Message-ID: <20120714100735.31133.88879@bitbucket01.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/227c24740b10/ changeset: 227c24740b10 user: hpk42 date: 2012-07-14 12:06:58 summary: add a little example on how to group test execution by parametrized resource affected #: 2 files diff -r d075ea394e78c26c6377347b576b568ffd12cf15 -r 227c24740b10cadfdb2f208cde64eb0702cdbeff doc/en/Makefile --- a/doc/en/Makefile +++ b/doc/en/Makefile @@ -40,7 +40,7 @@ -rm -rf $(BUILDDIR)/* install: html - rsync -avz _build/html/ pytest.org:/www/pytest.org/latest + rsync -avz _build/html/ pytest.org:/www/pytest.org/dev installpdf: latexpdf @scp $(BUILDDIR)/latex/pytest.pdf pytest.org:/www/pytest.org/latest diff -r d075ea394e78c26c6377347b576b568ffd12cf15 -r 227c24740b10cadfdb2f208cde64eb0702cdbeff doc/en/example/parametrize.txt --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.txt @@ -38,8 +38,8 @@ $ py.test -q collecting ... collected 3 items ..F - ================================= FAILURES ================================= - ____________________________ test_eval[6*9-42] _____________________________ + =================================== FAILURES =================================== + ______________________________ test_eval[6*9-42] _______________________________ input = '6*9', expected = 42 @@ -54,7 +54,7 @@ E + where 54 = eval('6*9') test_expectation.py:8: AssertionError - 1 failed, 2 passed in 0.01 seconds + 1 failed, 2 passed in 0.02 seconds As expected only one pair of input/output values fails the simple test function. @@ -96,7 +96,7 @@ $ py.test -q test_compute.py collecting ... collected 2 items .. - 2 passed in 0.01 seconds + 2 passed in 0.02 seconds We run only two computations, so we see two dots. let's run the full monty:: @@ -104,8 +104,8 @@ $ py.test -q --all collecting ... collected 5 items ....F - ================================= FAILURES ================================= - _____________________________ test_compute[4] ______________________________ + =================================== FAILURES =================================== + _______________________________ test_compute[4] ________________________________ param1 = 4 @@ -153,20 +153,22 @@ this is a fully self-contained example which you can run with:: $ py.test test_scenarios.py - =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + ============================= test session starts ============================== + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 2 items test_scenarios.py .. - ========================= 2 passed in 0.01 seconds ========================= + =========================== 2 passed in 0.02 seconds =========================== If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function:: $ py.test --collectonly test_scenarios.py - =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + ============================= test session starts ============================== + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 2 items @@ -174,7 +176,7 @@ - ============================= in 0.00 seconds ============================= + =============================== in 0.01 seconds =============================== Deferring the setup of parametrized resources --------------------------------------------------- @@ -221,24 +223,25 @@ Let's first see how it looks like at collection time:: $ py.test test_backends.py --collectonly - =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + ============================= test session starts ============================== + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 2 items - ============================= in 0.00 seconds ============================= + =============================== in 0.01 seconds =============================== And then when we run the test:: $ py.test -q test_backends.py collecting ... collected 2 items .F - ================================= FAILURES ================================= - _________________________ test_db_initialized[d2] __________________________ + =================================== FAILURES =================================== + ___________________________ test_db_initialized[d2] ____________________________ - db = + db = def test_db_initialized(db): # a dummy test @@ -247,7 +250,7 @@ E Failed: deliberately failing for demo purposes test_backends.py:6: Failed - 1 failed, 1 passed in 0.01 seconds + 1 failed, 1 passed in 0.02 seconds The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``pytest_funcarg__db`` factory has instantiated each of the DB values during the setup phase while the ``pytest_generate_tests`` generated two according calls to the ``test_db_initialized`` during the collection phase. @@ -292,17 +295,17 @@ $ py.test -q collecting ... collected 3 items F.. - ================================= FAILURES ================================= - ________________________ TestClass.test_equals[1-2] ________________________ + =================================== FAILURES =================================== + __________________________ TestClass.test_equals[1-2] __________________________ - self = , a = 1, b = 2 + self = , a = 1, b = 2 def test_equals(self, a, b): > assert a == b E assert 1 == 2 test_parametrize.py:18: AssertionError - 1 failed, 2 passed in 0.01 seconds + 1 failed, 2 passed in 0.02 seconds Indirect parametrization with multiple resources -------------------------------------------------------------- @@ -323,6 +326,75 @@ . $ py.test -rs -q multipython.py collecting ... collected 75 items ............sss............sss............sss............ssssssssssssssssss - ========================= short test summary info ========================== - SKIP [27] /home/hpk/p/pytest/doc/example/multipython.py:36: 'python2.8' not found - 48 passed, 27 skipped in 1.71 seconds + =========================== short test summary info ============================ + SKIP [27] /home/hpk/p/pytest/doc/en/example/multipython.py:36: 'python2.8' not found + 48 passed, 27 skipped in 1.89 seconds + +.. regendoc:wipe + +Grouping test execution by parameter +----------------------------------------- + +By default pytest will execute test functions by executing all its parametrized invocations. If you rather want to group execution by parameter, you can +use something like the following ``conftest.py`` example. It uses +a parametrized "session" object:: + + # content of conftest.py + def pytest_collection_modifyitems(items): + def cmp(item1, item2): + param1 = item1.callspec.getparam("session") + param2 = item2.callspec.getparam("session") + if param1 < param2: + return -1 + elif param1 > param2: + return 1 + return 0 + items.sort(cmp=cmp) + + def pytest_generate_tests(metafunc): + if "session" in metafunc.funcargnames: + metafunc.parametrize("session", [1,2], indirect=True) + + class Session: + def __init__(self, num): + self.num = num + + def pytest_funcarg__session(request): + return Session(request.param) + + +If you know have a test file like this:: + + # content of test_session.py + def test_hello(session): + pass + + def test_world(session): + pass + + class TestClass: + def test_method1(self, session): + pass + def test_method2(self, session): + pass + +then a subsequent execution will order the running of tests by +parameter value:: + + $ py.test -v + ============================= test session starts ============================== + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-313/.cache + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + collecting ... collected 8 items + + test_session.py:1: test_hello[1] PASSED + test_session.py:4: test_world[1] PASSED + test_session.py:8: TestClass.test_method1[1] PASSED + test_session.py:10: TestClass.test_method2[1] PASSED + test_session.py:1: test_hello[2] PASSED + test_session.py:4: test_world[2] PASSED + test_session.py:8: TestClass.test_method1[2] PASSED + test_session.py:10: TestClass.test_method2[2] PASSED + + =========================== 8 passed in 0.02 seconds =========================== 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. From commits-noreply at bitbucket.org Wed Jul 18 08:33:26 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 18 Jul 2012 06:33:26 -0000 Subject: [py-svn] commit/pytest: Wes Turner: DOC: typo in doc/en/goodpractices.txt ("pytest" -> "PyTest") Message-ID: <20120718063326.18420.96462@bitbucket05.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/bbe2455f3fef/ changeset: bbe2455f3fef user: Wes Turner date: 2012-07-18 08:01:37 summary: DOC: typo in doc/en/goodpractices.txt ("pytest" -> "PyTest") affected #: 1 file diff -r 227c24740b10cadfdb2f208cde64eb0702cdbeff -r bbe2455f3fefeb0ce1dafbb295137b14a2652d47 doc/en/goodpractises.txt --- a/doc/en/goodpractises.txt +++ b/doc/en/goodpractises.txt @@ -119,7 +119,7 @@ setup( #..., tests_require=['pytest'], - cmdclass = {'test': pytest}, + cmdclass = {'test': PyTest}, ) Now if you run:: 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. From commits-noreply at bitbucket.org Wed Jul 18 19:48:55 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Wed, 18 Jul 2012 17:48:55 -0000 Subject: [py-svn] commit/pytest: 4 new changesets Message-ID: <20120718174855.12724.11552@bitbucket01.managed.contegix.com> 4 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/5ce84793fd92/ changeset: 5ce84793fd92 user: hpk42 date: 2012-07-16 10:46:44 summary: put automatic funcarg_ API to Py*objects only, refine internal subclassing and initialisation logic affected #: 4 files diff -r 227c24740b10cadfdb2f208cde64eb0702cdbeff -r 5ce84793fd929b65fd666c77093580424f7b1f11 _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -119,8 +119,9 @@ return "", "" def activate_funcargs(self, pyfuncitem): - if pyfuncitem.funcargs: - for name, capfuncarg in pyfuncitem.funcargs.items(): + funcargs = getattr(pyfuncitem, "funcargs", None) + if funcargs is not None: + for name, capfuncarg in funcargs.items(): if name in ('capsys', 'capfd'): assert not hasattr(self, '_capturing_funcarg') self._capturing_funcarg = capfuncarg diff -r 227c24740b10cadfdb2f208cde64eb0702cdbeff -r 5ce84793fd929b65fd666c77093580424f7b1f11 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -3,7 +3,6 @@ import py import pytest, _pytest import os, sys, imp -from _pytest.monkeypatch import monkeypatch tracebackcutdir = py.path.local(_pytest.__file__).dirpath() @@ -151,130 +150,6 @@ return property(fget) -def pyobj_property(name): - def get(self): - node = self.getparent(getattr(pytest, name)) - if node is not None: - return node.obj - doc = "python %s object this node was collected from (can be None)." % ( - name.lower(),) - return property(get, None, None, doc) - -class Request(object): - _argprefix = "pytest_funcarg__" - - class LookupError(LookupError): - """ error while performing funcarg factory lookup. """ - - def _initattr(self): - self._name2factory = {} - self._currentarg = None - - @property - def _plugins(self): - extra = [obj for obj in (self.module, self.instance) if obj] - return self.getplugins() + extra - - def _getscopeitem(self, scope): - if scope == "function": - return self - elif scope == "session": - return None - elif scope == "class": - x = self.getparent(pytest.Class) - if x is not None: - return x - scope = "module" - if scope == "module": - return self.getparent(pytest.Module) - raise ValueError("unknown scope %r" %(scope,)) - - def getfuncargvalue(self, argname): - """ Retrieve a named function argument value. - - This function looks up a matching factory and invokes - it to obtain the return value. The factory receives - the same request object and can itself perform recursive - calls to this method, effectively allowing to make use of - multiple other funcarg values or to decorate values from - other name-matching factories. - """ - try: - return self.funcargs[argname] - except KeyError: - pass - except TypeError: - self.funcargs = getattr(self, "_funcargs", {}) - if argname not in self._name2factory: - self._name2factory[argname] = self.config.pluginmanager.listattr( - plugins=self._plugins, - attrname=self._argprefix + str(argname) - ) - #else: we are called recursively - if not self._name2factory[argname]: - self._raiselookupfailed(argname) - funcargfactory = self._name2factory[argname].pop() - oldarg = self._currentarg - mp = monkeypatch() - mp.setattr(self, '_currentarg', argname) - try: - param = self.callspec.getparam(argname) - except (AttributeError, ValueError): - pass - else: - mp.setattr(self, 'param', param, raising=False) - try: - self.funcargs[argname] = res = funcargfactory(self) - finally: - mp.undo() - return res - - def addfinalizer(self, finalizer): - """ add a no-args finalizer function to be called when the underlying - node is torn down.""" - self.session._setupstate.addfinalizer(finalizer, self) - - def cached_setup(self, setup, teardown=None, - scope="module", extrakey=None): - """ Return a cached testing resource created by ``setup`` & - detroyed by a respective ``teardown(resource)`` call. - - :arg teardown: function receiving a previously setup resource. - :arg setup: a no-argument function creating a resource. - :arg scope: a string value out of ``function``, ``class``, ``module`` - or ``session`` indicating the caching lifecycle of the resource. - :arg extrakey: added to internal caching key. - """ - if not hasattr(self.config, '_setupcache'): - self.config._setupcache = {} # XXX weakref? - colitem = self._getscopeitem(scope) - cachekey = (self._currentarg, colitem, extrakey) - cache = self.config._setupcache - try: - val = cache[cachekey] - except KeyError: - val = setup() - cache[cachekey] = val - if teardown is not None: - def finalizer(): - del cache[cachekey] - teardown(val) - self.session._setupstate.addfinalizer(finalizer, colitem) - return val - - def _raiselookupfailed(self, argname): - available = [] - for plugin in self._plugins: - for name in vars(plugin): - if name.startswith(self._argprefix): - name = name[len(self._argprefix):] - if name not in available: - available.append(name) - fspath, lineno, msg = self.reportinfo() - msg = "LookupError: no factory found for function argument %r" % (argname,) - msg += "\n available funcargs: %s" %(", ".join(available),) - msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise self.LookupError(msg) class Node(object): """ base class for Collector and Item the test collection tree. @@ -302,6 +177,12 @@ #: fspath sensitive hook proxy used to call pytest hooks self.ihook = self.session.gethookproxy(self.fspath) + self.extrainit() + + def extrainit(self): + """"extra initialization after Node is initialized. Implemented + by some subclasses. """ + Module = compatproperty("Module") Class = compatproperty("Class") Instance = compatproperty("Instance") @@ -309,11 +190,6 @@ File = compatproperty("File") Item = compatproperty("Item") - module = pyobj_property("Module") - cls = pyobj_property("Class") - instance = pyobj_property("Instance") - - def _getcustomclass(self, name): cls = getattr(self, name) if cls != getattr(pytest, name): @@ -476,20 +352,12 @@ class File(FSCollector): """ base class for collecting tests from a file. """ -class Item(Node, Request): +class Item(Node): """ a basic test invocation item. Note that for a single function there might be multiple test invocation items. """ nextitem = None - def __init__(self, name, parent=None, config=None, session=None): - super(Item, self).__init__(name, parent, config, session) - self._initattr() - self.funcargs = None # later set to a dict from fillfuncargs() or - # from getfuncargvalue(). Setting it to - # None prevents users from performing - # "name in item.funcargs" checks too early. - def reportinfo(self): return self.fspath, None, "" @@ -532,10 +400,9 @@ __module__ = 'builtins' # for py3 def __init__(self, config): - super(Session, self).__init__(py.path.local(), parent=None, - config=config, session=self) - assert self.config.pluginmanager.register( - self, name="session", prepend=True) + FSCollector.__init__(self, py.path.local(), parent=None, + config=config, session=self) + self.config.pluginmanager.register(self, name="session", prepend=True) self._testsfailed = 0 self.shouldstop = False self.trace = config.trace.root.get("collection") diff -r 227c24740b10cadfdb2f208cde64eb0702cdbeff -r 5ce84793fd929b65fd666c77093580424f7b1f11 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -4,7 +4,7 @@ import sys import pytest from py._code.code import TerminalRepr -from _pytest.main import Request, Item +from _pytest.monkeypatch import monkeypatch import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() @@ -24,6 +24,135 @@ return x return property(get) +def pyobj_property(name): + def get(self): + node = self.getparent(getattr(pytest, name)) + if node is not None: + return node.obj + doc = "python %s object this node was collected from (can be None)." % ( + name.lower(),) + return property(get, None, None, doc) + +class Request(object): + _argprefix = "pytest_funcarg__" + + class LookupError(LookupError): + """ error while performing funcarg factory lookup. """ + + def extrainit(self): + self._name2factory = {} + self._currentarg = None + self.funcargs = None # later set to a dict from fillfuncargs() or + # from getfuncargvalue(). Setting it to + # None prevents users from performing + # "name in item.funcargs" checks too early. + + @property + def _plugins(self): + extra = [obj for obj in (self.module, self.instance) if obj] + return self.getplugins() + extra + + def _getscopeitem(self, scope): + if scope == "function": + return self + elif scope == "session": + return None + elif scope == "class": + x = self.getparent(pytest.Class) + if x is not None: + return x + scope = "module" + if scope == "module": + return self.getparent(pytest.Module) + raise ValueError("unknown scope %r" %(scope,)) + + def getfuncargvalue(self, argname): + """ Retrieve a named function argument value. + + This function looks up a matching factory and invokes + it to obtain the return value. The factory receives + can itself perform recursive calls to this method, + either for using multiple other funcarg values under the hood + or to decorate values from other factories matching the same name. + """ + try: + return self.funcargs[argname] + except KeyError: + pass + except TypeError: + self.funcargs = getattr(self, "_funcargs", {}) + if argname not in self._name2factory: + self._name2factory[argname] = self.config.pluginmanager.listattr( + plugins=self._plugins, + attrname=self._argprefix + str(argname) + ) + #else: we are called recursively + if not self._name2factory[argname]: + self._raiselookupfailed(argname) + funcargfactory = self._name2factory[argname].pop() + oldarg = self._currentarg + mp = monkeypatch() + mp.setattr(self, '_currentarg', argname) + try: + param = self.callspec.getparam(argname) + except (AttributeError, ValueError): + pass + else: + mp.setattr(self, 'param', param, raising=False) + try: + self.funcargs[argname] = res = funcargfactory(self) + finally: + mp.undo() + return res + + def addfinalizer(self, finalizer): + """ add a no-args finalizer function to be called when the underlying + node is torn down.""" + self.session._setupstate.addfinalizer(finalizer, self) + + def cached_setup(self, setup, teardown=None, + scope="module", extrakey=None): + """ Return a cached testing resource created by ``setup`` & + detroyed by a respective ``teardown(resource)`` call. + + :arg teardown: function receiving a previously setup resource. + :arg setup: a no-argument function creating a resource. + :arg scope: a string value out of ``function``, ``class``, ``module`` + or ``session`` indicating the caching lifecycle of the resource. + :arg extrakey: added to internal caching key. + """ + if not hasattr(self.config, '_setupcache'): + self.config._setupcache = {} # XXX weakref? + colitem = self._getscopeitem(scope) + cachekey = (self._currentarg, colitem, extrakey) + cache = self.config._setupcache + try: + val = cache[cachekey] + except KeyError: + val = setup() + cache[cachekey] = val + if teardown is not None: + def finalizer(): + del cache[cachekey] + teardown(val) + self.session._setupstate.addfinalizer(finalizer, colitem) + return val + + def _raiselookupfailed(self, argname): + available = [] + for plugin in self._plugins: + for name in vars(plugin): + if name.startswith(self._argprefix): + name = name[len(self._argprefix):] + if name not in available: + available.append(name) + fspath, lineno, msg = self.reportinfo() + msg = "LookupError: no factory found for function argument %r" % (argname,) + msg += "\n available funcargs: %s" %(", ".join(available),) + msg += "\n use 'py.test --funcargs [testpath]' for help on them." + raise self.LookupError(msg) + + def pytest_addoption(parser): group = parser.getgroup("general") @@ -133,8 +262,12 @@ # assume them to not be generators return False -class PyobjMixin(object): +class PyobjContext(object): + module = pyobj_property("Module") + cls = pyobj_property("Class") + instance = pyobj_property("Instance") +class PyobjMixin(Request, PyobjContext): def obj(): def fget(self): try: @@ -203,7 +336,7 @@ assert isinstance(lineno, int) return fspath, lineno, modpath -class PyCollectorMixin(PyobjMixin, pytest.Collector): +class PyCollector(PyobjMixin, pytest.Collector): def funcnamefilter(self, name): for prefix in self.config.getini("python_functions"): @@ -283,7 +416,7 @@ else: pytestmark(funcobj) -class Module(pytest.File, PyCollectorMixin): +class Module(pytest.File, PyCollector): """ Collector for test classes and functions. """ def _getobj(self): return self._memoizedcall('_obj', self._importtestmodule) @@ -331,7 +464,7 @@ else: self.obj.teardown_module() -class Class(PyCollectorMixin, pytest.Collector): +class Class(PyCollector): """ Collector for test methods. """ def collect(self): return [self._getcustomclass("Instance")(name="()", parent=self)] @@ -350,7 +483,7 @@ teardown_class = getattr(teardown_class, '__func__', teardown_class) teardown_class(self.obj) -class Instance(PyCollectorMixin, pytest.Collector): +class Instance(PyCollector): def _getobj(self): return self.parent.obj() @@ -437,7 +570,7 @@ tw.line("%s:%d" % (self.filename, self.firstlineno+1)) -class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): +class Generator(FunctionMixin, PyCollector): def collect(self): # test generators are seen as collectors but they also # invoke setup/teardown on popular request @@ -882,7 +1015,7 @@ return property(get, set, None, doc) -class OldFuncargRequest(Request): +class OldFuncargRequest(Request, PyobjContext): """ (deprecated) helper interactions with a test function invocation. Note that there is an optional ``param`` attribute in case @@ -892,9 +1025,11 @@ """ def __init__(self, pyfuncitem): self._pyfuncitem = pyfuncitem - Request._initattr(self) + Request.extrainit(self) + self.funcargs = pyfuncitem.funcargs self.getplugins = self._pyfuncitem.getplugins self.reportinfo = self._pyfuncitem.reportinfo + self.getparent = self._pyfuncitem.getparent try: self.param = self._pyfuncitem.param except AttributeError: @@ -906,9 +1041,6 @@ _getscopeitem = itemapi_property("_getscopeitem") funcargs = itemapi_property("funcargs", set=True) keywords = itemapi_property("keywords") - module = itemapi_property("module") - cls = itemapi_property("cls") - instance = itemapi_property("instance") config = itemapi_property("config") session = itemapi_property("session") fspath = itemapi_property("fspath") diff -r 227c24740b10cadfdb2f208cde64eb0702cdbeff -r 5ce84793fd929b65fd666c77093580424f7b1f11 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1647,3 +1647,21 @@ ]) + +class TestResourceIntegrationFunctional: + def test_parametrize_with_ids(self, testdir): + testdir.makepyfile(""" + import pytest + def pytest_generate_tests(metafunc): + metafunc.parametrize(("a", "b"), [(1,1), (1,2)], + ids=["basic", "advanced"]) + + def test_function(a, b): + assert a == b + """) + result = testdir.runpytest("-v") + assert result.ret == 1 + result.stdout.fnmatch_lines([ + "*test_function*basic*PASSED", + "*test_function*advanced*FAILED", + ]) https://bitbucket.org/hpk42/pytest/changeset/748fede1c05e/ changeset: 748fede1c05e user: hpk42 date: 2012-07-16 10:47:00 summary: V1 of the resources API draft affected #: 2 files diff -r 5ce84793fd929b65fd666c77093580424f7b1f11 -r 748fede1c05eb3c2c5f181559b71843488cca839 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -609,3 +609,29 @@ for x in self.genitems(subnode): yield x node.ihook.pytest_collectreport(report=rep) + + def register_resource_factory(self, name, factoryfunc, + matchscope=None, + cachescope=None): + """ register a factory function for the given name. + + :param name: the name which can be used to retrieve a value constructed + by the factory function later. + :param factoryfunc: a function accepting (name, reqnode) parameters + and returning a value. + :param matchscope: denotes visibility of the factory func. + Pass a particular Node instance if you want to + restrict factory function visilbility to its descendants. + Pass None if you want the factory func to be globally + availabile. + :param cachescope: denotes caching scope. If you pass a node instance + the value returned by getresource() will be reused + for all descendants of that node. Pass None (the default) + if you want no caching. Pass "session" if you want to + to cache on a per-session level. + """ + + + + + diff -r 5ce84793fd929b65fd666c77093580424f7b1f11 -r 748fede1c05eb3c2c5f181559b71843488cca839 doc/en/example/resources.txt --- /dev/null +++ b/doc/en/example/resources.txt @@ -0,0 +1,167 @@ + +V2: Creating and working with parametrized test resources +=============================================================== + +# XXX collection versus setup-time +# XXX parametrize-relation? + +pytest-2.3 provides generalized resource management allowing +to flexibly manage caching and parametrization across your test suite. + +This is draft documentation, pending refinements and changes according +to feedback and to implementation or backward compatibility issues +(the new mechanism is supposed to allow fully backward compatible +operations for uses of the "funcarg" mechanism. + +the new global pytest_runtest_init hook +------------------------------------------------------ + +Prior to 2.3, pytest offered a pytest_configure and a pytest_sessionstart +hook which was used often to setup global resources. This suffers from +several problems. First of all, in distributed testing the master would +also setup test resources that are never needed because it only co-ordinates +the test run activities of the slave processes. Secondly, in large test +suites resources are setup that might not be needed for the concrete test +run. The first issue is solved through the introduction of a specific +hook:: + + def pytest_runtest_init(session): + # called ahead of pytest_runtestloop() test execution + +This hook will only be called in processes that actually run tests. + +The second issue is solved through a new register/getresource API which +will only ever setup resources if they are needed. See the following +examples and sections on how this works. + + +managing a global database resource +--------------------------------------------------------------- + +If you have one database object which you want to use in tests +you can write the following into a conftest.py file:: + + class Database: + def __init__(self): + print ("database instance created") + def destroy(self): + print ("database instance destroyed") + + def factory_db(name, node): + db = Database() + node.addfinalizer(db.destroy) + return db + + def pytest_runtest_init(session): + session.register_resource("db", factory_db, atnode=session) + +You can then access the constructed resource in a test like this:: + + def test_something(db): + ... + +The "db" function argument will lead to a lookup of the respective +factory value and be passed to the function body. According to the +registration, the db object will be instantiated on a per-session basis +and thus reused across all test functions that require it. + +instantiating a database resource per-module +--------------------------------------------------------------- + +If you want one database instance per test module you can restrict +caching by modifying the "atnode" parameter of the registration +call above:: + + def pytest_runtest_init(session): + session.register_resource("db", factory_db, atnode=pytest.Module) + +Neither the tests nor the factory function will need to change. +This also means that you can decide the scoping of resources +at runtime - e.g. based on a command line option: for developer +settings you might want per-session and for Continous Integration +runs you might prefer per-module or even per-function scope like this:: + + def pytest_runtest_init(session): + session.register_resource_factory("db", factory_db, + atnode=pytest.Function) + +parametrized resources +---------------------------------- + +If you want to rerun tests with different resource values you can specify +a list of factories instead of just one:: + + def pytest_runtest_init(session): + session.register_factory("db", [factory1, factory2], atnode=session) + +In this case all tests that depend on the "db" resource will be run twice +using the respective values obtained from the two factory functions. + + +Using a resource from another resource factory +---------------------------------------------- + +You can use the database resource from a another resource factory through +the ``node.getresource()`` method. Let's add a resource factory for +a "db_users" table at module-level, extending the previous db-example:: + + def pytest_runtest_init(session): + ... + session.register_factory("db_users", createusers, atnode=module) + + def createusers(name, node): + db = node.getresource("db") + table = db.create_table("users", ...) + node.addfinalizer(lambda: db.destroy_table("users") + + def test_user_creation(db_users): + ... + +The create-users will be called for each module. After the tests in +that module finish execution, the table will be destroyed according +to registered finalizer. Note that calling getresource() for a resource +which has a tighter scope will raise a LookupError because the +is not available at a more general scope. Concretely, if you +table is defined as a per-session resource and the database object as a +per-module one, the table creation cannot work on a per-session basis. + + +Setting resources as class attributes +------------------------------------------- + +If you want to make an attribute available on a test class, you can +use the resource_attr marker:: + + @pytest.mark.resource_attr("db") + class TestClass: + def test_something(self): + #use self.db + +Note that this way of using resources can be used on unittest.TestCase +instances as well (function arguments can not be added due to unittest +limitations). + + +How the funcarg mechanism is implemented (internal notes) +------------------------------------------------------------- + +Prior to pytest-2.3/4, pytest advertised the "funcarg" mechanism +which provided a subset functionality to the generalized resource management. +In fact, the previous mechanism is implemented in terms of the new API +and should continue to work unmodified. It basically automates the +registration of factories through automatic discovery of +``pytest_funcarg_NAME`` function on plugins, Python modules and classes. + +As an example let's consider the Module.setup() method:: + + class Module(PyCollector): + def setup(self): + for name, func in self.obj.__dict__.items(): + if name.startswith("pytest_funcarg__"): + resourcename = name[len("pytest_funcarg__"):] + self._register_factory(resourcename, + RequestAdapter(self, name, func)) + +The request adapater takes care to provide the pre-2.3 API for funcarg +factories, providing request.cached_setup/addfinalizer/getfuncargvalue +methods. https://bitbucket.org/hpk42/pytest/changeset/62b2ea480504/ changeset: 62b2ea480504 user: hpk42 date: 2012-07-16 10:47:41 summary: v2 of resources API draft affected #: 17 files diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/assert.txt --- a/doc/en/assert.txt +++ b/doc/en/assert.txt @@ -24,7 +24,8 @@ $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_assert1.py F @@ -38,7 +39,7 @@ E + where 3 = f() test_assert1.py:5: AssertionError - ========================= 1 failed in 0.01 seconds ========================= + ========================= 1 failed in 0.02 seconds ========================= py.test has support for showing the values of the most common subexpressions including calls, attributes, comparisons, and binary and unary @@ -106,7 +107,8 @@ $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_assert2.py F @@ -125,7 +127,7 @@ E '5' test_assert2.py:5: AssertionError - ========================= 1 failed in 0.01 seconds ========================= + ========================= 1 failed in 0.02 seconds ========================= Special comparisons are done for a number of cases: @@ -182,7 +184,7 @@ E vals: 1 != 2 test_foocompare.py:8: AssertionError - 1 failed in 0.01 seconds + 1 failed in 0.02 seconds .. _assert-details: .. _`assert introspection`: diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/builtin.txt --- a/doc/en/builtin.txt +++ b/doc/en/builtin.txt @@ -28,7 +28,8 @@ $ py.test --funcargs =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collected 0 items pytestconfig the pytest config object with access to command line opts. @@ -76,5 +77,7 @@ See http://docs.python.org/library/warnings.html for information on warning categories. + cov + A pytest funcarg that provides access to the underlying coverage object. - ============================= in 0.00 seconds ============================= + ============================= in 0.01 seconds ============================= diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/capture.txt --- a/doc/en/capture.txt +++ b/doc/en/capture.txt @@ -64,7 +64,8 @@ $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 2 items test_module.py .F @@ -78,8 +79,8 @@ test_module.py:9: AssertionError ----------------------------- Captured stdout ------------------------------ - setting up - ==================== 1 failed, 1 passed in 0.01 seconds ==================== + setting up + ==================== 1 failed, 1 passed in 0.02 seconds ==================== Accessing captured output from a test function --------------------------------------------------- diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/conf.py --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,7 +17,7 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = release = "2.3.0.dev1" +version = release = "2.3.0.dev5" import sys, os diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/contents.txt --- a/doc/en/contents.txt +++ b/doc/en/contents.txt @@ -23,4 +23,5 @@ :hidden: changelog.txt + examples/resources.txt diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/doctest.txt --- a/doc/en/doctest.txt +++ b/doc/en/doctest.txt @@ -44,9 +44,10 @@ $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items mymodule.py . - ========================= 1 passed in 0.02 seconds ========================= + ========================= 1 passed in 0.07 seconds ========================= diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/example/markers.txt --- a/doc/en/example/markers.txt +++ b/doc/en/example/markers.txt @@ -26,25 +26,29 @@ $ py.test -v -m webtest =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.2.5.dev1 -- /home/hpk/venv/1/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-305/.cache + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 2 items test_server.py:3: test_send_http PASSED =================== 1 tests deselected by "-m 'webtest'" =================== - ================== 1 passed, 1 deselected in 0.00 seconds ================== + ================== 1 passed, 1 deselected in 0.02 seconds ================== Or the inverse, running all tests except the webtest ones:: $ py.test -v -m "not webtest" =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.2.5.dev1 -- /home/hpk/venv/1/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-305/.cache + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 2 items test_server.py:6: test_something_quick PASSED ================= 1 tests deselected by "-m 'not webtest'" ================= - ================== 1 passed, 1 deselected in 0.01 seconds ================== + ================== 1 passed, 1 deselected in 0.02 seconds ================== Registering markers ------------------------------------- @@ -143,38 +147,41 @@ $ py.test -k send_http # running with the above defined examples =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.2.5.dev1 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 4 items test_server.py . =================== 3 tests deselected by '-ksend_http' ==================== - ================== 1 passed, 3 deselected in 0.01 seconds ================== + ================== 1 passed, 3 deselected in 0.02 seconds ================== And you can also run all tests except the ones that match the keyword:: $ py.test -k-send_http =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.2.5.dev1 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 4 items test_mark_classlevel.py .. test_server.py . =================== 1 tests deselected by '-k-send_http' =================== - ================== 3 passed, 1 deselected in 0.01 seconds ================== + ================== 3 passed, 1 deselected in 0.02 seconds ================== Or to only select the class:: $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.2.5.dev1 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 4 items test_mark_classlevel.py .. =================== 2 tests deselected by '-kTestClass' ==================== - ================== 2 passed, 2 deselected in 0.01 seconds ================== + ================== 2 passed, 2 deselected in 0.02 seconds ================== .. _`adding a custom marker from a plugin`: @@ -223,23 +230,25 @@ $ py.test -E stage2 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.2.5.dev1 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_someenv.py s - ======================== 1 skipped in 0.01 seconds ========================= + ======================== 1 skipped in 0.02 seconds ========================= and here is one that specifies exactly the environment needed:: $ py.test -E stage1 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.2.5.dev1 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_someenv.py . - ========================= 1 passed in 0.01 seconds ========================= + ========================= 1 passed in 0.02 seconds ========================= The ``--markers`` option always gives you a list of available markers:: @@ -298,7 +307,7 @@ glob args=('class',) kwargs={'x': 2} glob args=('module',) kwargs={'x': 1} . - 1 passed in 0.01 seconds + 1 passed in 0.02 seconds marking platform specific tests with pytest -------------------------------------------------------------- @@ -351,25 +360,27 @@ $ py.test -rs # this option reports skip reasons =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.2.5.dev1 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 4 items test_plat.py s.s. ========================= short test summary info ========================== - SKIP [2] /home/hpk/tmp/doc-exec-222/conftest.py:12: cannot run on platform linux2 + SKIP [2] /home/hpk/tmp/doc-exec-305/conftest.py:12: cannot run on platform linux2 - =================== 2 passed, 2 skipped in 0.01 seconds ==================== + =================== 2 passed, 2 skipped in 0.02 seconds ==================== Note that if you specify a platform via the marker-command line option like this:: $ py.test -m linux2 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.2.5.dev1 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 4 items test_plat.py . =================== 3 tests deselected by "-m 'linux2'" ==================== - ================== 1 passed, 3 deselected in 0.01 seconds ================== + ================== 1 passed, 3 deselected in 0.02 seconds ================== then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests. diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/example/mysetup.txt --- a/doc/en/example/mysetup.txt +++ b/doc/en/example/mysetup.txt @@ -49,7 +49,8 @@ $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_sample.py F @@ -57,7 +58,7 @@ ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -66,7 +67,7 @@ E assert 54 == 42 test_sample.py:4: AssertionError - ========================= 1 failed in 0.01 seconds ========================= + ========================= 1 failed in 0.02 seconds ========================= This means that our ``mysetup`` object was successfully instantiated and ``mysetup.app()`` returned an initialized ``MyApp`` instance. @@ -122,14 +123,15 @@ $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-220/conftest.py:22: specify ssh host with --ssh + SKIP [1] /home/hpk/tmp/doc-exec-306/conftest.py:22: specify ssh host with --ssh - ======================== 1 skipped in 0.01 seconds ========================= + ======================== 1 skipped in 0.02 seconds ========================= If you specify a command line option like ``py.test --ssh=python.org`` the test will execute as expected. diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/example/nonpython.txt --- a/doc/en/example/nonpython.txt +++ b/doc/en/example/nonpython.txt @@ -27,7 +27,8 @@ nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 2 items test_simple.yml .F @@ -37,7 +38,7 @@ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.06 seconds ==================== + ==================== 1 failed, 1 passed in 0.11 seconds ==================== You get one dot for the passing ``sub1: sub1`` check and one failure. Obviously in the above ``conftest.py`` you'll want to implement a more @@ -56,7 +57,9 @@ nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/p/pytest/doc/en/.cache + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 2 items test_simple.yml:1: usecase: ok PASSED @@ -67,17 +70,18 @@ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.06 seconds ==================== + ==================== 1 failed, 1 passed in 0.04 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: nonpython $ py.test --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 2 items - ============================= in 0.07 seconds ============================= + ============================= in 0.04 seconds ============================= diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/example/resources.txt --- a/doc/en/example/resources.txt +++ b/doc/en/example/resources.txt @@ -2,38 +2,65 @@ V2: Creating and working with parametrized test resources =============================================================== -# XXX collection versus setup-time -# XXX parametrize-relation? +pytest-2.X provides generalized resource parametrization, unifying +and extending all existing funcarg and parametrization features of +previous pytest versions. Existing test suites and plugins written +for previous pytest versions shall run unmodified. -pytest-2.3 provides generalized resource management allowing -to flexibly manage caching and parametrization across your test suite. +This V2 draft focuses on incorporating feedback provided by Floris Bruynooghe, +Carl Meyer and Ronny Pfannschmidt. It remains as draft documentation, pending +further refinements and changes according to implementation or backward +compatibility issues. The main changes to V1 are: -This is draft documentation, pending refinements and changes according -to feedback and to implementation or backward compatibility issues -(the new mechanism is supposed to allow fully backward compatible -operations for uses of the "funcarg" mechanism. +* changed API names (atnode -> scopenode) +* register_factory now happens at Node.collect_init() or pytest_collection_init + time. It will raise an Error if called during the runtestloop + (which performs setup/call/teardown for each collected test). +* new examples and notes related to @parametrize and metafunc.parametrize() +* use 2.X as the version for introduction - not sure if 2.3 or 2.4 will + actually bring it. +* examples/uses which were previously not possible to implement easily + are marked with "NEW" in the title. -the new global pytest_runtest_init hook +(NEW) the init_collection and init_runtestloop hooks ------------------------------------------------------ -Prior to 2.3, pytest offered a pytest_configure and a pytest_sessionstart -hook which was used often to setup global resources. This suffers from -several problems. First of all, in distributed testing the master would -also setup test resources that are never needed because it only co-ordinates -the test run activities of the slave processes. Secondly, in large test -suites resources are setup that might not be needed for the concrete test -run. The first issue is solved through the introduction of a specific -hook:: +pytest for a long time offers a pytest_configure and a pytest_sessionstart +hook which are often used to setup global resources. This suffers from +several problems: - def pytest_runtest_init(session): - # called ahead of pytest_runtestloop() test execution +1. in distributed testing the master process would setup test resources + that are never needed because it only co-ordinates the test run + activities of the slave processes. -This hook will only be called in processes that actually run tests. +2. In large test suites resources are created which might not be needed + for the concrete test run. -The second issue is solved through a new register/getresource API which -will only ever setup resources if they are needed. See the following -examples and sections on how this works. +3. Thirdly, even if you only perform a collection (with "--collectonly") + resource-setup will be executed. +4. there is no place way to allow global parametrized collection and setup + +The existing hooks are not a good place regarding these issues. pytest-2.X +solves all these issues through the introduction of two specific hooks +(and the new register_factory/getresource API):: + + def pytest_init_collection(session): + # called ahead of pytest_collection, which implements the + # collection process + + def pytest_init_runtestloop(session): + # called ahead of pytest_runtestloop() which executes the + # setup and calling of tests + +The pytest_init_collection hook can be used for registering resources, +see `global resource management`_ and `parametrizing global resources`_. + +The init_runtests can be used to setup and/or interact with global +resources. If you just use a global resource, you may explicitely +use it in a function argument or through a `class resource attribute`_. + +.. _`global resource management`: managing a global database resource --------------------------------------------------------------- @@ -41,6 +68,8 @@ If you have one database object which you want to use in tests you can write the following into a conftest.py file:: + # contest of conftest.py + class Database: def __init__(self): print ("database instance created") @@ -52,51 +81,71 @@ node.addfinalizer(db.destroy) return db - def pytest_runtest_init(session): - session.register_resource("db", factory_db, atnode=session) + def pytest_init_collection(session): + session.register_factory("db", factory_db) -You can then access the constructed resource in a test like this:: +You can then access the constructed resource in a test by specifying +the pre-registered name in your function definition:: def test_something(db): ... -The "db" function argument will lead to a lookup of the respective -factory value and be passed to the function body. According to the -registration, the db object will be instantiated on a per-session basis -and thus reused across all test functions that require it. +The "db" function argument will lead to a lookup and call of the respective +factory function and its result will be passed to the function body. +As the factory is registered on the session, it will by default only +get called once per session and its value will thus be re-used across +the whole test session. -instantiating a database resource per-module +Previously, factories would need to call the ``request.cached_setup()`` +method to manage caching. Here is how we could implement the above +with traditional funcargs:: + + # content of conftest.py + class DataBase: + ... as above + + def pytest_funcarg__db(request): + return request.cached_setup(setup=DataBase, + teardown=lambda db: db.destroy, + scope="session") + +As the funcarg factory is automatically registered by detecting its +name and because it is called each time "db" is requested, it needs +to care for caching itself, here by calling the cached_setup() method +to manage it. As it encodes the caching scope in the factory code body, +py.test has no way to report this via e. g. "py.test --funcargs". +More seriously, it's not exactly trivial to provide parametrization: +we would need to add a "parametrize" decorator where the resource is +used or implement a pytest_generate_tests(metafunc) hook to +call metafunc.parametrize() with the "db" argument, and then the +factory would need to care to pass the appropriate "extrakey" into +cached_setup(). By contrast, the new way just requires a modified +call to register factories:: + + def pytest_init_collection(session): + session.register_factory("db", [factory_mysql, factory_pg]) + +and no other code needs to change or get decorated. + +(NEW) instantiating one database for each test module --------------------------------------------------------------- If you want one database instance per test module you can restrict -caching by modifying the "atnode" parameter of the registration -call above:: +caching by modifying the "scopenode" parameter of the registration +call above: - def pytest_runtest_init(session): - session.register_resource("db", factory_db, atnode=pytest.Module) + def pytest_init_collection(session): + session.register_factory("db", factory_db, scopenode=pytest.Module) Neither the tests nor the factory function will need to change. -This also means that you can decide the scoping of resources -at runtime - e.g. based on a command line option: for developer -settings you might want per-session and for Continous Integration -runs you might prefer per-module or even per-function scope like this:: +This means that you can decide the scoping of resources at runtime - +e.g. based on a command line option: for developer settings you might +want per-session and for Continous Integration runs you might prefer +per-module or even per-function scope like this:: - def pytest_runtest_init(session): - session.register_resource_factory("db", factory_db, - atnode=pytest.Function) - -parametrized resources ----------------------------------- - -If you want to rerun tests with different resource values you can specify -a list of factories instead of just one:: - - def pytest_runtest_init(session): - session.register_factory("db", [factory1, factory2], atnode=session) - -In this case all tests that depend on the "db" resource will be run twice -using the respective values obtained from the two factory functions. - + def pytest_init_collection(session): + session.register_factory("db", factory_db, + scopenode=pytest.Function) Using a resource from another resource factory ---------------------------------------------- @@ -105,9 +154,11 @@ the ``node.getresource()`` method. Let's add a resource factory for a "db_users" table at module-level, extending the previous db-example:: - def pytest_runtest_init(session): + def pytest_init_collection(session): ... - session.register_factory("db_users", createusers, atnode=module) + # this factory will be using a scopenode=pytest.Module because + # it is defined in a test module. + session.register_factory("db_users", createusers) def createusers(name, node): db = node.getresource("db") @@ -125,43 +176,194 @@ table is defined as a per-session resource and the database object as a per-module one, the table creation cannot work on a per-session basis. +amending/decorating a resource / funcarg__ compatibility +---------------------------------------------------------------------- -Setting resources as class attributes +If you want to decorate a session-registered resource with +a test-module one, you can do the following:: + + # content of conftest.py + def pytest_init_collection(session): + session.register_factory("db_users", createusers) + +This will register a db_users method on a per-session basis. +If you want to create a dummy user such that all test +methods in a test module can work with it:: + + # content of test_user_admin.py + def setup_class(cls, db_users): + + def pytest_init_collection(session): + session.register_factory("db_users", createcreate_users, + scopenode=pytest.Module) + + def create_users(name, node): + # get the session-managed resource + db_users = node.getresource(name) + # add a user and define a remove_user undo function + ... + node.addfinalizer(remove_user) + return db_users + + def test_user_fields(db_users): + # work with db_users with a pre-created entry + ... + +Using the pytest_funcarg__ mechanism, you can do the equivalent:: + + # content of test_user_admin.py + + def pytest_funcarg__db_users(request): + def create_user(): + db_users = request.getfuncargvalue("db_users") + # add a user + return db_users + def remove_user(db_users): + ... + return request.cached_setup(create_user, remove_user, scope="module") + +As the funcarg mechanism is implemented in terms of the new API +it's also possible to mix - use register_factory/getresource at plugin-level +and pytest_funcarg__ factories at test module level. + +As discussed previously with `global resource management`_, the funcarg-factory +does not easily extend to provide parametrization. + + +.. _`class resource attributes`: + +(NEW) Setting resources as class attributes ------------------------------------------- If you want to make an attribute available on a test class, you can -use the resource_attr marker:: +use a new mark:: - @pytest.mark.resource_attr("db") + @pytest.mark.class_resource("db") class TestClass: def test_something(self): #use self.db -Note that this way of using resources can be used on unittest.TestCase -instances as well (function arguments can not be added due to unittest -limitations). +Note that this way of using resources work with unittest.TestCase-style +tests as well. If you have defined "db" as a parametrized resource, +the functions of the Test class will be run multiple times with different +values found in "self.db". +Previously, pytest could not offer its resource management features +since those were tied to passing function arguments ("funcargs") and +this cannot be easily integrated with the unittest framework and its +common per-project customizations. -How the funcarg mechanism is implemented (internal notes) + +.. _`parametrizing global resources`: + +(NEW) parametrizing global resources +---------------------------------------------------- + +If you want to rerun tests with different resource values you can specify +a list of factories instead of just one:: + + def pytest_init_collection(session): + session.register_factory("db", [factory1, factory2]) + +In this case all tests that require the "db" resource will be run twice +using the respective values obtained from the two factory functions. + +For reporting purposes you might want to also define identifiers +for the db values:: + + def pytest_init_collection(session): + session.register_factory("db", [factory1, factory2], + ids=["mysql", "pg"]) + +This will make pytest use the respective id values when reporting +nodeids. + + +(New) Declaring resource usage / implicit parametrization +---------------------------------------------------------- + +Sometimes you may have a resource that can work in multiple variants, +like using different database backends. As another use-case, +pytest's own test suite uses a "testdir" funcarg which helps to setup +example scenarios, perform a subprocess-pytest run and check the output. +However, there are many features that should also work with the pytest-xdist +mode, distributing tests to multiple CPUs or hosts. The invocation +variants are not visible in the function signature and cannot be easily +addressed through a "parametrize" decorator or call. Nevertheless we want +to have both invocation variants to be collected and executed. + +The solution is to tell pytest that you are using a resource implicitely:: + + @pytest.mark.uses_resource("invocation-option") + class TestClass: + def test_method(self, testdir): + ... + +When the testdir factory gets the parametrized "invocation-option" +resource, it will see different values, depending on what the respective +factories provide. To register the invocation-mode factory you would write:: + + # content of conftest.py + def pytest_init_collection(session): + session.register_factory("invocation-option", + [lambda **kw: "", lambda **kw: "-n1"]) + +The testdir factory can then access it easily:: + + option = node.getresource("invocation-option", "") + ... + +.. note:: + + apart from the "uses_resource" decoration none of the already + written test functions needs to be modified for the new API. + + The implicit "testdir" parametrization only happens for the tests + which declare use of the invocation-option resource. All other + tests will get the default value passed as the second parameter + to node.getresource() above. You can thus restrict + running the variants to particular tests or test sets. + +To conclude, these three code fragments work together to allow efficient +cross-session resource parametrization. + + +Implementation and compatibility notes +============================================================ + +The new API is designed to support all existing resource parametrization +and funcarg usages. This chapter discusses implementation aspects. +Feel free to choose ignorance and only consider the above usage-level. + +Implementing the funcarg mechanism in terms of the new API ------------------------------------------------------------- -Prior to pytest-2.3/4, pytest advertised the "funcarg" mechanism -which provided a subset functionality to the generalized resource management. -In fact, the previous mechanism is implemented in terms of the new API -and should continue to work unmodified. It basically automates the -registration of factories through automatic discovery of -``pytest_funcarg_NAME`` function on plugins, Python modules and classes. +Prior to pytest-2.X, pytest mainly advertised the "funcarg" mechanism +for resource management. It provides automatic registration of +factories through discovery of ``pytest_funcarg__NAME`` factory methods +on plugins, test modules, classes and functions. Those factories are be +called *each time* a resource (funcarg) is required, hence the support +for a ``request.cached_setup" method which helps to cache resources +across calls. Request objects internally keep a (item, requested_name, +remaining-factories) state. The "reamaining-factories" state is +used for implementing decorating factories; a factory for a given +name can call ``getfuncargvalue(name)`` to invoke the next-matching +factory factories and then amend the return value. -As an example let's consider the Module.setup() method:: +In order to implement the existing funcarg mechanism through +the new API, the new API needs to internally keep around similar +state. XXX + +As an example let's consider the Module.setup_collect() method:: class Module(PyCollector): - def setup(self): + def setup_collect(self): for name, func in self.obj.__dict__.items(): if name.startswith("pytest_funcarg__"): resourcename = name[len("pytest_funcarg__"):] - self._register_factory(resourcename, - RequestAdapter(self, name, func)) + self.register_factory(resourcename, + RequestAdapter(self, name, func)) -The request adapater takes care to provide the pre-2.3 API for funcarg -factories, providing request.cached_setup/addfinalizer/getfuncargvalue -methods. +The request adapater takes care to provide the pre-2.X API for funcarg +factories, i.e. request.cached_setup/addfinalizer/getfuncargvalue +methods and some attributes. diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/funcargs.txt --- a/doc/en/funcargs.txt +++ b/doc/en/funcargs.txt @@ -61,7 +61,8 @@ $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_simplefactory.py F @@ -76,7 +77,7 @@ E assert 42 == 17 test_simplefactory.py:5: AssertionError - ========================= 1 failed in 0.01 seconds ========================= + ========================= 1 failed in 0.02 seconds ========================= This shows that the test function was called with a ``myfuncarg`` argument value of ``42`` and the assert fails as expected. Here is @@ -154,7 +155,8 @@ $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 10 items test_example.py .........F @@ -169,7 +171,7 @@ E assert 9 < 9 test_example.py:6: AssertionError - ==================== 1 failed, 9 passed in 0.02 seconds ==================== + ==================== 1 failed, 9 passed in 0.03 seconds ==================== Obviously, only when ``numiter`` has the value of ``9`` does the test fail. Note that the ``pytest_generate_tests(metafunc)`` hook is called during the test collection phase which is separate from the actual test running. @@ -177,7 +179,8 @@ $ py.test --collectonly test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 10 items @@ -191,19 +194,39 @@ - ============================= in 0.00 seconds ============================= + ============================= in 0.02 seconds ============================= If you want to select only the run with the value ``7`` you could do:: $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-271/.cache + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 10 items + test_example.py:5: test_func[0] PASSED + test_example.py:5: test_func[1] PASSED + test_example.py:5: test_func[2] PASSED + test_example.py:5: test_func[3] PASSED + test_example.py:5: test_func[4] PASSED + test_example.py:5: test_func[5] PASSED + test_example.py:5: test_func[6] PASSED test_example.py:5: test_func[7] PASSED + test_example.py:5: test_func[8] PASSED + test_example.py:5: test_func[9] FAILED - ======================= 9 tests deselected by '-k7' ======================== - ================== 1 passed, 9 deselected in 0.01 seconds ================== + ================================= FAILURES ================================= + _______________________________ test_func[9] _______________________________ + + numiter = 9 + + def test_func(numiter): + > assert numiter < 9 + E assert 9 < 9 + + test_example.py:6: AssertionError + ==================== 1 failed, 9 passed in 0.03 seconds ==================== You might want to look at :ref:`more parametrization examples `. diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/getting-started.txt --- a/doc/en/getting-started.txt +++ b/doc/en/getting-started.txt @@ -22,9 +22,14 @@ To check your installation has installed the correct version:: $ py.test --version - This is py.test version 2.2.4, imported from /home/hpk/p/pytest/pytest.py + This is py.test version 2.3.0.dev2, imported from /home/hpk/p/pytest/pytest.pyc setuptools registered plugins: pytest-xdist-1.8 at /home/hpk/p/pytest-xdist/xdist/plugin.pyc + pytest-bugzilla-0.1 at /home/hpk/tmp/eanxgeek/pytest_bugzilla.pyc + pytest-cache-0.9 at /home/hpk/p/pytest-cache/pytest_cache.pyc + oejskit-0.9.0 at /home/hpk/p/js-infrastructure/oejskit/pytest_jstests.pyc + pytest-pep8-1.0.1 at /home/hpk/venv/1/local/lib/python2.7/site-packages/pytest_pep8.pyc + pytest-cov-1.6 at /home/hpk/venv/1/local/lib/python2.7/site-packages/pytest_cov.pyc If you get an error checkout :ref:`installation issues`. @@ -46,7 +51,8 @@ $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_sample.py F @@ -60,7 +66,7 @@ E + where 4 = func(3) test_sample.py:5: AssertionError - ========================= 1 failed in 0.01 seconds ========================= + ========================= 1 failed in 0.02 seconds ========================= py.test found the ``test_answer`` function by following :ref:`standard test discovery rules `, basically detecting the ``test_`` prefixes. We got a failure report because our little ``func(3)`` call did not return ``5``. @@ -95,7 +101,7 @@ $ py.test -q test_sysexit.py collecting ... collected 1 items . - 1 passed in 0.00 seconds + 1 passed in 0.02 seconds .. todo:: For further ways to assert exceptions see the `raises` @@ -126,7 +132,7 @@ ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -134,7 +140,7 @@ E assert hasattr('hello', 'check') test_class.py:8: AssertionError - 1 failed, 1 passed in 0.01 seconds + 1 failed, 1 passed in 0.02 seconds The first test passed, the second failed. Again we can easily see the intermediate values used in the assertion, helping us to @@ -163,7 +169,7 @@ ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - tmpdir = local('/tmp/pytest-22/test_needsfiles0') + tmpdir = local('/home/hpk/tmp/pytest-2885/test_needsfiles0') def test_needsfiles(tmpdir): print tmpdir @@ -172,8 +178,8 @@ test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-22/test_needsfiles0 - 1 failed in 0.01 seconds + /home/hpk/tmp/pytest-2885/test_needsfiles0 + 1 failed in 0.22 seconds Before the test runs, a unique-per-test-invocation temporary directory was created. More info at :ref:`tmpdir handling`. diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/index.txt --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -37,7 +37,7 @@ - **integrates many common testing methods** - - can integrate ``nose``, ``unittest.py`` and ``doctest.py`` style + - can run many ``nose``, ``unittest.py`` and ``doctest.py`` style tests, including running testcases made for Django and trial - supports extended :ref:`xUnit style setup ` - supports domain-specific :ref:`non-python tests` diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/skipping.txt --- a/doc/en/skipping.txt +++ b/doc/en/skipping.txt @@ -130,7 +130,8 @@ example $ py.test -rx xfail_demo.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 6 items xfail_demo.py xxxxxx @@ -147,7 +148,7 @@ XFAIL xfail_demo.py::test_hello6 reason: reason - ======================== 6 xfailed in 0.03 seconds ========================= + ======================== 6 xfailed in 0.04 seconds ========================= .. _`evaluation of skipif/xfail conditions`: diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/tmpdir.txt --- a/doc/en/tmpdir.txt +++ b/doc/en/tmpdir.txt @@ -28,7 +28,8 @@ $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_tmpdir.py F @@ -36,7 +37,7 @@ ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-23/test_create_file0') + tmpdir = local('/home/hpk/tmp/pytest-2886/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") @@ -47,7 +48,7 @@ E assert 0 test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.23 seconds ========================= .. _`base temporary directory`: diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/unittest.txt --- a/doc/en/unittest.txt +++ b/doc/en/unittest.txt @@ -24,7 +24,8 @@ $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.1 -- pytest-2.2.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + plugins: xdist, bugzilla, cache, oejskit, pep8, cov collecting ... collected 1 items test_unittest.py F @@ -42,7 +43,7 @@ test_unittest.py:8: AssertionError ----------------------------- Captured stdout ------------------------------ hello - ========================= 1 failed in 0.01 seconds ========================= + ========================= 1 failed in 0.03 seconds ========================= .. _`unittest.py style`: http://docs.python.org/library/unittest.html diff -r 748fede1c05eb3c2c5f181559b71843488cca839 -r 62b2ea480504673325a25020f6a83595e4d1390f doc/en/usage.txt --- a/doc/en/usage.txt +++ b/doc/en/usage.txt @@ -185,7 +185,7 @@ $ python myinvoke.py collecting ... collected 0 items - in 0.00 seconds + in 0.01 seconds *** test run reporting finishing .. include:: links.inc https://bitbucket.org/hpk42/pytest/changeset/4f076fee4f6d/ changeset: 4f076fee4f6d user: hpk42 date: 2012-07-18 19:48:43 summary: ci affected #: 1 file diff -r 62b2ea480504673325a25020f6a83595e4d1390f -r 4f076fee4f6d9f1710701507fded8fe994d9431a doc/en/goodpractises.txt --- a/doc/en/goodpractises.txt +++ b/doc/en/goodpractises.txt @@ -119,7 +119,7 @@ setup( #..., tests_require=['pytest'], - cmdclass = {'test': pytest}, + cmdclass = {'test': PyTest}, ) Now if you run:: 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. From commits-noreply at bitbucket.org Fri Jul 20 14:17:04 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Fri, 20 Jul 2012 12:17:04 -0000 Subject: [py-svn] commit/pytest: 8 new changesets Message-ID: <20120720121704.24634.67632@bitbucket02.managed.contegix.com> 8 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/ffe816408bb9/ changeset: ffe816408bb9 user: hpk42 date: 2012-07-16 11:11:26 summary: V3 draft of resource api affected #: 7 files diff -r 4f076fee4f6d9f1710701507fded8fe994d9431a -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -610,6 +610,7 @@ yield x node.ihook.pytest_collectreport(report=rep) + # XXX not used yet def register_resource_factory(self, name, factoryfunc, matchscope=None, cachescope=None): @@ -634,4 +635,3 @@ - diff -r 4f076fee4f6d9f1710701507fded8fe994d9431a -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 doc/en/conf.py --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -27,6 +27,7 @@ #sys.path.insert(0, os.path.abspath('.')) autodoc_member_order = "bysource" +todo_include_todos = 1 # -- General configuration ----------------------------------------------------- diff -r 4f076fee4f6d9f1710701507fded8fe994d9431a -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 doc/en/contents.txt --- a/doc/en/contents.txt +++ b/doc/en/contents.txt @@ -23,5 +23,6 @@ :hidden: changelog.txt - examples/resources.txt + resources + example/resources_attic diff -r 4f076fee4f6d9f1710701507fded8fe994d9431a -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 doc/en/example/resources.txt --- a/doc/en/example/resources.txt +++ /dev/null @@ -1,369 +0,0 @@ - -V2: Creating and working with parametrized test resources -=============================================================== - -pytest-2.X provides generalized resource parametrization, unifying -and extending all existing funcarg and parametrization features of -previous pytest versions. Existing test suites and plugins written -for previous pytest versions shall run unmodified. - -This V2 draft focuses on incorporating feedback provided by Floris Bruynooghe, -Carl Meyer and Ronny Pfannschmidt. It remains as draft documentation, pending -further refinements and changes according to implementation or backward -compatibility issues. The main changes to V1 are: - -* changed API names (atnode -> scopenode) -* register_factory now happens at Node.collect_init() or pytest_collection_init - time. It will raise an Error if called during the runtestloop - (which performs setup/call/teardown for each collected test). -* new examples and notes related to @parametrize and metafunc.parametrize() -* use 2.X as the version for introduction - not sure if 2.3 or 2.4 will - actually bring it. -* examples/uses which were previously not possible to implement easily - are marked with "NEW" in the title. - -(NEW) the init_collection and init_runtestloop hooks ------------------------------------------------------- - -pytest for a long time offers a pytest_configure and a pytest_sessionstart -hook which are often used to setup global resources. This suffers from -several problems: - -1. in distributed testing the master process would setup test resources - that are never needed because it only co-ordinates the test run - activities of the slave processes. - -2. In large test suites resources are created which might not be needed - for the concrete test run. - -3. Thirdly, even if you only perform a collection (with "--collectonly") - resource-setup will be executed. - -4. there is no place way to allow global parametrized collection and setup - -The existing hooks are not a good place regarding these issues. pytest-2.X -solves all these issues through the introduction of two specific hooks -(and the new register_factory/getresource API):: - - def pytest_init_collection(session): - # called ahead of pytest_collection, which implements the - # collection process - - def pytest_init_runtestloop(session): - # called ahead of pytest_runtestloop() which executes the - # setup and calling of tests - -The pytest_init_collection hook can be used for registering resources, -see `global resource management`_ and `parametrizing global resources`_. - -The init_runtests can be used to setup and/or interact with global -resources. If you just use a global resource, you may explicitely -use it in a function argument or through a `class resource attribute`_. - -.. _`global resource management`: - -managing a global database resource ---------------------------------------------------------------- - -If you have one database object which you want to use in tests -you can write the following into a conftest.py file:: - - # contest of conftest.py - - class Database: - def __init__(self): - print ("database instance created") - def destroy(self): - print ("database instance destroyed") - - def factory_db(name, node): - db = Database() - node.addfinalizer(db.destroy) - return db - - def pytest_init_collection(session): - session.register_factory("db", factory_db) - -You can then access the constructed resource in a test by specifying -the pre-registered name in your function definition:: - - def test_something(db): - ... - -The "db" function argument will lead to a lookup and call of the respective -factory function and its result will be passed to the function body. -As the factory is registered on the session, it will by default only -get called once per session and its value will thus be re-used across -the whole test session. - -Previously, factories would need to call the ``request.cached_setup()`` -method to manage caching. Here is how we could implement the above -with traditional funcargs:: - - # content of conftest.py - class DataBase: - ... as above - - def pytest_funcarg__db(request): - return request.cached_setup(setup=DataBase, - teardown=lambda db: db.destroy, - scope="session") - -As the funcarg factory is automatically registered by detecting its -name and because it is called each time "db" is requested, it needs -to care for caching itself, here by calling the cached_setup() method -to manage it. As it encodes the caching scope in the factory code body, -py.test has no way to report this via e. g. "py.test --funcargs". -More seriously, it's not exactly trivial to provide parametrization: -we would need to add a "parametrize" decorator where the resource is -used or implement a pytest_generate_tests(metafunc) hook to -call metafunc.parametrize() with the "db" argument, and then the -factory would need to care to pass the appropriate "extrakey" into -cached_setup(). By contrast, the new way just requires a modified -call to register factories:: - - def pytest_init_collection(session): - session.register_factory("db", [factory_mysql, factory_pg]) - -and no other code needs to change or get decorated. - -(NEW) instantiating one database for each test module ---------------------------------------------------------------- - -If you want one database instance per test module you can restrict -caching by modifying the "scopenode" parameter of the registration -call above: - - def pytest_init_collection(session): - session.register_factory("db", factory_db, scopenode=pytest.Module) - -Neither the tests nor the factory function will need to change. -This means that you can decide the scoping of resources at runtime - -e.g. based on a command line option: for developer settings you might -want per-session and for Continous Integration runs you might prefer -per-module or even per-function scope like this:: - - def pytest_init_collection(session): - session.register_factory("db", factory_db, - scopenode=pytest.Function) - -Using a resource from another resource factory ----------------------------------------------- - -You can use the database resource from a another resource factory through -the ``node.getresource()`` method. Let's add a resource factory for -a "db_users" table at module-level, extending the previous db-example:: - - def pytest_init_collection(session): - ... - # this factory will be using a scopenode=pytest.Module because - # it is defined in a test module. - session.register_factory("db_users", createusers) - - def createusers(name, node): - db = node.getresource("db") - table = db.create_table("users", ...) - node.addfinalizer(lambda: db.destroy_table("users") - - def test_user_creation(db_users): - ... - -The create-users will be called for each module. After the tests in -that module finish execution, the table will be destroyed according -to registered finalizer. Note that calling getresource() for a resource -which has a tighter scope will raise a LookupError because the -is not available at a more general scope. Concretely, if you -table is defined as a per-session resource and the database object as a -per-module one, the table creation cannot work on a per-session basis. - -amending/decorating a resource / funcarg__ compatibility ----------------------------------------------------------------------- - -If you want to decorate a session-registered resource with -a test-module one, you can do the following:: - - # content of conftest.py - def pytest_init_collection(session): - session.register_factory("db_users", createusers) - -This will register a db_users method on a per-session basis. -If you want to create a dummy user such that all test -methods in a test module can work with it:: - - # content of test_user_admin.py - def setup_class(cls, db_users): - - def pytest_init_collection(session): - session.register_factory("db_users", createcreate_users, - scopenode=pytest.Module) - - def create_users(name, node): - # get the session-managed resource - db_users = node.getresource(name) - # add a user and define a remove_user undo function - ... - node.addfinalizer(remove_user) - return db_users - - def test_user_fields(db_users): - # work with db_users with a pre-created entry - ... - -Using the pytest_funcarg__ mechanism, you can do the equivalent:: - - # content of test_user_admin.py - - def pytest_funcarg__db_users(request): - def create_user(): - db_users = request.getfuncargvalue("db_users") - # add a user - return db_users - def remove_user(db_users): - ... - return request.cached_setup(create_user, remove_user, scope="module") - -As the funcarg mechanism is implemented in terms of the new API -it's also possible to mix - use register_factory/getresource at plugin-level -and pytest_funcarg__ factories at test module level. - -As discussed previously with `global resource management`_, the funcarg-factory -does not easily extend to provide parametrization. - - -.. _`class resource attributes`: - -(NEW) Setting resources as class attributes -------------------------------------------- - -If you want to make an attribute available on a test class, you can -use a new mark:: - - @pytest.mark.class_resource("db") - class TestClass: - def test_something(self): - #use self.db - -Note that this way of using resources work with unittest.TestCase-style -tests as well. If you have defined "db" as a parametrized resource, -the functions of the Test class will be run multiple times with different -values found in "self.db". - -Previously, pytest could not offer its resource management features -since those were tied to passing function arguments ("funcargs") and -this cannot be easily integrated with the unittest framework and its -common per-project customizations. - - -.. _`parametrizing global resources`: - -(NEW) parametrizing global resources ----------------------------------------------------- - -If you want to rerun tests with different resource values you can specify -a list of factories instead of just one:: - - def pytest_init_collection(session): - session.register_factory("db", [factory1, factory2]) - -In this case all tests that require the "db" resource will be run twice -using the respective values obtained from the two factory functions. - -For reporting purposes you might want to also define identifiers -for the db values:: - - def pytest_init_collection(session): - session.register_factory("db", [factory1, factory2], - ids=["mysql", "pg"]) - -This will make pytest use the respective id values when reporting -nodeids. - - -(New) Declaring resource usage / implicit parametrization ----------------------------------------------------------- - -Sometimes you may have a resource that can work in multiple variants, -like using different database backends. As another use-case, -pytest's own test suite uses a "testdir" funcarg which helps to setup -example scenarios, perform a subprocess-pytest run and check the output. -However, there are many features that should also work with the pytest-xdist -mode, distributing tests to multiple CPUs or hosts. The invocation -variants are not visible in the function signature and cannot be easily -addressed through a "parametrize" decorator or call. Nevertheless we want -to have both invocation variants to be collected and executed. - -The solution is to tell pytest that you are using a resource implicitely:: - - @pytest.mark.uses_resource("invocation-option") - class TestClass: - def test_method(self, testdir): - ... - -When the testdir factory gets the parametrized "invocation-option" -resource, it will see different values, depending on what the respective -factories provide. To register the invocation-mode factory you would write:: - - # content of conftest.py - def pytest_init_collection(session): - session.register_factory("invocation-option", - [lambda **kw: "", lambda **kw: "-n1"]) - -The testdir factory can then access it easily:: - - option = node.getresource("invocation-option", "") - ... - -.. note:: - - apart from the "uses_resource" decoration none of the already - written test functions needs to be modified for the new API. - - The implicit "testdir" parametrization only happens for the tests - which declare use of the invocation-option resource. All other - tests will get the default value passed as the second parameter - to node.getresource() above. You can thus restrict - running the variants to particular tests or test sets. - -To conclude, these three code fragments work together to allow efficient -cross-session resource parametrization. - - -Implementation and compatibility notes -============================================================ - -The new API is designed to support all existing resource parametrization -and funcarg usages. This chapter discusses implementation aspects. -Feel free to choose ignorance and only consider the above usage-level. - -Implementing the funcarg mechanism in terms of the new API -------------------------------------------------------------- - -Prior to pytest-2.X, pytest mainly advertised the "funcarg" mechanism -for resource management. It provides automatic registration of -factories through discovery of ``pytest_funcarg__NAME`` factory methods -on plugins, test modules, classes and functions. Those factories are be -called *each time* a resource (funcarg) is required, hence the support -for a ``request.cached_setup" method which helps to cache resources -across calls. Request objects internally keep a (item, requested_name, -remaining-factories) state. The "reamaining-factories" state is -used for implementing decorating factories; a factory for a given -name can call ``getfuncargvalue(name)`` to invoke the next-matching -factory factories and then amend the return value. - -In order to implement the existing funcarg mechanism through -the new API, the new API needs to internally keep around similar -state. XXX - -As an example let's consider the Module.setup_collect() method:: - - class Module(PyCollector): - def setup_collect(self): - for name, func in self.obj.__dict__.items(): - if name.startswith("pytest_funcarg__"): - resourcename = name[len("pytest_funcarg__"):] - self.register_factory(resourcename, - RequestAdapter(self, name, func)) - -The request adapater takes care to provide the pre-2.X API for funcarg -factories, i.e. request.cached_setup/addfinalizer/getfuncargvalue -methods and some attributes. diff -r 4f076fee4f6d9f1710701507fded8fe994d9431a -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 doc/en/funcargs.txt --- a/doc/en/funcargs.txt +++ b/doc/en/funcargs.txt @@ -110,12 +110,13 @@ The request object passed to factories ----------------------------------------- -Each funcarg factory receives a :py:class:`~_pytest.main.Request` object which +Each funcarg factory receives a :py:class:`~_pytest.python.Request` object which provides methods to manage caching and finalization in the context of the test invocation as well as several attributes of the the underlying test item. In fact, as of version pytest-2.3, the request API is implemented on all Item objects and therefore the request object has general :py:class:`Node attributes and methods <_pytest.main.Node>` attributes. This is a backward compatible change so no changes are neccessary for pre-2.3 funcarg factories. + .. _`parametrizing tests, generalized`: http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests-generalized/ .. _`blog post about the monkeypatch funcarg`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ diff -r 4f076fee4f6d9f1710701507fded8fe994d9431a -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 doc/en/plugins.txt --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -296,6 +296,7 @@ The :py:mod:`_pytest.terminal` reported specifically uses the reporting hook to print information about a test run. + Collection hooks ------------------------------ @@ -309,6 +310,7 @@ you can use the following hook: .. autofunction:: pytest_pycollect_makeitem +.. autofunction:: pytest_generate_tests Reporting hooks @@ -329,7 +331,7 @@ Reference of objects involved in hooks =========================================================== -.. autoclass:: _pytest.main.Request() +.. autoclass:: _pytest.python.Request() :members: .. autoclass:: _pytest.config.Config() diff -r 4f076fee4f6d9f1710701507fded8fe994d9431a -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 doc/en/resources.txt --- /dev/null +++ b/doc/en/resources.txt @@ -0,0 +1,426 @@ + +V3: Creating and working with parametrized test resources +=============================================================== + +**Target audience**: Reading this document requires basic knowledge of +python testing, xUnit setup methods and the basic pytest funcarg mechanism, +see http://pytest.org/latest/funcargs.html + +**Abstract**: pytest-2.X provides more powerful and more flexible funcarg +and setup machinery. It does so by introducing a new @funcarg and a +new @setup marker which allows to define scoping and parametrization +parameters. If using ``@funcarg``, following the ``pytest_funcarg__`` +naming pattern becomes optional. Functions decorated with ``@setup`` +are called independenlty from the definition of funcargs but can +access funcarg values if needed. This allows for ultimate flexibility +in designing your test fixtures and their parametrization. Also, +you can now use ``py.test --collectonly`` to inspect your fixture +setup. Nonwithstanding these extensions, pre-existing test suites +and plugins written to work for previous pytest versions shall run unmodified. + + +**Changes**: This V3 draft is based on incorporating and thinking about +feedback provided by Floris Bruynooghe, Carl Meyer and Samuele Pedroni. +It remains as draft documentation, pending further refinements and +changes according to implementation or backward compatibility issues. +The main changes to V2 are: + +* Collapse funcarg factory decorator into a single "@funcarg" one. + You can specify scopes and params with it. Moreover, if you supply + a "name" you do not need to follow the "pytest_funcarg__NAME" naming + pattern. Keeping with "funcarg" naming arguable now makes more + sense since the main interface using these resources are test and + setup functions. Keeping it probably causes the least semantic friction. + +* Drop setup_directory/setup_session and introduce a new @setup + decorator similar to the @funcarg one but accepting funcargs. + +* cosnider the extended setup_X funcargs for dropping because + the new @setup decorator probably is more flexible and introduces + less implementation complexity. + +.. currentmodule:: _pytest + + +Shortcomings of the previous pytest_funcarg__ mechanism +--------------------------------------------------------- + +The previous funcarg mechanism calls a factory each time a +funcarg for a test function is requested. If a factory wants +t re-use a resource across different scopes, it often used +the ``request.cached_setup()`` helper to manage caching of +resources. Here is a basic example how we could implement +a per-session Database object:: + + # content of conftest.py + class Database: + def __init__(self): + print ("database instance created") + def destroy(self): + print ("database instance destroyed") + + def pytest_funcarg__db(request): + return request.cached_setup(setup=DataBase, + teardown=lambda db: db.destroy, + scope="session") + +There are some problems with this approach: + +1. Scoping resource creation is not straight forward, instead one must + understand the intricate cached_setup() method mechanics. + +2. parametrizing the "db" resource is not straight forward: + you need to apply a "parametrize" decorator or implement a + :py:func:`~hookspec.pytest_generate_tests` hook + calling :py:func:`~python.Metafunc.parametrize` which + performs parametrization at the places where the resource + is used. Moreover, you need to modify the factory to use an + ``extrakey`` parameter containing ``request.param`` to the + :py:func:`~python.Request.cached_setup` call. + +3. the current implementation is inefficient: it performs factory discovery + each time a "db" argument is required. This discovery wrongly happens at + setup-time. + +4. there is no way how you can use funcarg factories, let alone + parametrization, when your tests use the xUnit setup_X approach. + +5. there is no way to specify a per-directory scope for caching. + +In the following sections, API extensions are presented to solve +each of these problems. + + +Direct scoping of funcarg factories +-------------------------------------------------------- + +Instead of calling cached_setup(), you can decorate your factory +to state its scope:: + + @pytest.mark.funcarg(scope="session") + def pytest_funcarg__db(request): + # factory will only be invoked once per session - + db = DataBase() + request.addfinalizer(db.destroy) # destroy when session is finished + return db + +This factory implementation does not need to call ``cached_setup()`` anymore +because it will only be invoked once per session. Moreover, the +``request.addfinalizer()`` registers a finalizer according to the specified +resource scope on which the factory function is operating. With this new +scoping, the still existing ``cached_setup()`` should be much less used +but will remain for compatibility reasons and for the case where you +still want to have your factory get called on a per-item basis. + + +Direct parametrization of funcarg resource factories +---------------------------------------------------------- + +Previously, funcarg factories could not directly cause parametrization. +You needed to specify a ``@parametrize`` or implement a ``pytest_generate_tests`` hook to perform parametrization, i.e. calling a test multiple times +with different value sets. pytest-2.X introduces a decorator for use +on the factory itself:: + + @pytest.mark.funcarg(params=["mysql", "pg"]) + def pytest_funcarg__db(request): + ... + +Here the factory will be invoked twice (with the respective "mysql" +and "pg" values set as ``request.param`` attributes) and and all of +the tests requiring "db" will run twice as well. The "mysql" and +"pg" values will also be used for reporting the test-invocation variants. + +This new way of parametrizing funcarg factories should in many cases +allow to re-use already written factories because effectively +``request.param`` are already the parametrization attribute for test +functions/classes were parametrized via +:py:func:`~_pytest.python.Metafunc.parametrize(indirect=True)` calls. + +Of course it's perfectly fine to combine parametrization and scoping:: + + @pytest.mark.funcarg(scope="session", params=["mysql", "pg"]) + def pytest_funcarg__db(request): + if request.param == "mysql": + db = MySQL() + elif request.param == "pg": + db = PG() + request.addfinalizer(db.destroy) # destroy when session is finished + return db + +This would execute all tests requiring the per-session "db" resource twice, +receiving the values created by the two respective invocations to the +factory function. + +Direct usage of funcargs with funcargs factories +---------------------------------------------------------- + +You can now directly use funcargs in funcarg factories. Example:: + + @pytest.mark.funcarg(scope="session") + def db(request, tmpdir): + # tmpdir is a session-specific tempdir + +Apart from convenience it also solves an issue when your factory +depends on a parametrized funcarg. Previously, a call to +``request.getfuncargvalue()`` would not allow pytest to know +at collection time about the fact that a required resource is +actually parametrized. + +The "pytest_funcarg__" prefix becomes optional +----------------------------------------------------- + +When using the ``@funcarg`` decorator you do not need to use +the ``pytest_funcarg__`` prefix any more:: + + @pytest.mark.funcarg + def db(request): + ... + +The name under which the funcarg resource can be requested is ``db``. +Any ``pytest_funcarg__`` prefix will be stripped. Note that a an +unqualified funcarg-marker implies a scope of "function" meaning +that the funcarg factory will be called for each test function invocation. + + + +support for a new @setup marker +------------------------------------------------------ + +pytest for a long time offered a pytest_configure and a pytest_sessionstart +hook which are often used to setup global resources. This suffers from +several problems: + +1. in distributed testing the master process would setup test resources + that are never needed because it only co-ordinates the test run + activities of the slave processes. + +2. if you only perform a collection (with "--collectonly") + resource-setup will still be executed. + +3. If a pytest_sessionstart is contained in some subdirectories + conftest.py file, it will not be called. This stems from the + fact that this hook is actually used for reporting, in particular + the test-header with platform/custom information. + +4. there is no direct way how you can restrict setup to a directory scope. + +Moreover, it is today not easy to define scoped setup from plugins or +conftest files other than to implement a ``pytest_runtest_setup()`` hook +and caring for scoping/caching yourself. And it's virtually impossible +to do this with parametrization as ``pytest_runtest_setup()`` is called +during test execution and parametrization happens at collection time. + +It follows that pytest_configure/session/runtest_setup are often not +appropriate for implementing common fixture needs. Therefore, +pytest-2.X introduces a new "@pytest.mark.setup" marker, accepting +the same parameters as the @funcargs decorator. The difference is +that the decorated function can accept function arguments itself +Example:: + + # content of conftest.py + import pytest + @pytest.mark.setup(scope="session") + def mysetup(db): + ... + +This ``mysetup`` function is going to be executed when the first +test in the directory tree executes. It is going to be executed once +per-session and it receives the ``db`` funcarg which must be of same +of higher scope; you e. g. generally cannot use a per-module or per-function +scoped resource in a session-scoped setup function. + +You can also use ``@setup`` inside a test module or class:: + + # content of test_module.py + import pytest + + @pytest.mark.setup(scope="module", params=[1,2,3]) + def modes(tmpdir, request): + # ... + +This would execute the ``modes`` function once for each parameter. +In addition to normal funcargs you can also receive the "request" +funcarg which represents a takes on each of the values in the +``params=[1,2,3]`` decorator argument. + +.. note:: + + For each scope, the funcargs will be setup and then the setup functions + will be called. This allows @setup-decorated functions to depend + on already setup funcarg values by accessing ``request.funcargs``. + +Using funcarg resources in xUnit setup methods +------------------------------------------------------------ + +XXX Consider this feature in contrast to the @setup feature - probably +introducing one of them is better and the @setup decorator is more flexible. + +For a long time, pytest has recommended the usage of funcarg +factories as a primary means for managing resources in your test run. +It is a better approach than the jUnit-based approach in many cases, even +more with the new pytest-2.X features, because the funcarg resource factory +provides a single place to determine scoping and parametrization. Your tests +do not need to encode setup/teardown details in every test file's +setup_module/class/method. + +However, the jUnit methods originally introduced by pytest to Python, +remain popoular with nose and unittest-based test suites. Without question, +there are large existing test suites using this paradigm. pytest-2.X +recognizes this fact and now offers direct integration with funcarg resources. Here is a basic example for getting a per-module tmpdir:: + + def setup_module(mod, tmpdir): + mod.tmpdir = tmpdir + +This will trigger pytest's funcarg mechanism to create a value of +"tmpdir" which can then be used throughout the module as a global. + +The new extension to setup_X methods also works in case a resource is +parametrized. For example, let's consider an setup_class example using +our "db" resource:: + + class TestClass: + def setup_class(cls, db): + cls.db = db + # perform some extra things on db + # so that test methods can work with it + +With pytest-2.X the setup* methods will be discovered at collection-time, +allowing to seemlessly integrate this approach with parametrization, +allowing the factory specification to determine all details. The +setup_class itself does not itself need to be aware of the fact that +"db" might be a mysql/PG database. +Note that if the specified resource is provided only as a per-testfunction +resource, collection would early on report a ScopingMismatch error. + + +the "directory" caching scope +-------------------------------------------- + +All API accepting a scope (:py:func:`cached_setup()` and +the new funcarg/setup decorators) now also accept a "directory" +specification. This allows to restrict/cache resource values on a +per-directory level. + +funcarg and setup discovery now happens at collection time +--------------------------------------------------------------------- + +pytest-2.X takes care to discover funcarg factories and setup_X methods +at collection time. This is more efficient especially for large test suites. +Moreover, a call to "py.test --collectonly" should be able to show +a lot of setup-information and thus presents a nice method to get an +overview of resource management in your project. + +Implementation level +=================================================================== + +To implement the above new features, pytest-2.X grows some new hooks and +methods. At the time of writing V2 and without actually implementing +it, it is not clear how much of this new internal API will also be +exposed and advertised e. g. for plugin writers. + +The main effort, however, will lie in revising what is done at +collection and what at test setup time. All funcarg factories and +xUnit setup methods need to be discovered at collection time +for the above mechanism to work. Additionally all test function +signatures need to be parsed in order to know which resources are +used. On the plus side, all previously collected fixtures and +test functions only need to be called, no discovery is neccessary +is required anymore. + +the "request" object incorporates scope-specific behaviour +------------------------------------------------------------------ + +funcarg factories receive a request object to help with implementing +finalization and inspection of the requesting-context. If there is +no scoping is in effect, nothing much will change of the API behaviour. +However, with scoping the request object represents the according context. +Let's consider this example:: + + @pytest.mark.factory_scope("class") + def pytest_funcarg__db(request): + # ... + request.getfuncargvalue(...) + # + request.addfinalizer(db) + +Due to the class-scope, the request object will: + +- provide a ``None`` value for the ``request.function`` attribute. +- default to per-class finalization with the addfinalizer() call. +- raise a ScopeMismatchError if a more broadly scoped factory + wants to use a more tighly scoped factory (e.g. per-function) + +In fact, the request object is likely going to provide a "node" +attribute, denoting the current collection node on which it internally +operates. (Prior to pytest-2.3 there already was an internal +_pyfuncitem). + +As these are rather intuitive extensions, not much friction is expected +for test/plugin writers using the new scoping and parametrization mechanism. +It's, however, a serious internal effort to reorganize the pytest +implementation. + + +node.register_factory/getresource() methods +-------------------------------------------------------- + +In order to implement factory- and setup-method discovery at +collection time, a new node API will be introduced to allow +for factory registration and a getresource() call to obtain +created values. The exact details of this API remain subject +to experimentation. The basic idea is to introduce two new +methods to the Session class which is already available on all nodes +through the ``node.session`` attribute:: + + class Session: + def register_resource_factory(self, name, factory_or_list, scope): + """ register a resource factory for the given name. + + :param name: Name of the resource. + :factory_or_list: a function or a list of functions creating + one or multiple resource values. + :param scope: a node instance. The factory will be only visisble + available for all descendant nodes. + specify the "session" instance for global availability + """ + + def getresource(self, name, node): + """ get a named resource for the give node. + + This method looks up a matching funcarg resource factory + and calls it. + """ + +.. todo:: + + XXX While this new API (or some variant of it) may suffices to implement + all of the described new usage-level features, it remains unclear how the + existing "@parametrize" or "metafunc.parametrize()" calls will map to it. + These parametrize-approaches tie resource parametrization to the + function/funcargs-usage rather than to the factories. + + + +ISSUES +-------------------------- + +decorating a parametrized funcarg factory: + + @pytest.mark.funcarg(scope="session", params=["mysql", "pg"]) + def db(request): + ... + class TestClass: + @pytest.mark.funcarg(scope="function") + def something(self, request): + session_db = request.getfuncargvalue("db") + ... + +Here the function-scoped "something" factory uses the session-scoped +"db" factory to perform some additional steps. The dependency, however, +is only visible at setup-time, when the factory actually gets called. + +In order to allow parametrization at collection-time I see two ways: + +- allow specifying dependencies in the funcarg-marker +- allow funcargs for factories as well + https://bitbucket.org/hpk42/pytest/changeset/19affc442801/ changeset: 19affc442801 user: hpk42 date: 2012-07-18 19:49:14 summary: re-introduce the old 2.2.4 FuncargRequest implementation as it is a better base for implementing the new funcarg/setup api. Also Un-optimize funcargnames discovery for now. affected #: 8 files diff -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 -r 19affc442801ad53c603e3d5ac912539dc1a6533 _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -187,7 +187,7 @@ captured output available via ``capsys.readouterr()`` method calls which return a ``(out, err)`` tuple. """ - if "capfd" in request.funcargs: + if "capfd" in request._funcargs: raise request.LookupError(error_capsysfderror) return CaptureFuncarg(py.io.StdCapture) @@ -196,7 +196,7 @@ captured output available via ``capsys.readouterr()`` method calls which return a ``(out, err)`` tuple. """ - if "capsys" in request.funcargs: + if "capsys" in request._funcargs: raise request.LookupError(error_capsysfderror) if not hasattr(os, 'dup'): pytest.skip("capfd funcarg needs os.dup") diff -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 -r 19affc442801ad53c603e3d5ac912539dc1a6533 _pytest/impl --- /dev/null +++ b/_pytest/impl @@ -0,0 +1,97 @@ + +Implementation plan for resources +------------------------------------------ + +1. Revert FuncargRequest to the old form, unmerge item/request +2. make setup functions be discovered at collection time +3. make funcarg factories be discovered at collection time +4. Introduce funcarg marker +5. Introduce funcarg scope parameter +6. Introduce funcarg parametrize parameter +7. (Introduce a pytest_fixture_protocol/setup_funcargs hook) + +methods and data structures +-------------------------------- + +A FuncarcDB holds all information about funcarg definitions, +parametrization and the places where funcargs are required. It can +answer the following questions: + +* given a node and a funcargname, return a paramlist so that collection + can perform parametrization (parametrized nodes?) +* given a node (possibly containing a param), perform a funcargrequest + and return the value +* if funcargname is an empty string, it matches general setup. + +pytest could perform 2-pass collection: +- first perform normal collection (no parametrization at all!), populate + FuncargDB +- walk through the node tree and ask FuncargDB for each node for + required funcargs and their parameters - clone subtrees (deepcopy) and + substitute the un-parametrized node with parametrized ones + +as a simple example, let's consider a tree where a test function requires +a "abc" funcarg and its factory defines it as parametrized and scoped +for Modules. When the 2nd collection pass asks FuncargDB to return +params for the test module, it will know that the test functions in it +requires "abc" and that is it parametrized and defined for module scope. +Therefore parametrization of the module node is performed, substituting +the node with multiple module nodes ("test_module.py[1]", ...). +When test_module.py[1] is setup() it will call all its (parametrized) +factories and populate a funcargs dictionary, mapping funcargnames to values. +When a test function below test_module.py[1] is executed, it looks up +its required arguments from the thus populated funcargs dictionary. + +Let's add to this example a second funcarg "def" that has a per-function parametrization. When the 2nd collection pass asks FuncargDB to return +params for the test function, it will know that the test functions in it +requires "def" and that is it parametrized and defined for function scope. +Therefore parametrization of the function node is performed, substituting +the node with multiple function nodes ("test_function[1]", ...). + +When test_function[1] is setup() it will call all its (parametrized) +factories and populate a funcargs dictionary. The "def" will only appear +in the funcargs dict seen by test_function[1]. When test_function[1] +executes, it will use its funcargs. + + + + +where + +* ``nodeidbase`` is a basestring; for all nodeids matching + startswith(nodeidbase) it defines a (scopecls, factorylist) tuple +* ``scopecls`` is a node class for the which the factorylist s defined +* ``param`` is a parametrizing parameter for the factorylist +* ``factorylist`` is a list of factories which will be used to perform + a funcarg request +* the whole list is sorted by length of nodeidbase (longest first) + +conftest loading: + each funcarg-factory will populate FuncargDefs which keeps references + to all definitions the funcarg2 marked function or pytest_funcarg__ + + +scope can be a string or a nodenames-tuple. + + scopestring -> list of (funcargname, factorylist) + + nodenames -> (funcargname, list of factories) + +It needs to be a list because factories can decorate + +For any given node and a required funcarg it is thus +easy to lookup a list of matching factories. + +When a test item is collected, it grows a dictionary +(funcargname2factorycalllist). A factory lookup is performed +for each required funcarg. The resulting factory call is stored +with the item. If a function is parametrized multiple items are +created with respective factory calls. Else if a factory is parametrized +multiple items and calls to the factory function are created as well. + +At setup time, an item populates a funcargs mapping, mapping names +to values. If a value is funcarg factories are queried for a given item +test functions and setup functions are put in a class +which looks up required funcarg factories. + + diff -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 -r 19affc442801ad53c603e3d5ac912539dc1a6533 _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -177,11 +177,11 @@ #: fspath sensitive hook proxy used to call pytest hooks self.ihook = self.session.gethookproxy(self.fspath) - self.extrainit() + #self.extrainit() - def extrainit(self): - """"extra initialization after Node is initialized. Implemented - by some subclasses. """ + #def extrainit(self): + # """"extra initialization after Node is initialized. Implemented + # by some subclasses. """ Module = compatproperty("Module") Class = compatproperty("Class") diff -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 -r 19affc442801ad53c603e3d5ac912539dc1a6533 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -33,126 +33,6 @@ name.lower(),) return property(get, None, None, doc) -class Request(object): - _argprefix = "pytest_funcarg__" - - class LookupError(LookupError): - """ error while performing funcarg factory lookup. """ - - def extrainit(self): - self._name2factory = {} - self._currentarg = None - self.funcargs = None # later set to a dict from fillfuncargs() or - # from getfuncargvalue(). Setting it to - # None prevents users from performing - # "name in item.funcargs" checks too early. - - @property - def _plugins(self): - extra = [obj for obj in (self.module, self.instance) if obj] - return self.getplugins() + extra - - def _getscopeitem(self, scope): - if scope == "function": - return self - elif scope == "session": - return None - elif scope == "class": - x = self.getparent(pytest.Class) - if x is not None: - return x - scope = "module" - if scope == "module": - return self.getparent(pytest.Module) - raise ValueError("unknown scope %r" %(scope,)) - - def getfuncargvalue(self, argname): - """ Retrieve a named function argument value. - - This function looks up a matching factory and invokes - it to obtain the return value. The factory receives - can itself perform recursive calls to this method, - either for using multiple other funcarg values under the hood - or to decorate values from other factories matching the same name. - """ - try: - return self.funcargs[argname] - except KeyError: - pass - except TypeError: - self.funcargs = getattr(self, "_funcargs", {}) - if argname not in self._name2factory: - self._name2factory[argname] = self.config.pluginmanager.listattr( - plugins=self._plugins, - attrname=self._argprefix + str(argname) - ) - #else: we are called recursively - if not self._name2factory[argname]: - self._raiselookupfailed(argname) - funcargfactory = self._name2factory[argname].pop() - oldarg = self._currentarg - mp = monkeypatch() - mp.setattr(self, '_currentarg', argname) - try: - param = self.callspec.getparam(argname) - except (AttributeError, ValueError): - pass - else: - mp.setattr(self, 'param', param, raising=False) - try: - self.funcargs[argname] = res = funcargfactory(self) - finally: - mp.undo() - return res - - def addfinalizer(self, finalizer): - """ add a no-args finalizer function to be called when the underlying - node is torn down.""" - self.session._setupstate.addfinalizer(finalizer, self) - - def cached_setup(self, setup, teardown=None, - scope="module", extrakey=None): - """ Return a cached testing resource created by ``setup`` & - detroyed by a respective ``teardown(resource)`` call. - - :arg teardown: function receiving a previously setup resource. - :arg setup: a no-argument function creating a resource. - :arg scope: a string value out of ``function``, ``class``, ``module`` - or ``session`` indicating the caching lifecycle of the resource. - :arg extrakey: added to internal caching key. - """ - if not hasattr(self.config, '_setupcache'): - self.config._setupcache = {} # XXX weakref? - colitem = self._getscopeitem(scope) - cachekey = (self._currentarg, colitem, extrakey) - cache = self.config._setupcache - try: - val = cache[cachekey] - except KeyError: - val = setup() - cache[cachekey] = val - if teardown is not None: - def finalizer(): - del cache[cachekey] - teardown(val) - self.session._setupstate.addfinalizer(finalizer, colitem) - return val - - def _raiselookupfailed(self, argname): - available = [] - for plugin in self._plugins: - for name in vars(plugin): - if name.startswith(self._argprefix): - name = name[len(self._argprefix):] - if name not in available: - available.append(name) - fspath, lineno, msg = self.reportinfo() - msg = "LookupError: no factory found for function argument %r" % (argname,) - msg += "\n available funcargs: %s" %(", ".join(available),) - msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise self.LookupError(msg) - - def pytest_addoption(parser): group = parser.getgroup("general") @@ -222,6 +102,15 @@ funcargs[name] = pyfuncitem.funcargs[name] testfunction(**funcargs) +def pytest_pyfunc_call(__multicall__, pyfuncitem): + if not __multicall__.execute(): + testfunction = pyfuncitem.obj + if pyfuncitem._isyieldedfunction(): + testfunction(*pyfuncitem._args) + else: + funcargs = pyfuncitem.funcargs + testfunction(**funcargs) + def pytest_collect_file(path, parent): ext = path.ext pb = path.purebasename @@ -267,7 +156,7 @@ cls = pyobj_property("Class") instance = pyobj_property("Instance") -class PyobjMixin(Request, PyobjContext): +class PyobjMixin(PyobjContext): def obj(): def fget(self): try: @@ -390,14 +279,12 @@ gentesthook.pcall(plugins, metafunc=metafunc) Function = self._getcustomclass("Function") if not metafunc._calls: - return Function(name, parent=self, - funcargnames=metafunc.funcargnames) + return Function(name, parent=self) l = [] for callspec in metafunc._calls: subname = "%s[%s]" %(name, callspec.id) function = Function(name=subname, parent=self, callspec=callspec, callobj=funcobj, - funcargnames=metafunc.funcargnames, keywords={callspec.id:True}) l.append(function) return l @@ -494,6 +381,7 @@ class FunctionMixin(PyobjMixin): """ mixin for the code common to Function and Generator. """ + def setup(self): """ perform setup for this test function. """ if hasattr(self, '_preservedparent'): @@ -535,7 +423,7 @@ excinfo.traceback = ntraceback.filter() def _repr_failure_py(self, excinfo, style="long"): - if excinfo.errisinstance(Request.LookupError): + if excinfo.errisinstance(FuncargRequest.LookupError): fspath, lineno, msg = self.reportinfo() lines, _ = inspect.getsourcelines(self.obj) for i, line in enumerate(lines): @@ -626,10 +514,21 @@ return argnames[startindex:-numdefaults] return argnames[startindex:] -def fillfuncargs(node): +def fillfuncargs(function): """ fill missing funcargs. """ - if not isinstance(node, Function): - node = OldFuncargRequest(pyfuncitem=node) + #if not getattr(function, "_args", None) is not None: + # request = FuncargRequest(pyfuncitem=function) + # request._fillfuncargs() + if getattr(function, "_args", None) is None: + try: + request = function._request + except AttributeError: + request = FuncargRequest(function) + request._fillfuncargs() + +def XXXfillfuncargs(node): + """ fill missing funcargs. """ + node = FuncargRequest(node) if node.funcargs is None: node.funcargs = getattr(node, "_funcargs", {}) if not isinstance(node, Function) or not node._isyieldedfunction(): @@ -815,8 +714,8 @@ for plugin in plugins: available = [] for name, factory in vars(plugin).items(): - if name.startswith(Request._argprefix): - name = name[len(Request._argprefix):] + if name.startswith(FuncargRequest._argprefix): + name = name[len(FuncargRequest._argprefix):] if name not in available: available.append([name, factory]) if available: @@ -931,11 +830,9 @@ """ _genid = None def __init__(self, name, parent=None, args=None, config=None, - callspec=None, callobj=_dummy, keywords=None, - session=None, funcargnames=()): + callspec=None, callobj=_dummy, keywords=None, session=None): super(Function, self).__init__(name, parent, config=config, session=session) - self.funcargnames = funcargnames self._args = args if self._isyieldedfunction(): assert not callspec, ( @@ -943,12 +840,17 @@ else: if callspec is not None: self.callspec = callspec - self._funcargs = callspec.funcargs or {} + self.funcargs = callspec.funcargs or {} self._genid = callspec.id if hasattr(callspec, "param"): self.param = callspec.param + else: + self.funcargs = {} + self._request = req = FuncargRequest(self) if callobj is not _dummy: - self._obj = callobj + self.obj = callobj + startindex = int(self.cls is not None) + self.funcargnames = getfuncargnames(self.obj, startindex=startindex) self.keywords.update(py.builtin._getfuncdict(self.obj) or {}) if keywords: @@ -1002,49 +904,196 @@ return hash((self.parent, self.name)) -def itemapi_property(name, set=False): - prop = getattr(Function, name, None) - doc = getattr(prop, "__doc__", None) - def get(self): - return getattr(self._pyfuncitem, name) - if set: - def set(self, value): - setattr(self._pyfuncitem, name, value) - else: - set = None - return property(get, set, None, doc) +class FuncargRequest: + """ A request for function arguments from a test function. + Note that there is an optional ``param`` attribute in case + there was an invocation to metafunc.addcall(param=...). + If no such call was done in a ``pytest_generate_tests`` + hook, the attribute will not be present. + """ + _argprefix = "pytest_funcarg__" + _argname = None -class OldFuncargRequest(Request, PyobjContext): - """ (deprecated) helper interactions with a test function invocation. + class LookupError(LookupError): + """ error on performing funcarg request. """ - Note that there is an optional ``param`` attribute in case - there was an invocation to metafunc.addcall(param=...). - If no such call was done in a ``pytest_generate_tests`` - hook, the attribute will not be present. - """ def __init__(self, pyfuncitem): self._pyfuncitem = pyfuncitem - Request.extrainit(self) - self.funcargs = pyfuncitem.funcargs - self.getplugins = self._pyfuncitem.getplugins - self.reportinfo = self._pyfuncitem.reportinfo - self.getparent = self._pyfuncitem.getparent + if hasattr(pyfuncitem, '_requestparam'): + self.param = pyfuncitem._requestparam + self.getparent = pyfuncitem.getparent + self._funcargs = self._pyfuncitem.funcargs.copy() + self._name2factory = {} + self._currentarg = None + + @cached_property + def _plugins(self): + extra = [obj for obj in (self.module, self.instance) if obj] + return self._pyfuncitem.getplugins() + extra + + @property + def function(self): + """ function object of the test invocation. """ + return self._pyfuncitem.obj + + @property + def keywords(self): + """ keywords of the test function item. + + .. versionadded:: 2.0 + """ + return self._pyfuncitem.keywords + + @property + def module(self): + """ module where the test function was collected. """ + return self._pyfuncitem.getparent(pytest.Module).obj + + @property + def cls(self): + """ class (can be None) where the test function was collected. """ + clscol = self._pyfuncitem.getparent(pytest.Class) + if clscol: + return clscol.obj + @property + def instance(self): + """ instance (can be None) on which test function was collected. """ + return py.builtin._getimself(self.function) + + @property + def config(self): + """ the pytest config object associated with this request. """ + return self._pyfuncitem.config + + @property + def fspath(self): + """ the file system path of the test module which collected this test. """ + return self._pyfuncitem.fspath + + def _fillfuncargs(self): + argnames = getfuncargnames(self.function) + if argnames: + assert not getattr(self._pyfuncitem, '_args', None), ( + "yielded functions cannot have funcargs") + for argname in argnames: + if argname not in self._pyfuncitem.funcargs: + self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) + + + def applymarker(self, marker): + """ Apply a marker to a single test function invocation. + This method is useful if you don't want to have a keyword/marker + on all function invocations. + + :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object + created by a call to ``py.test.mark.NAME(...)``. + """ + if not isinstance(marker, py.test.mark.XYZ.__class__): + raise ValueError("%r is not a py.test.mark.* object") + self._pyfuncitem.keywords[marker.markname] = marker + + def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): + """ Return a testing resource managed by ``setup`` & + ``teardown`` calls. ``scope`` and ``extrakey`` determine when the + ``teardown`` function will be called so that subsequent calls to + ``setup`` would recreate the resource. + + :arg teardown: function receiving a previously setup resource. + :arg setup: a no-argument function creating a resource. + :arg scope: a string value out of ``function``, ``class``, ``module`` + or ``session`` indicating the caching lifecycle of the resource. + :arg extrakey: added to internal caching key of (funcargname, scope). + """ + if not hasattr(self.config, '_setupcache'): + self.config._setupcache = {} # XXX weakref? + cachekey = (self._currentarg, self._getscopeitem(scope), extrakey) + cache = self.config._setupcache try: - self.param = self._pyfuncitem.param - except AttributeError: + val = cache[cachekey] + except KeyError: + val = setup() + cache[cachekey] = val + if teardown is not None: + def finalizer(): + del cache[cachekey] + teardown(val) + self._addfinalizer(finalizer, scope=scope) + return val + + def getfuncargvalue(self, argname): + """ Retrieve a function argument by name for this test + function invocation. This allows one function argument factory + to call another function argument factory. If there are two + funcarg factories for the same test function argument the first + factory may use ``getfuncargvalue`` to call the second one and + do something additional with the resource. + """ + try: + return self._funcargs[argname] + except KeyError: pass + if argname not in self._name2factory: + self._name2factory[argname] = self.config.pluginmanager.listattr( + plugins=self._plugins, + attrname=self._argprefix + str(argname) + ) + #else: we are called recursively + if not self._name2factory[argname]: + self._raiselookupfailed(argname) + funcargfactory = self._name2factory[argname].pop() + oldarg = self._currentarg + mp = monkeypatch() + mp.setattr(self, '_currentarg', argname) + try: + param = self._pyfuncitem.callspec.getparam(argname) + except (AttributeError, ValueError): + pass + else: + mp.setattr(self, 'param', param, raising=False) + try: + self._funcargs[argname] = res = funcargfactory(request=self) + finally: + mp.undo() + return res + + def _getscopeitem(self, scope): + if scope == "function": + return self._pyfuncitem + elif scope == "session": + return None + elif scope == "class": + x = self._pyfuncitem.getparent(pytest.Class) + if x is not None: + return x + scope = "module" + if scope == "module": + return self._pyfuncitem.getparent(pytest.Module) + raise ValueError("unknown finalization scope %r" %(scope,)) + + def addfinalizer(self, finalizer): + """add finalizer function to be called after test function + finished execution. """ + self._addfinalizer(finalizer, scope="function") + + def _addfinalizer(self, finalizer, scope): + colitem = self._getscopeitem(scope) + self._pyfuncitem.session._setupstate.addfinalizer( + finalizer=finalizer, colitem=colitem) def __repr__(self): - return "" % (self._pyfuncitem.name) + return "" %(self._pyfuncitem) - _getscopeitem = itemapi_property("_getscopeitem") - funcargs = itemapi_property("funcargs", set=True) - keywords = itemapi_property("keywords") - config = itemapi_property("config") - session = itemapi_property("session") - fspath = itemapi_property("fspath") - applymarker = itemapi_property("applymarker") - @property - def function(self): - return self._pyfuncitem.obj + def _raiselookupfailed(self, argname): + available = [] + for plugin in self._plugins: + for name in vars(plugin): + if name.startswith(self._argprefix): + name = name[len(self._argprefix):] + if name not in available: + available.append(name) + fspath, lineno, msg = self._pyfuncitem.reportinfo() + msg = "LookupError: no factory found for function argument %r" % (argname,) + msg += "\n available funcargs: %s" %(", ".join(available),) + msg += "\n use 'py.test --funcargs [testpath]' for help on them." + raise self.LookupError(msg) diff -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 -r 19affc442801ad53c603e3d5ac912539dc1a6533 _pytest/tmpdir.py --- a/_pytest/tmpdir.py +++ b/_pytest/tmpdir.py @@ -54,15 +54,15 @@ mp.setattr(config, '_tmpdirhandler', t, raising=False) mp.setattr(pytest, 'ensuretemp', t.ensuretemp, raising=False) -def pytest_funcarg__tmpdir(item): +def pytest_funcarg__tmpdir(request): """return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. The returned object is a `py.path.local`_ path object. """ - name = item.name + name = request._pyfuncitem.name name = py.std.re.sub("[\W]", "_", name) - x = item.config._tmpdirhandler.mktemp(name, numbered=True) + x = request.config._tmpdirhandler.mktemp(name, numbered=True) return x diff -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 -r 19affc442801ad53c603e3d5ac912539dc1a6533 doc/en/funcargs.txt --- a/doc/en/funcargs.txt +++ b/doc/en/funcargs.txt @@ -110,7 +110,7 @@ The request object passed to factories ----------------------------------------- -Each funcarg factory receives a :py:class:`~_pytest.python.Request` object which +Each funcarg factory receives a :py:class:`~_pytest.python.FuncargRequest` object which provides methods to manage caching and finalization in the context of the test invocation as well as several attributes of the the underlying test item. In fact, as of version pytest-2.3, the request API is implemented on all Item objects and therefore the request object has general :py:class:`Node attributes and methods <_pytest.main.Node>` attributes. This is a backward compatible diff -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 -r 19affc442801ad53c603e3d5ac912539dc1a6533 doc/en/plugins.txt --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -331,7 +331,7 @@ Reference of objects involved in hooks =========================================================== -.. autoclass:: _pytest.python.Request() +.. autoclass:: _pytest.python.FuncargRequest() :members: .. autoclass:: _pytest.config.Config() diff -r ffe816408bb9e8bd4dc31b5d181b8e6984f7a5c0 -r 19affc442801ad53c603e3d5ac912539dc1a6533 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -277,14 +277,18 @@ def test_function_equality(self, testdir, tmpdir): config = testdir.parseconfigure() session = testdir.Session(config) + def func1(): + pass + def func2(): + pass f1 = pytest.Function(name="name", config=config, - args=(1,), callobj=isinstance, session=session) + args=(1,), callobj=func1, session=session) f2 = pytest.Function(name="name",config=config, - args=(1,), callobj=py.builtin.callable, session=session) + args=(1,), callobj=func2, session=session) assert not f1 == f2 assert f1 != f2 f3 = pytest.Function(name="name", config=config, - args=(1,2), callobj=py.builtin.callable, session=session) + args=(1,2), callobj=func2, session=session) assert not f3 == f2 assert f3 != f2 @@ -292,7 +296,7 @@ assert f3 != f1 f1_b = pytest.Function(name="name", config=config, - args=(1,), callobj=isinstance, session=session) + args=(1,), callobj=func1, session=session) assert f1 == f1_b assert not f1 != f1_b @@ -307,10 +311,12 @@ funcargs = {} id = "world" session = testdir.Session(config) + def func(): + pass f5 = pytest.Function(name="name", config=config, - callspec=callspec1, callobj=isinstance, session=session) + callspec=callspec1, callobj=func, session=session) f5b = pytest.Function(name="name", config=config, - callspec=callspec2, callobj=isinstance, session=session) + callspec=callspec2, callobj=func, session=session) assert f5 != f5b assert not (f5 == f5b) @@ -549,7 +555,7 @@ return 42 """) item = testdir.getitem("def test_func(some): pass") - exc = pytest.raises(funcargs.OldFuncargRequest.LookupError, + exc = pytest.raises(funcargs.FuncargRequest.LookupError, "funcargs.fillfuncargs(item)") s = str(exc.value) assert s.find("xyzsomething") != -1 @@ -624,7 +630,7 @@ def pytest_funcarg__something(request): pass def test_func(something): pass """) - req = funcargs.OldFuncargRequest(item) + req = funcargs.FuncargRequest(item) assert req.function == item.obj assert req.keywords is item.keywords assert hasattr(req.module, 'test_func') @@ -639,7 +645,7 @@ def test_func(self, something): pass """) - req = funcargs.OldFuncargRequest(item) + req = funcargs.FuncargRequest(item) assert req.cls.__name__ == "TestB" assert req.instance.__class__ == req.cls @@ -653,7 +659,7 @@ """) item1, = testdir.genitems([modcol]) assert item1.name == "test_method" - name2factory = funcargs.OldFuncargRequest(item1)._name2factory + name2factory = funcargs.FuncargRequest(item1)._name2factory assert len(name2factory) == 1 assert name2factory[0].__name__ == "pytest_funcarg__something" @@ -668,7 +674,7 @@ def test_func(something): assert something == 2 """) - req = funcargs.OldFuncargRequest(item) + req = funcargs.FuncargRequest(item) val = req.getfuncargvalue("something") assert val == 2 @@ -680,7 +686,7 @@ return l.pop() def test_func(something): pass """) - req = funcargs.OldFuncargRequest(item) + req = funcargs.FuncargRequest(item) pytest.raises(req.LookupError, req.getfuncargvalue, "notexists") val = req.getfuncargvalue("something") assert val == 1 @@ -691,7 +697,8 @@ val2 = req.getfuncargvalue("other") # see about caching assert val2 == 2 pytest._fillfuncargs(item) - assert item.funcargs == {'something': 1, "other": 2} + assert item.funcargs == {'something': 1} + #assert item.funcargs == {'something': 1, "other": 2} def test_request_addfinalizer(self, testdir): item = testdir.getitem(""" @@ -728,7 +735,7 @@ def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") item, = testdir.genitems([modcol]) - req = funcargs.OldFuncargRequest(item) + req = funcargs.FuncargRequest(item) assert req.fspath == modcol.fspath def test_applymarker(testdir): @@ -739,7 +746,7 @@ def test_func2(self, something): pass """) - req1 = funcargs.OldFuncargRequest(item1) + req1 = funcargs.FuncargRequest(item1) assert 'xfail' not in item1.keywords req1.applymarker(pytest.mark.xfail) assert 'xfail' in item1.keywords @@ -757,7 +764,7 @@ def test_func2(self, something): pass """) - req1 = funcargs.OldFuncargRequest(item1) + req1 = funcargs.FuncargRequest(item1) l = ["hello"] def setup(): return l.pop() @@ -766,7 +773,7 @@ assert ret1 == "hello" ret1b = req1.cached_setup(setup) assert ret1 == ret1b - req2 = funcargs.OldFuncargRequest(item2) + req2 = funcargs.FuncargRequest(item2) ret2 = req2.cached_setup(setup) assert ret2 == ret1 @@ -782,7 +789,7 @@ def test_func2b(self, something): pass """) - req1 = funcargs.OldFuncargRequest(item2) + req1 = funcargs.FuncargRequest(item2) l = ["hello2", "hello"] def setup(): return l.pop() @@ -791,22 +798,22 @@ # automatically turn "class" to "module" scope ret1 = req1.cached_setup(setup, scope="class") assert ret1 == "hello" - req2 = funcargs.OldFuncargRequest(item2) + req2 = funcargs.FuncargRequest(item2) ret2 = req2.cached_setup(setup, scope="class") assert ret2 == "hello" - req3 = funcargs.OldFuncargRequest(item3) + req3 = funcargs.FuncargRequest(item3) ret3a = req3.cached_setup(setup, scope="class") ret3b = req3.cached_setup(setup, scope="class") assert ret3a == "hello2" assert ret3b == "hello2" - req4 = funcargs.OldFuncargRequest(item4) + req4 = funcargs.FuncargRequest(item4) ret4 = req4.cached_setup(setup, scope="class") assert ret4 == ret3a def test_request_cachedsetup_extrakey(self, testdir): item1 = testdir.getitem("def test_func(): pass") - req1 = funcargs.OldFuncargRequest(item1) + req1 = funcargs.FuncargRequest(item1) l = ["hello", "world"] def setup(): return l.pop() @@ -821,7 +828,7 @@ def test_request_cachedsetup_cache_deletion(self, testdir): item1 = testdir.getitem("def test_func(): pass") - req1 = funcargs.OldFuncargRequest(item1) + req1 = funcargs.FuncargRequest(item1) l = [] def setup(): l.append("setup") @@ -1093,9 +1100,9 @@ def pytest_generate_tests(metafunc): metafunc.addcall(param=metafunc) - def pytest_funcarg__metafunc(item): - assert item._genid == "0" - return item.param + def pytest_funcarg__metafunc(request): + assert request._pyfuncitem._genid == "0" + return request.param def test_function(metafunc, pytestconfig): assert metafunc.config == pytestconfig @@ -1591,6 +1598,7 @@ ]) class TestRequestAPI: + @pytest.mark.xfail(reason="reverted refactoring") def test_addfinalizer_cachedsetup_getfuncargvalue(self, testdir): testdir.makeconftest(""" l = [] @@ -1615,10 +1623,11 @@ "*2 passed*", ]) + @pytest.mark.xfail(reason="consider item's funcarg access and error conditions") def test_runtest_setup_sees_filled_funcargs(self, testdir): testdir.makeconftest(""" def pytest_runtest_setup(item): - assert item.funcargs is None + assert not hasattr(item, "_request") """) testdir.makepyfile(""" def pytest_funcarg__a(request): https://bitbucket.org/hpk42/pytest/changeset/ab0f23204d9f/ changeset: ab0f23204d9f user: hpk42 date: 2012-07-19 09:20:14 summary: move funcarg factory to a new FuncargManager object at session level affected #: 7 files diff -r 19affc442801ad53c603e3d5ac912539dc1a6533 -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e _pytest/capture.py --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -188,7 +188,7 @@ which return a ``(out, err)`` tuple. """ if "capfd" in request._funcargs: - raise request.LookupError(error_capsysfderror) + raise request.raiseerror(error_capsysfderror) return CaptureFuncarg(py.io.StdCapture) def pytest_funcarg__capfd(request): @@ -197,7 +197,7 @@ which return a ``(out, err)`` tuple. """ if "capsys" in request._funcargs: - raise request.LookupError(error_capsysfderror) + request.raiseerror(error_capsysfderror) if not hasattr(os, 'dup'): pytest.skip("capfd funcarg needs os.dup") return CaptureFuncarg(py.io.StdCaptureFD) diff -r 19affc442801ad53c603e3d5ac912539dc1a6533 -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e _pytest/impl --- a/_pytest/impl +++ b/_pytest/impl @@ -3,84 +3,32 @@ ------------------------------------------ 1. Revert FuncargRequest to the old form, unmerge item/request -2. make setup functions be discovered at collection time -3. make funcarg factories be discovered at collection time -4. Introduce funcarg marker -5. Introduce funcarg scope parameter -6. Introduce funcarg parametrize parameter + (done) +2. make funcarg factories be discovered at collection time +3. Introduce funcarg marker +4. Introduce funcarg scope parameter +5. Introduce funcarg parametrize parameter +6. make setup functions be discovered at collection time 7. (Introduce a pytest_fixture_protocol/setup_funcargs hook) methods and data structures -------------------------------- -A FuncarcDB holds all information about funcarg definitions, -parametrization and the places where funcargs are required. It can -answer the following questions: - -* given a node and a funcargname, return a paramlist so that collection - can perform parametrization (parametrized nodes?) -* given a node (possibly containing a param), perform a funcargrequest - and return the value -* if funcargname is an empty string, it matches general setup. - -pytest could perform 2-pass collection: -- first perform normal collection (no parametrization at all!), populate - FuncargDB -- walk through the node tree and ask FuncargDB for each node for - required funcargs and their parameters - clone subtrees (deepcopy) and - substitute the un-parametrized node with parametrized ones +A FuncarcManager holds all information about funcarg definitions +including parametrization and scope definitions. It implements +a pytest_generate_tests hook which performs parametrization as appropriate. as a simple example, let's consider a tree where a test function requires a "abc" funcarg and its factory defines it as parametrized and scoped -for Modules. When the 2nd collection pass asks FuncargDB to return -params for the test module, it will know that the test functions in it -requires "abc" and that is it parametrized and defined for module scope. -Therefore parametrization of the module node is performed, substituting -the node with multiple module nodes ("test_module.py[1]", ...). -When test_module.py[1] is setup() it will call all its (parametrized) -factories and populate a funcargs dictionary, mapping funcargnames to values. -When a test function below test_module.py[1] is executed, it looks up -its required arguments from the thus populated funcargs dictionary. - -Let's add to this example a second funcarg "def" that has a per-function parametrization. When the 2nd collection pass asks FuncargDB to return -params for the test function, it will know that the test functions in it -requires "def" and that is it parametrized and defined for function scope. -Therefore parametrization of the function node is performed, substituting -the node with multiple function nodes ("test_function[1]", ...). - -When test_function[1] is setup() it will call all its (parametrized) -factories and populate a funcargs dictionary. The "def" will only appear -in the funcargs dict seen by test_function[1]. When test_function[1] -executes, it will use its funcargs. - - - - -where - -* ``nodeidbase`` is a basestring; for all nodeids matching - startswith(nodeidbase) it defines a (scopecls, factorylist) tuple -* ``scopecls`` is a node class for the which the factorylist s defined -* ``param`` is a parametrizing parameter for the factorylist -* ``factorylist`` is a list of factories which will be used to perform - a funcarg request -* the whole list is sorted by length of nodeidbase (longest first) +for Modules. When collections hits the function item, it creates +the metafunc object, and calls funcargdb.pytest_generate_tests(metafunc) +which looks up available funcarg factories and their scope and parametrization. +This information is equivalent to what can be provided today directly +at the function site and it should thus be relatively straight forward +to implement the additional way of defining parametrization/scoping. conftest loading: - each funcarg-factory will populate FuncargDefs which keeps references - to all definitions the funcarg2 marked function or pytest_funcarg__ - - -scope can be a string or a nodenames-tuple. - - scopestring -> list of (funcargname, factorylist) - - nodenames -> (funcargname, list of factories) - -It needs to be a list because factories can decorate - -For any given node and a required funcarg it is thus -easy to lookup a list of matching factories. + each funcarg-factory will populate the session.funcargmanager When a test item is collected, it grows a dictionary (funcargname2factorycalllist). A factory lookup is performed diff -r 19affc442801ad53c603e3d5ac912539dc1a6533 -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -2,8 +2,12 @@ import py import pytest, _pytest +import inspect import os, sys, imp +from _pytest.monkeypatch import monkeypatch +from py._code.code import TerminalRepr + tracebackcutdir = py.path.local(_pytest.__file__).dirpath() # exitcodes for the command line @@ -279,6 +283,15 @@ pass def _repr_failure_py(self, excinfo, style=None): + LE = self.session.funcargmanager.FuncargLookupError + if excinfo.errisinstance(LE): + request = excinfo.value.request + fspath, lineno, msg = request._pyfuncitem.reportinfo() + lines, _ = inspect.getsourcelines(request.function) + for i, line in enumerate(lines): + if line.strip().startswith('def'): + return FuncargLookupErrorRepr(fspath, lineno, lines[:i+1], + str(excinfo.value.msg)) if self.config.option.fulltrace: style="long" else: @@ -391,6 +404,75 @@ self._location = location return location +class FuncargLookupError(LookupError): + """ could not find a factory. """ + def __init__(self, request, msg): + self.request = request + self.msg = msg + +class FuncargManager: + _argprefix = "pytest_funcarg__" + FuncargLookupError = FuncargLookupError + + def __init__(self, session): + self.session = session + self.config = session.config + self.node2name2factory = {} + + def _discoverfactories(self, request, argname): + node = request._pyfuncitem + name2factory = self.node2name2factory.setdefault(node, {}) + if argname not in name2factory: + name2factory[argname] = self.config.pluginmanager.listattr( + plugins=request._plugins, + attrname=self._argprefix + str(argname) + ) + #else: we are called recursively + if not name2factory[argname]: + self._raiselookupfailed(request, argname) + + def _getfuncarg(self, request, argname): + node = request._pyfuncitem + try: + factorylist = self.node2name2factory[node][argname] + except KeyError: + # XXX at collection time this funcarg was not know to be a + # requirement, would be better if it would be known + self._discoverfactories(request, argname) + factorylist = self.node2name2factory[node][argname] + + if not factorylist: + self._raiselookupfailed(request, argname) + funcargfactory = factorylist.pop() + oldarg = request._currentarg + mp = monkeypatch() + mp.setattr(request, '_currentarg', argname) + try: + param = node.callspec.getparam(argname) + except (AttributeError, ValueError): + pass + else: + mp.setattr(request, 'param', param, raising=False) + try: + return funcargfactory(request=request) + finally: + mp.undo() + + def _raiselookupfailed(self, request, argname): + available = [] + for plugin in request._plugins: + for name in vars(plugin): + if name.startswith(self._argprefix): + name = name[len(self._argprefix):] + if name not in available: + available.append(name) + fspath, lineno, msg = request._pyfuncitem.reportinfo() + msg = "LookupError: no factory found for argument %r" % (argname,) + msg += "\n available funcargs: %s" %(", ".join(available),) + msg += "\n use 'py.test --funcargs [testpath]' for help on them." + raise FuncargLookupError(request, msg) + + class NoMatch(Exception): """ raised if matching cannot locate a matching names. """ @@ -407,6 +489,7 @@ self.shouldstop = False self.trace = config.trace.root.get("collection") self._norecursepatterns = config.getini("norecursedirs") + self.funcargmanager = FuncargManager(self) def pytest_collectstart(self): if self.shouldstop: @@ -634,4 +717,18 @@ +class FuncargLookupErrorRepr(TerminalRepr): + def __init__(self, filename, firstlineno, deflines, errorstring): + self.deflines = deflines + self.errorstring = errorstring + self.filename = filename + self.firstlineno = firstlineno + def toterminal(self, tw): + tw.line() + for line in self.deflines: + tw.line(" " + line.strip()) + for line in self.errorstring.split("\n"): + tw.line(" " + line.strip(), red=True) + tw.line() + tw.line("%s:%d" % (self.filename, self.firstlineno+1)) diff -r 19affc442801ad53c603e3d5ac912539dc1a6533 -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -3,8 +3,6 @@ import inspect import sys import pytest -from py._code.code import TerminalRepr -from _pytest.monkeypatch import monkeypatch import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() @@ -278,9 +276,9 @@ plugins = self.getplugins() + extra gentesthook.pcall(plugins, metafunc=metafunc) Function = self._getcustomclass("Function") + l = [] if not metafunc._calls: - return Function(name, parent=self) - l = [] + l.append(Function(name, parent=self)) for callspec in metafunc._calls: subname = "%s[%s]" %(name, callspec.id) function = Function(name=subname, parent=self, @@ -423,13 +421,6 @@ excinfo.traceback = ntraceback.filter() def _repr_failure_py(self, excinfo, style="long"): - if excinfo.errisinstance(FuncargRequest.LookupError): - fspath, lineno, msg = self.reportinfo() - lines, _ = inspect.getsourcelines(self.obj) - for i, line in enumerate(lines): - if line.strip().startswith('def'): - return FuncargLookupErrorRepr(fspath, lineno, - lines[:i+1], str(excinfo.value)) if excinfo.errisinstance(pytest.fail.Exception): if not excinfo.value.pytrace: return str(excinfo.value) @@ -441,22 +432,6 @@ return self._repr_failure_py(excinfo, style=self.config.option.tbstyle) -class FuncargLookupErrorRepr(TerminalRepr): - def __init__(self, filename, firstlineno, deflines, errorstring): - self.deflines = deflines - self.errorstring = errorstring - self.filename = filename - self.firstlineno = firstlineno - - def toterminal(self, tw): - tw.line() - for line in self.deflines: - tw.line(" " + line.strip()) - for line in self.errorstring.split("\n"): - tw.line(" " + line.strip(), red=True) - tw.line() - tw.line("%s:%d" % (self.filename, self.firstlineno+1)) - class Generator(FunctionMixin, PyCollector): def collect(self): @@ -523,23 +498,9 @@ try: request = function._request except AttributeError: - request = FuncargRequest(function) + request = function._request = FuncargRequest(function) request._fillfuncargs() -def XXXfillfuncargs(node): - """ fill missing funcargs. """ - node = FuncargRequest(node) - if node.funcargs is None: - node.funcargs = getattr(node, "_funcargs", {}) - if not isinstance(node, Function) or not node._isyieldedfunction(): - try: - funcargnames = node.funcargnames - except AttributeError: - funcargnames = getfuncargnames(node.function) - if funcargnames: - for argname in funcargnames: - node.getfuncargvalue(argname) - _notexists = object() class CallSpec2(object): @@ -711,11 +672,12 @@ curdir = py.path.local() tw = py.io.TerminalWriter() verbose = config.getvalue("verbose") + argprefix = session.funcargmanager._argprefix for plugin in plugins: available = [] for name, factory in vars(plugin).items(): - if name.startswith(FuncargRequest._argprefix): - name = name[len(FuncargRequest._argprefix):] + if name.startswith(argprefix): + name = name[len(argprefix):] if name not in available: available.append([name, factory]) if available: @@ -847,11 +809,11 @@ else: self.funcargs = {} self._request = req = FuncargRequest(self) + req._discoverfactories() if callobj is not _dummy: self.obj = callobj startindex = int(self.cls is not None) self.funcargnames = getfuncargnames(self.obj, startindex=startindex) - self.keywords.update(py.builtin._getfuncdict(self.obj) or {}) if keywords: self.keywords.update(keywords) @@ -912,11 +874,6 @@ If no such call was done in a ``pytest_generate_tests`` hook, the attribute will not be present. """ - _argprefix = "pytest_funcarg__" - _argname = None - - class LookupError(LookupError): - """ error on performing funcarg request. """ def __init__(self, pyfuncitem): self._pyfuncitem = pyfuncitem @@ -925,13 +882,24 @@ self.getparent = pyfuncitem.getparent self._funcargs = self._pyfuncitem.funcargs.copy() self._name2factory = {} + self.funcargmanager = pyfuncitem.session.funcargmanager self._currentarg = None + self.funcargnames = getfuncargnames(self.function) + + def _discoverfactories(self): + for argname in self.funcargnames: + if argname not in self._funcargs: + self.funcargmanager._discoverfactories(self, argname) @cached_property def _plugins(self): extra = [obj for obj in (self.module, self.instance) if obj] return self._pyfuncitem.getplugins() + extra + def raiseerror(self, msg): + """ raise a FuncargLookupError with the given message. """ + raise self.funcargmanager.FuncargLookupError(self, msg) + @property def function(self): """ function object of the test invocation. """ @@ -972,14 +940,13 @@ return self._pyfuncitem.fspath def _fillfuncargs(self): - argnames = getfuncargnames(self.function) - if argnames: + if self.funcargnames: assert not getattr(self._pyfuncitem, '_args', None), ( "yielded functions cannot have funcargs") - for argname in argnames: + for argname in self.funcargnames: if argname not in self._pyfuncitem.funcargs: - self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) - + self._pyfuncitem.funcargs[argname] = \ + self.getfuncargvalue(argname) def applymarker(self, marker): """ Apply a marker to a single test function invocation. @@ -1021,6 +988,7 @@ self._addfinalizer(finalizer, scope=scope) return val + def getfuncargvalue(self, argname): """ Retrieve a function argument by name for this test function invocation. This allows one function argument factory @@ -1033,29 +1001,9 @@ return self._funcargs[argname] except KeyError: pass - if argname not in self._name2factory: - self._name2factory[argname] = self.config.pluginmanager.listattr( - plugins=self._plugins, - attrname=self._argprefix + str(argname) - ) - #else: we are called recursively - if not self._name2factory[argname]: - self._raiselookupfailed(argname) - funcargfactory = self._name2factory[argname].pop() - oldarg = self._currentarg - mp = monkeypatch() - mp.setattr(self, '_currentarg', argname) - try: - param = self._pyfuncitem.callspec.getparam(argname) - except (AttributeError, ValueError): - pass - else: - mp.setattr(self, 'param', param, raising=False) - try: - self._funcargs[argname] = res = funcargfactory(request=self) - finally: - mp.undo() - return res + val = self.funcargmanager._getfuncarg(self, argname) + self._funcargs[argname] = val + return val def _getscopeitem(self, scope): if scope == "function": @@ -1084,16 +1032,3 @@ def __repr__(self): return "" %(self._pyfuncitem) - def _raiselookupfailed(self, argname): - available = [] - for plugin in self._plugins: - for name in vars(plugin): - if name.startswith(self._argprefix): - name = name[len(self._argprefix):] - if name not in available: - available.append(name) - fspath, lineno, msg = self._pyfuncitem.reportinfo() - msg = "LookupError: no factory found for function argument %r" % (argname,) - msg += "\n available funcargs: %s" %(", ".join(available),) - msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise self.LookupError(msg) diff -r 19affc442801ad53c603e3d5ac912539dc1a6533 -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e testing/test_mark.py --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -272,7 +272,7 @@ import pytest @pytest.mark.hello("pos1", z=4) @pytest.mark.hello("pos0", z=3) - def test_func(self): + def test_func(): pass """) items, rec = testdir.inline_genitems(p) diff -r 19affc442801ad53c603e3d5ac912539dc1a6533 -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1,5 +1,6 @@ import pytest, py, sys from _pytest import python as funcargs +from _pytest.main import FuncargLookupError class TestModule: def test_failing_import(self, testdir): @@ -301,24 +302,14 @@ assert not f1 != f1_b def test_function_equality_with_callspec(self, testdir, tmpdir): - config = testdir.parseconfigure() - class callspec1: - param = 1 - funcargs = {} - id = "hello" - class callspec2: - param = 1 - funcargs = {} - id = "world" - session = testdir.Session(config) - def func(): - pass - f5 = pytest.Function(name="name", config=config, - callspec=callspec1, callobj=func, session=session) - f5b = pytest.Function(name="name", config=config, - callspec=callspec2, callobj=func, session=session) - assert f5 != f5b - assert not (f5 == f5b) + items = testdir.getitems(""" + import pytest + @pytest.mark.parametrize('arg', [1,2]) + def test_function(arg): + pass + """) + assert items[0] != items[1] + assert not (items[0] == items[1]) def test_pyfunc_call(self, testdir): item = testdir.getitem("def test_func(): raise ValueError") @@ -550,33 +541,30 @@ assert pytest._fillfuncargs == funcargs.fillfuncargs def test_funcarg_lookupfails(self, testdir): - testdir.makeconftest(""" + testdir.makepyfile(""" def pytest_funcarg__xyzsomething(request): return 42 + + def test_func(some): + pass """) - item = testdir.getitem("def test_func(some): pass") - exc = pytest.raises(funcargs.FuncargRequest.LookupError, - "funcargs.fillfuncargs(item)") - s = str(exc.value) - assert s.find("xyzsomething") != -1 - - def test_funcarg_lookup_default(self, testdir): - item = testdir.getitem("def test_func(some, other=42): pass") - class Provider: - def pytest_funcarg__some(self, request): - return request.function.__name__ - item.config.pluginmanager.register(Provider()) - funcargs.fillfuncargs(item) - assert len(item.funcargs) == 1 + result = testdir.runpytest() # "--collectonly") + assert result.ret != 0 + result.stdout.fnmatch_lines([ + "*def test_func(some)*", + "*LookupError*", + "*xyzsomething*", + ]) def test_funcarg_basic(self, testdir): - item = testdir.getitem("def test_func(some, other): pass") - class Provider: - def pytest_funcarg__some(self, request): + item = testdir.getitem(""" + def pytest_funcarg__some(request): return request.function.__name__ - def pytest_funcarg__other(self, request): + def pytest_funcarg__other(request): return 42 - item.config.pluginmanager.register(Provider()) + def test_func(some, other): + pass + """) funcargs.fillfuncargs(item) assert len(item.funcargs) == 2 assert item.funcargs['some'] == "test_func" @@ -612,17 +600,6 @@ "*1 passed*" ]) - def test_fillfuncargs_exposed(self, testdir): - item = testdir.getitem("def test_func(some, other=42): pass") - class Provider: - def pytest_funcarg__some(self, request): - return request.function.__name__ - item.config.pluginmanager.register(Provider()) - if hasattr(item, '_args'): - del item._args - from _pytest.python import fillfuncargs - fillfuncargs(item) - assert len(item.funcargs) == 1 class TestRequest: def test_request_attributes(self, testdir): @@ -642,10 +619,12 @@ def test_request_attributes_method(self, testdir): item, = testdir.getitems(""" class TestB: + def pytest_funcarg__something(request): + return 1 def test_func(self, something): pass """) - req = funcargs.FuncargRequest(item) + req = item._request assert req.cls.__name__ == "TestB" assert req.instance.__class__ == req.cls @@ -686,8 +665,8 @@ return l.pop() def test_func(something): pass """) - req = funcargs.FuncargRequest(item) - pytest.raises(req.LookupError, req.getfuncargvalue, "notexists") + req = item._request + pytest.raises(FuncargLookupError, req.getfuncargvalue, "notexists") val = req.getfuncargvalue("something") assert val == 1 val = req.getfuncargvalue("something") @@ -729,7 +708,7 @@ """) result = testdir.runpytest(p) result.stdout.fnmatch_lines([ - "*1 passed*1 error*" + "*1 error*" # XXX the whole module collection fails ]) def test_request_getmodulepath(self, testdir): @@ -740,6 +719,8 @@ def test_applymarker(testdir): item1,item2 = testdir.getitems(""" + def pytest_funcarg__something(request): + pass class TestClass: def test_func1(self, something): pass @@ -756,60 +737,38 @@ pytest.raises(ValueError, "req1.applymarker(42)") class TestRequestCachedSetup: - def test_request_cachedsetup(self, testdir): - item1,item2 = testdir.getitems(""" - def test_func1(self, something): - pass + def test_request_cachedsetup_defaultmodule(self, testdir): + reprec = testdir.inline_runsource(""" + mysetup = ["hello",].pop + + def pytest_funcarg__something(request): + return request.cached_setup(mysetup, scope="module") + + def test_func1(something): + assert something == "hello" class TestClass: - def test_func2(self, something): - pass + def test_func1a(self, something): + assert something == "hello" """) - req1 = funcargs.FuncargRequest(item1) - l = ["hello"] - def setup(): - return l.pop() - # cached_setup's scope defaults to 'module' - ret1 = req1.cached_setup(setup) - assert ret1 == "hello" - ret1b = req1.cached_setup(setup) - assert ret1 == ret1b - req2 = funcargs.FuncargRequest(item2) - ret2 = req2.cached_setup(setup) - assert ret2 == ret1 + reprec.assertoutcome(passed=2) def test_request_cachedsetup_class(self, testdir): - item1, item2, item3, item4 = testdir.getitems(""" - def test_func1(self, something): - pass - def test_func2(self, something): - pass + reprec = testdir.inline_runsource(""" + mysetup = ["hello", "hello2"].pop + + def pytest_funcarg__something(request): + return request.cached_setup(mysetup, scope="class") + def test_func1(something): + assert something == "hello2" + def test_func2(something): + assert something == "hello2" class TestClass: def test_func1a(self, something): - pass + assert something == "hello" def test_func2b(self, something): - pass + assert something == "hello" """) - req1 = funcargs.FuncargRequest(item2) - l = ["hello2", "hello"] - def setup(): - return l.pop() - - # module level functions setup with scope=class - # automatically turn "class" to "module" scope - ret1 = req1.cached_setup(setup, scope="class") - assert ret1 == "hello" - req2 = funcargs.FuncargRequest(item2) - ret2 = req2.cached_setup(setup, scope="class") - assert ret2 == "hello" - - req3 = funcargs.FuncargRequest(item3) - ret3a = req3.cached_setup(setup, scope="class") - ret3b = req3.cached_setup(setup, scope="class") - assert ret3a == "hello2" - assert ret3b == "hello2" - req4 = funcargs.FuncargRequest(item4) - ret4 = req4.cached_setup(setup, scope="class") - assert ret4 == ret3a + reprec.assertoutcome(passed=4) def test_request_cachedsetup_extrakey(self, testdir): item1 = testdir.getitem("def test_func(): pass") @@ -1351,7 +1310,7 @@ """) result = testdir.runpytest() result.stdout.fnmatch_lines([ - "*ERROR at setup of test_lookup_error*", + "*ERROR*collecting*test_funcarg_lookup_error.py*", "*def test_lookup_error(unknown):*", "*LookupError: no factory found*unknown*", "*available funcargs*", diff -r 19affc442801ad53c603e3d5ac912539dc1a6533 -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e testing/test_session.py --- a/testing/test_session.py +++ b/testing/test_session.py @@ -9,24 +9,24 @@ assert 0 def test_other(): raise ValueError(23) - def test_two(someargs): - pass + class TestClass: + def test_two(self, someargs): + pass """) reprec = testdir.inline_run(tfile) passed, skipped, failed = reprec.listoutcomes() assert len(skipped) == 0 assert len(passed) == 1 - assert len(failed) == 3 + assert len(failed) == 2 end = lambda x: x.nodeid.split("::")[-1] assert end(failed[0]) == "test_one_one" assert end(failed[1]) == "test_other" - assert end(failed[2]) == "test_two" itemstarted = reprec.getcalls("pytest_itemcollected") - assert len(itemstarted) == 4 - colstarted = reprec.getcalls("pytest_collectstart") - assert len(colstarted) == 1 + 1 - col = colstarted[1].collector - assert isinstance(col, pytest.Module) + assert len(itemstarted) == 3 + # XXX check for failing funcarg setup + colreports = reprec.getcalls("pytest_collectreport") + assert len(colreports) == 4 + assert colreports[1].report.failed def test_nested_import_error(self, testdir): tfile = testdir.makepyfile(""" https://bitbucket.org/hpk42/pytest/changeset/a644996f978e/ changeset: a644996f978e user: hpk42 date: 2012-07-20 14:16:28 summary: discover funcarg factories independently from request/Function items affected #: 6 files diff -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e -r a644996f978eb7a02138c8bde1b9fffdebdf6a8f _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -285,13 +285,15 @@ def _repr_failure_py(self, excinfo, style=None): LE = self.session.funcargmanager.FuncargLookupError if excinfo.errisinstance(LE): - request = excinfo.value.request - fspath, lineno, msg = request._pyfuncitem.reportinfo() - lines, _ = inspect.getsourcelines(request.function) - for i, line in enumerate(lines): - if line.strip().startswith('def'): - return FuncargLookupErrorRepr(fspath, lineno, lines[:i+1], - str(excinfo.value.msg)) + function = excinfo.value.function + if function is not None: + fspath, lineno = getfslineno(function) + lines, _ = inspect.getsourcelines(function) + for i, line in enumerate(lines): + if line.strip().startswith('def'): + return FuncargLookupErrorRepr(fspath, + lineno, lines[:i+1], + str(excinfo.value.msg)) if self.config.option.fulltrace: style="long" else: @@ -406,8 +408,8 @@ class FuncargLookupError(LookupError): """ could not find a factory. """ - def __init__(self, request, msg): - self.request = request + def __init__(self, function, msg): + self.function = function self.msg = msg class FuncargManager: @@ -417,60 +419,68 @@ def __init__(self, session): self.session = session self.config = session.config - self.node2name2factory = {} + self.arg2facspec = {} + session.config.pluginmanager.register(self, "funcmanage") + self._holderobjseen = set() - def _discoverfactories(self, request, argname): - node = request._pyfuncitem - name2factory = self.node2name2factory.setdefault(node, {}) - if argname not in name2factory: - name2factory[argname] = self.config.pluginmanager.listattr( - plugins=request._plugins, - attrname=self._argprefix + str(argname) - ) - #else: we are called recursively - if not name2factory[argname]: - self._raiselookupfailed(request, argname) - - def _getfuncarg(self, request, argname): - node = request._pyfuncitem + ### XXX this hook should be called for historic events like pytest_configure + ### so that we don't have to do the below pytest_collection hook + def pytest_plugin_registered(self, plugin): + #print "plugin_registered", plugin + nodeid = "" try: - factorylist = self.node2name2factory[node][argname] - except KeyError: - # XXX at collection time this funcarg was not know to be a - # requirement, would be better if it would be known - self._discoverfactories(request, argname) - factorylist = self.node2name2factory[node][argname] - - if not factorylist: - self._raiselookupfailed(request, argname) - funcargfactory = factorylist.pop() - oldarg = request._currentarg - mp = monkeypatch() - mp.setattr(request, '_currentarg', argname) - try: - param = node.callspec.getparam(argname) - except (AttributeError, ValueError): + p = py.path.local(plugin.__file__) + except AttributeError: pass else: - mp.setattr(request, 'param', param, raising=False) + if p.basename.startswith("conftest.py"): + nodeid = p.dirpath().relto(self.session.fspath) + self._parsefactories(plugin, nodeid) + + @pytest.mark.tryfirst + def pytest_collection(self, session): + plugins = session.config.pluginmanager.getplugins() + for plugin in plugins: + self.pytest_plugin_registered(plugin) + + def _parsefactories(self, holderobj, nodeid): + if holderobj in self._holderobjseen: + return + #print "parsefactories", holderobj + self._holderobjseen.add(holderobj) + for name in dir(holderobj): + #print "check", holderobj, name + if name.startswith(self._argprefix): + fname = name[len(self._argprefix):] + faclist = self.arg2facspec.setdefault(fname, []) + obj = getattr(holderobj, name) + faclist.append((nodeid, obj)) + + def getfactorylist(self, argname, nodeid, function): try: - return funcargfactory(request=request) - finally: - mp.undo() + factorydef = self.arg2facspec[argname] + except KeyError: + self._raiselookupfailed(argname, function, nodeid) + return self._matchfactories(factorydef, nodeid) - def _raiselookupfailed(self, request, argname): + def _matchfactories(self, factorydef, nodeid): + l = [] + for baseid, factory in factorydef: + #print "check", basepath, nodeid + if nodeid.startswith(baseid): + l.append(factory) + return l + + def _raiselookupfailed(self, argname, function, nodeid): available = [] - for plugin in request._plugins: - for name in vars(plugin): - if name.startswith(self._argprefix): - name = name[len(self._argprefix):] - if name not in available: - available.append(name) - fspath, lineno, msg = request._pyfuncitem.reportinfo() + for name, facdef in self.arg2facspec.items(): + faclist = self._matchfactories(facdef, nodeid) + if faclist: + available.append(name) msg = "LookupError: no factory found for argument %r" % (argname,) msg += "\n available funcargs: %s" %(", ".join(available),) msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise FuncargLookupError(request, msg) + raise FuncargLookupError(function, msg) class NoMatch(Exception): @@ -715,6 +725,13 @@ to cache on a per-session level. """ +def getfslineno(obj): + # xxx let decorators etc specify a sane ordering + if hasattr(obj, 'place_as'): + obj = obj.place_as + fslineno = py.code.getfslineno(obj) + assert isinstance(fslineno[1], int), obj + return fslineno class FuncargLookupErrorRepr(TerminalRepr): diff -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e -r a644996f978eb7a02138c8bde1b9fffdebdf6a8f _pytest/pytester.py --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -388,10 +388,12 @@ return config def getitem(self, source, funcname="test_func"): - for item in self.getitems(source): + items = self.getitems(source) + for item in items: if item.name == funcname: return item - assert 0, "%r item not found in module:\n%s" %(funcname, source) + assert 0, "%r item not found in module:\n%s\nitems: %s" %( + funcname, source, items) def getitems(self, source): modcol = self.getmodulecol(source) diff -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e -r a644996f978eb7a02138c8bde1b9fffdebdf6a8f _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -3,6 +3,8 @@ import inspect import sys import pytest +from _pytest.main import getfslineno +from _pytest.monkeypatch import monkeypatch import _pytest cutdir = py.path.local(_pytest.__file__).dirpath() @@ -192,18 +194,7 @@ return s.replace(".[", "[") def _getfslineno(self): - try: - return self._fslineno - except AttributeError: - pass - obj = self.obj - # xxx let decorators etc specify a sane ordering - if hasattr(obj, 'place_as'): - obj = obj.place_as - - self._fslineno = py.code.getfslineno(obj) - assert isinstance(self._fslineno[1], int), obj - return self._fslineno + return getfslineno(self.obj) def reportinfo(self): # XXX caching? @@ -213,12 +204,10 @@ fspath = sys.modules[obj.__module__].__file__ if fspath.endswith(".pyc"): fspath = fspath[:-1] - #assert 0 - #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) lineno = obj.compat_co_firstlineno modpath = obj.__module__ else: - fspath, lineno = self._getfslineno() + fspath, lineno = getfslineno(obj) modpath = self.getmodpath() assert isinstance(lineno, int) return fspath, lineno, modpath @@ -306,6 +295,10 @@ def _getobj(self): return self._memoizedcall('_obj', self._importtestmodule) + def collect(self): + self.session.funcargmanager._parsefactories(self.obj, self.nodeid) + return super(Module, self).collect() + def _importtestmodule(self): # we assume we are only called once per module try: @@ -370,7 +363,12 @@ class Instance(PyCollector): def _getobj(self): - return self.parent.obj() + obj = self.parent.obj() + return obj + + def collect(self): + self.session.funcargmanager._parsefactories(self.obj, self.nodeid) + return super(Instance, self).collect() def newinstance(self): self.obj = self._getobj() @@ -809,7 +807,7 @@ else: self.funcargs = {} self._request = req = FuncargRequest(self) - req._discoverfactories() + #req._discoverfactories() if callobj is not _dummy: self.obj = callobj startindex = int(self.cls is not None) @@ -885,20 +883,28 @@ self.funcargmanager = pyfuncitem.session.funcargmanager self._currentarg = None self.funcargnames = getfuncargnames(self.function) + self.parentid = pyfuncitem.parent.nodeid def _discoverfactories(self): for argname in self.funcargnames: if argname not in self._funcargs: - self.funcargmanager._discoverfactories(self, argname) + self._getfaclist(argname) - @cached_property - def _plugins(self): - extra = [obj for obj in (self.module, self.instance) if obj] - return self._pyfuncitem.getplugins() + extra + def _getfaclist(self, argname): + faclist = self._name2factory.get(argname, None) + if faclist is None: + faclist = self.funcargmanager.getfactorylist(argname, + self.parentid, + self.function) + self._name2factory[argname] = faclist + elif not faclist: + self.funcargmanager._raiselookupfailed(argname, self.function, + self.parentid) + return faclist def raiseerror(self, msg): """ raise a FuncargLookupError with the given message. """ - raise self.funcargmanager.FuncargLookupError(self, msg) + raise self.funcargmanager.FuncargLookupError(self.function, msg) @property def function(self): @@ -1001,9 +1007,23 @@ return self._funcargs[argname] except KeyError: pass - val = self.funcargmanager._getfuncarg(self, argname) - self._funcargs[argname] = val - return val + factorylist = self._getfaclist(argname) + funcargfactory = factorylist.pop() + node = self._pyfuncitem + oldarg = self._currentarg + mp = monkeypatch() + mp.setattr(self, '_currentarg', argname) + try: + param = node.callspec.getparam(argname) + except (AttributeError, ValueError): + pass + else: + mp.setattr(self, 'param', param, raising=False) + try: + self._funcargs[argname] = val = funcargfactory(request=self) + return val + finally: + mp.undo() def _getscopeitem(self, scope): if scope == "function": diff -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e -r a644996f978eb7a02138c8bde1b9fffdebdf6a8f testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -647,15 +647,14 @@ def pytest_funcarg__something(request): return 1 """) - item = testdir.getitem(""" + item = testdir.makepyfile(""" def pytest_funcarg__something(request): return request.getfuncargvalue("something") + 1 def test_func(something): assert something == 2 """) - req = funcargs.FuncargRequest(item) - val = req.getfuncargvalue("something") - assert val == 2 + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) def test_getfuncargvalue(self, testdir): item = testdir.getitem(""" @@ -1296,7 +1295,9 @@ class MyClass: pass """) - clscol = modcol.collect()[0] + # this hook finds funcarg factories + rep = modcol.ihook.pytest_make_collect_report(collector=modcol) + clscol = rep.result[0] clscol.obj = lambda arg1: None clscol.funcargs = {} funcargs.fillfuncargs(clscol) @@ -1310,7 +1311,7 @@ """) result = testdir.runpytest() result.stdout.fnmatch_lines([ - "*ERROR*collecting*test_funcarg_lookup_error.py*", + "*ERROR*test_lookup_error*", "*def test_lookup_error(unknown):*", "*LookupError: no factory found*unknown*", "*available funcargs*", @@ -1633,3 +1634,49 @@ "*test_function*basic*PASSED", "*test_function*advanced*FAILED", ]) + +### XXX shift to test_session.py +class TestFuncargManager: + def pytest_funcarg__testdir(self, request): + testdir = request.getfuncargvalue("testdir") + testdir.makeconftest(""" + def pytest_funcarg__hello(request): + return "conftest" + + def pytest_funcarg__fm(request): + return request.funcargmanager + + def pytest_funcarg__item(request): + return request._pyfuncitem + """) + return testdir + + def test_parsefactories_conftest(self, testdir): + testdir.makepyfile(""" + def test_hello(item, fm): + for name in ("fm", "hello", "item"): + faclist = fm.getfactorylist(name, item.nodeid, item.obj) + assert len(faclist) == 1 + fac = faclist[0] + assert fac.__name__ == "pytest_funcarg__" + name + """) + reprec = testdir.inline_run("-s") + reprec.assertoutcome(passed=1) + + def test_parsefactories_conftest_and_module_and_class(self, testdir): + testdir.makepyfile(""" + def pytest_funcarg__hello(request): + return "module" + class TestClass: + def pytest_funcarg__hello(self, request): + return "class" + def test_hello(self, item, fm): + faclist = fm.getfactorylist("hello", item.nodeid, item.obj) + print faclist + assert len(faclist) == 3 + assert faclist[0](item._request) == "conftest" + assert faclist[1](item._request) == "module" + assert faclist[2](item._request) == "class" + """) + reprec = testdir.inline_run("-s") + reprec.assertoutcome(passed=1) diff -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e -r a644996f978eb7a02138c8bde1b9fffdebdf6a8f testing/test_session.py --- a/testing/test_session.py +++ b/testing/test_session.py @@ -17,16 +17,16 @@ passed, skipped, failed = reprec.listoutcomes() assert len(skipped) == 0 assert len(passed) == 1 - assert len(failed) == 2 + assert len(failed) == 3 end = lambda x: x.nodeid.split("::")[-1] assert end(failed[0]) == "test_one_one" assert end(failed[1]) == "test_other" itemstarted = reprec.getcalls("pytest_itemcollected") - assert len(itemstarted) == 3 + assert len(itemstarted) == 4 # XXX check for failing funcarg setup - colreports = reprec.getcalls("pytest_collectreport") - assert len(colreports) == 4 - assert colreports[1].report.failed + #colreports = reprec.getcalls("pytest_collectreport") + #assert len(colreports) == 4 + #assert colreports[1].report.failed def test_nested_import_error(self, testdir): tfile = testdir.makepyfile(""" @@ -225,3 +225,4 @@ result = testdir.runpytest("--ignore=hello", "--ignore=hello2") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) + diff -r ab0f23204d9f6dfa9e41c26bd293798d33f5255e -r a644996f978eb7a02138c8bde1b9fffdebdf6a8f testing/test_tmpdir.py --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -4,12 +4,18 @@ from _pytest.tmpdir import pytest_funcarg__tmpdir, TempdirHandler def test_funcarg(testdir): - item = testdir.getitem(""" + testdir.makepyfile(""" def pytest_generate_tests(metafunc): metafunc.addcall(id='a') metafunc.addcall(id='b') def test_func(tmpdir): pass - """, 'test_func[a]') + """) + reprec = testdir.inline_run() + calls = reprec.getcalls("pytest_runtest_setup") + item = calls[0].item + # pytest_unconfigure has deleted the TempdirHandler already + config = item.config + config._tmpdirhandler = TempdirHandler(config) p = pytest_funcarg__tmpdir(item) assert p.check() bn = p.basename.strip("0123456789") https://bitbucket.org/hpk42/pytest/changeset/bec67ad07ffb/ changeset: bec67ad07ffb user: hpk42 date: 2012-07-20 14:16:46 summary: extend Metafunc and write a pytest_generate_tests hook on the funcarg manager which discovers factories affected #: 3 files diff -r a644996f978eb7a02138c8bde1b9fffdebdf6a8f -r bec67ad07ffbed5d029496c588090642b95b944d _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -443,6 +443,19 @@ for plugin in plugins: self.pytest_plugin_registered(plugin) + def pytest_generate_tests(self, metafunc): + for argname in metafunc.funcargnames: + faclist = self.getfactorylist(argname, metafunc.parentid, + metafunc.function, raising=False) + if faclist is None: + continue # will raise at setup time + for fac in faclist: + marker = getattr(fac, "funcarg", None) + if marker is not None: + params = marker.kwargs.get("params") + if params is not None: + metafunc.parametrize(argname, params, indirect=True) + def _parsefactories(self, holderobj, nodeid): if holderobj in self._holderobjseen: return @@ -456,12 +469,14 @@ obj = getattr(holderobj, name) faclist.append((nodeid, obj)) - def getfactorylist(self, argname, nodeid, function): + def getfactorylist(self, argname, nodeid, function, raising=True): try: factorydef = self.arg2facspec[argname] except KeyError: - self._raiselookupfailed(argname, function, nodeid) - return self._matchfactories(factorydef, nodeid) + if raising: + self._raiselookupfailed(argname, function, nodeid) + else: + return self._matchfactories(factorydef, nodeid) def _matchfactories(self, factorydef, nodeid): l = [] diff -r a644996f978eb7a02138c8bde1b9fffdebdf6a8f -r bec67ad07ffbed5d029496c588090642b95b944d _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -256,7 +256,7 @@ clscol = self.getparent(Class) cls = clscol and clscol.obj or None transfer_markers(funcobj, cls, module) - metafunc = Metafunc(funcobj, config=self.config, + metafunc = Metafunc(funcobj, parentid=self.nodeid, config=self.config, cls=cls, module=module) gentesthook = self.config.hook.pytest_generate_tests extra = [module] @@ -555,10 +555,12 @@ class Metafunc: - def __init__(self, function, config=None, cls=None, module=None): + def __init__(self, function, config=None, cls=None, module=None, + parentid=""): self.config = config self.module = module self.function = function + self.parentid = parentid self.funcargnames = getfuncargnames(function, startindex=int(cls is not None)) self.cls = cls @@ -885,11 +887,6 @@ self.funcargnames = getfuncargnames(self.function) self.parentid = pyfuncitem.parent.nodeid - def _discoverfactories(self): - for argname in self.funcargnames: - if argname not in self._funcargs: - self._getfaclist(argname) - def _getfaclist(self, argname): faclist = self._name2factory.get(argname, None) if faclist is None: diff -r a644996f978eb7a02138c8bde1b9fffdebdf6a8f -r bec67ad07ffbed5d029496c588090642b95b944d testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -989,6 +989,7 @@ def test_parametrize_functional(self, testdir): testdir.makepyfile(""" def pytest_generate_tests(metafunc): + assert "test_parametrize_functional" in metafunc.parentid metafunc.parametrize('x', [1,2], indirect=True) metafunc.parametrize('y', [2]) def pytest_funcarg__x(request): @@ -1680,3 +1681,20 @@ """) reprec = testdir.inline_run("-s") reprec.assertoutcome(passed=1) + +class TestFuncargMarker: + def test_parametrize(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.funcarg(params=["a", "b", "c"]) + def pytest_funcarg__arg(request): + return request.param + l = [] + def test_param(arg): + l.append(arg) + def test_result(): + assert l == list("abc") + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=4) + https://bitbucket.org/hpk42/pytest/changeset/cae9ed2d4230/ changeset: cae9ed2d4230 user: hpk42 date: 2012-07-20 14:16:46 summary: implement funcarg factory scope marker and ScopeMismatch detection affected #: 2 files diff -r bec67ad07ffbed5d029496c588090642b95b944d -r cae9ed2d4230a4ade4e8f7c5fd8864004ecd3d85 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -886,6 +886,7 @@ self._currentarg = None self.funcargnames = getfuncargnames(self.function) self.parentid = pyfuncitem.parent.nodeid + self.scope = "function" def _getfaclist(self, argname): faclist = self._name2factory.get(argname, None) @@ -982,6 +983,9 @@ try: val = cache[cachekey] except KeyError: + __tracebackhide__ = True + check_scope(self.scope, scope) + __tracebackhide__ = False val = setup() cache[cachekey] = val if teardown is not None: @@ -1007,7 +1011,6 @@ factorylist = self._getfaclist(argname) funcargfactory = factorylist.pop() node = self._pyfuncitem - oldarg = self._currentarg mp = monkeypatch() mp.setattr(self, '_currentarg', argname) try: @@ -1016,11 +1019,24 @@ pass else: mp.setattr(self, 'param', param, raising=False) - try: - self._funcargs[argname] = val = funcargfactory(request=self) - return val - finally: - mp.undo() + + # implemenet funcarg marker scope + marker = getattr(funcargfactory, "funcarg", None) + scope = None + if marker is not None: + scope = marker.kwargs.get("scope") + if scope is not None: + __tracebackhide__ = True + check_scope(self.scope, scope) + __tracebackhide__ = False + mp.setattr(self, "scope", scope) + val = self.cached_setup(lambda: funcargfactory(request=self), + scope=scope) + else: + val = funcargfactory(request=self) + mp.undo() + self._funcargs[argname] = val + return val def _getscopeitem(self, scope): if scope == "function": @@ -1039,7 +1055,7 @@ def addfinalizer(self, finalizer): """add finalizer function to be called after test function finished execution. """ - self._addfinalizer(finalizer, scope="function") + self._addfinalizer(finalizer, scope=self.scope) def _addfinalizer(self, finalizer, scope): colitem = self._getscopeitem(scope) @@ -1049,3 +1065,15 @@ def __repr__(self): return "" %(self._pyfuncitem) +class ScopeMismatchError(Exception): + """ A funcarg factory tries to access a funcargvalue/factory + which has a lower scope (e.g. a Session one calls a function one) + """ +scopes = "session module class function".split() +def check_scope(currentscope, newscope): + __tracebackhide__ = True + i_currentscope = scopes.index(currentscope) + i_newscope = scopes.index(newscope) + if i_newscope > i_currentscope: + raise ScopeMismatchError("You tried to access a %r scoped funcarg " + "from a %r scoped one." % (newscope, currentscope)) diff -r bec67ad07ffbed5d029496c588090642b95b944d -r cae9ed2d4230a4ade4e8f7c5fd8864004ecd3d85 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -58,7 +58,7 @@ ]) def test_setup_teardown_class_as_classmethod(self, testdir): - testdir.makepyfile(""" + testdir.makepyfile(test_mod1=""" class TestClassMethod: @classmethod def setup_class(cls): @@ -1698,3 +1698,110 @@ reprec = testdir.inline_run() reprec.assertoutcome(passed=4) + def test_scope_session(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.mark.funcarg(scope="module") + def pytest_funcarg__arg(request): + l.append(1) + return 1 + + def test_1(arg): + assert arg == 1 + def test_2(arg): + assert arg == 1 + assert len(l) == 1 + class TestClass: + def test3(self, arg): + assert arg == 1 + assert len(l) == 1 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=3) + + def test_scope_module_uses_session(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.mark.funcarg(scope="module") + def pytest_funcarg__arg(request): + l.append(1) + return 1 + + def test_1(arg): + assert arg == 1 + def test_2(arg): + assert arg == 1 + assert len(l) == 1 + class TestClass: + def test3(self, arg): + assert arg == 1 + assert len(l) == 1 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=3) + + def test_scope_module_and_finalizer(self, testdir): + testdir.makeconftest(""" + import pytest + finalized = [] + created = [] + @pytest.mark.funcarg(scope="module") + def pytest_funcarg__arg(request): + created.append(1) + assert request.scope == "module" + request.addfinalizer(lambda: finalized.append(1)) + def pytest_funcarg__created(request): + return len(created) + def pytest_funcarg__finalized(request): + return len(finalized) + """) + testdir.makepyfile( + test_mod1=""" + def test_1(arg, created, finalized): + assert created == 1 + assert finalized == 0 + def test_2(arg, created, finalized): + assert created == 1 + assert finalized == 0""", + test_mod2=""" + def test_3(arg, created, finalized): + assert created == 2 + assert finalized == 1""", + test_mode3=""" + def test_4(arg, created, finalized): + assert created == 3 + assert finalized == 2 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=4) + + @pytest.mark.parametrize("method", [ + 'request.getfuncargvalue("arg")', + 'request.cached_setup(lambda: None, scope="function")', + ], ids=["getfuncargvalue", "cached_setup"]) + def test_scope_mismatch(self, testdir, method): + testdir.makeconftest(""" + import pytest + finalized = [] + created = [] + @pytest.mark.funcarg(scope="function") + def pytest_funcarg__arg(request): + pass + """) + testdir.makepyfile( + test_mod1=""" + import pytest + @pytest.mark.funcarg(scope="session") + def pytest_funcarg__arg(request): + %s + def test_1(arg): + pass + """ % method) + result = testdir.runpytest() + assert result.ret != 0 + result.stdout.fnmatch_lines([ + "*ScopeMismatch*You tried*function*from*session*", + ]) + https://bitbucket.org/hpk42/pytest/changeset/1c8d7b1e1d9f/ changeset: 1c8d7b1e1d9f user: hpk42 date: 2012-07-20 14:16:49 summary: allow registration of "funcarg" marked factories affected #: 2 files diff -r cae9ed2d4230a4ade4e8f7c5fd8864004ecd3d85 -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -463,11 +463,20 @@ self._holderobjseen.add(holderobj) for name in dir(holderobj): #print "check", holderobj, name - if name.startswith(self._argprefix): - fname = name[len(self._argprefix):] - faclist = self.arg2facspec.setdefault(fname, []) - obj = getattr(holderobj, name) - faclist.append((nodeid, obj)) + obj = getattr(holderobj, name) + # funcarg factories either have a pytest_funcarg__ prefix + # or are "funcarg" marked + if hasattr(obj, "funcarg"): + if name.startswith(self._argprefix): + argname = name[len(self._argprefix):] + else: + argname = name + elif name.startswith(self._argprefix): + argname = name[len(self._argprefix):] + else: + continue + faclist = self.arg2facspec.setdefault(argname, []) + faclist.append((nodeid, obj)) def getfactorylist(self, argname, nodeid, function, raising=True): try: diff -r cae9ed2d4230a4ade4e8f7c5fd8864004ecd3d85 -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1805,3 +1805,25 @@ "*ScopeMismatch*You tried*function*from*session*", ]) + def test_register_only_with_mark(self, testdir): + testdir.makeconftest(""" + import pytest + finalized = [] + created = [] + @pytest.mark.funcarg + def arg(request): + return 1 + """) + testdir.makepyfile( + test_mod1=""" + import pytest + @pytest.mark.funcarg + def arg(request): + return request.getfuncargvalue("arg") + 1 + def test_1(arg): + assert arg == 2 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + + https://bitbucket.org/hpk42/pytest/changeset/0f6467a7fbe8/ changeset: 0f6467a7fbe8 user: hpk42 date: 2012-07-20 14:16:50 summary: implement a scope/parametrized examples using the so-far new features also fix a bug with scoping/parametrization affected #: 8 files diff -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad -r 0f6467a7fbe8a78e0e07d88de95808398ce5ac20 _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.0.dev2' +__version__ = '2.3.0.dev3' diff -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad -r 0f6467a7fbe8a78e0e07d88de95808398ce5ac20 _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1030,8 +1030,11 @@ check_scope(self.scope, scope) __tracebackhide__ = False mp.setattr(self, "scope", scope) + kwargs = {} + if hasattr(self, "param"): + kwargs["extrakey"] = param val = self.cached_setup(lambda: funcargfactory(request=self), - scope=scope) + scope=scope, **kwargs) else: val = funcargfactory(request=self) mp.undo() diff -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad -r 0f6467a7fbe8a78e0e07d88de95808398ce5ac20 doc/en/conf.py --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,7 +17,7 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = release = "2.3.0.dev5" +version = release = "2.3.0.dev3" import sys, os diff -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad -r 0f6467a7fbe8a78e0e07d88de95808398ce5ac20 doc/en/example/index.txt --- a/doc/en/example/index.txt +++ b/doc/en/example/index.txt @@ -21,3 +21,4 @@ markers.txt pythoncollection.txt nonpython.txt + newexamples.txt diff -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad -r 0f6467a7fbe8a78e0e07d88de95808398ce5ac20 doc/en/example/newexamples.txt --- /dev/null +++ b/doc/en/example/newexamples.txt @@ -0,0 +1,183 @@ + +Scoping and parametrizing Funcarg factories +--------------------------------------------------- + +.. regendoc:wipe + +.. versionadded:: 2.3 + +The ``@pytest.mark.funcarg`` marker allows + +* to mark a function without a ``pytest_funcarg__`` as a factory +* to cause parametrization and run all tests multiple times + with the multiple created resources +* to set a scope which determines the level of caching + +Here is a simple example for defining a SMTPServer server +object with a session scope:: + + # content of conftest.py + import pytest + import smtplib + + @pytest.mark.funcarg(scope="session") + def smtp(request): + smtp = smtplib.SMTP("merlinux.eu") + request.addfinalizer(smtp.close) + return smtp + +You can now use this server connection from your tests:: + + # content of test_module.py + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + assert "merlinux" in response[1] + assert 0 # for demo purposes + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + assert 0 # for demo purposes + +If you run the tests:: + + $ py.test -q + collecting ... collected 2 items + FF + ================================= FAILURES ================================= + ________________________________ test_ehlo _________________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + assert "merlinux" in response[1] + > assert 0 # for demo purposes + E assert 0 + + test_module.py:5: AssertionError + ________________________________ test_noop _________________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + 2 failed in 0.14 seconds + +you will see the two ``assert 0`` failing and can see that +the same (session-scoped) object was passed into the two test functions. + +If you now want to test multiple servers you can simply parametrize +the ``smtp`` factory:: + + # content of conftest.py + import pytest + import smtplib + + @pytest.mark.funcarg(scope="session", + params=["merlinux.eu", "mail.python.org"]) + def smtp(request): + smtp = smtplib.SMTP(request.param) + def fin(): + print "closing", smtp + smtp.close() + request.addfinalizer(fin) + return smtp + +Only two lines changed and no test code needs to change. Let's do +another run:: + + $ py.test -q + collecting ... collected 4 items + FFFF + ================================= FAILURES ================================= + __________________________ test_ehlo[merlinux.eu] __________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + assert "merlinux" in response[1] + > assert 0 # for demo purposes + E assert 0 + + test_module.py:5: AssertionError + ________________________ test_ehlo[mail.python.org] ________________________ + + smtp = + + def test_ehlo(smtp): + response = smtp.ehlo() + assert response[0] == 250 + > assert "merlinux" in response[1] + E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + + test_module.py:4: AssertionError + __________________________ test_noop[merlinux.eu] __________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + ________________________ test_noop[mail.python.org] ________________________ + + smtp = + + def test_noop(smtp): + response = smtp.noop() + assert response[0] == 250 + > assert 0 # for demo purposes + E assert 0 + + test_module.py:10: AssertionError + 4 failed in 5.70 seconds + closing + closing + +We get four failures because we are running the two tests twice with +different ``smtp`` instantiations as defined on the factory. +Note that with the ``mail.python.org`` connection the second tests +fails already in ``test_ehlo`` because it wrongly expects a specific +server string. + +You can look at what tests pytest collects without running them:: + + $ py.test --collectonly + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev3 + plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov + collecting ... collected 4 items + + + + + + + ============================= in 0.02 seconds ============================= + +And you can run without output capturing and minimized failure reporting to check that the ``smtp`` objects are finalized at session end:: + + $ py.test --tb=line -q -s + collecting ... collected 4 items + FFFF + ================================= FAILURES ================================= + /home/hpk/tmp/doc-exec-330/test_module.py:5: assert 0 + /home/hpk/tmp/doc-exec-330/test_module.py:4: assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + /home/hpk/tmp/doc-exec-330/test_module.py:10: assert 0 + /home/hpk/tmp/doc-exec-330/test_module.py:10: assert 0 + 4 failed in 6.02 seconds + closing + closing diff -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad -r 0f6467a7fbe8a78e0e07d88de95808398ce5ac20 doc/en/resources.txt --- a/doc/en/resources.txt +++ b/doc/en/resources.txt @@ -94,6 +94,8 @@ Direct scoping of funcarg factories -------------------------------------------------------- +.. note:: Implemented + Instead of calling cached_setup(), you can decorate your factory to state its scope:: @@ -116,6 +118,8 @@ Direct parametrization of funcarg resource factories ---------------------------------------------------------- +.. note:: Implemented + Previously, funcarg factories could not directly cause parametrization. You needed to specify a ``@parametrize`` or implement a ``pytest_generate_tests`` hook to perform parametrization, i.e. calling a test multiple times with different value sets. pytest-2.X introduces a decorator for use @@ -154,6 +158,8 @@ Direct usage of funcargs with funcargs factories ---------------------------------------------------------- +.. note:: Not Implemented - unclear if to. + You can now directly use funcargs in funcarg factories. Example:: @pytest.mark.funcarg(scope="session") @@ -169,6 +175,8 @@ The "pytest_funcarg__" prefix becomes optional ----------------------------------------------------- +.. note:: Implemented + When using the ``@funcarg`` decorator you do not need to use the ``pytest_funcarg__`` prefix any more:: @@ -186,6 +194,8 @@ support for a new @setup marker ------------------------------------------------------ +.. note:: Not-Implemented, still under consideration if to. + pytest for a long time offered a pytest_configure and a pytest_sessionstart hook which are often used to setup global resources. This suffers from several problems: @@ -252,6 +262,8 @@ Using funcarg resources in xUnit setup methods ------------------------------------------------------------ +.. note:: Not implemented. Not clear if to. + XXX Consider this feature in contrast to the @setup feature - probably introducing one of them is better and the @setup decorator is more flexible. @@ -296,6 +308,8 @@ the "directory" caching scope -------------------------------------------- +.. note:: Not implemented. + All API accepting a scope (:py:func:`cached_setup()` and the new funcarg/setup decorators) now also accept a "directory" specification. This allows to restrict/cache resource values on a @@ -304,6 +318,8 @@ funcarg and setup discovery now happens at collection time --------------------------------------------------------------------- +.. note:: Partially implemented - collectonly shows no extra information + pytest-2.X takes care to discover funcarg factories and setup_X methods at collection time. This is more efficient especially for large test suites. Moreover, a call to "py.test --collectonly" should be able to show @@ -404,7 +420,7 @@ ISSUES -------------------------- -decorating a parametrized funcarg factory: +decorating a parametrized funcarg factory:: @pytest.mark.funcarg(scope="session", params=["mysql", "pg"]) def db(request): diff -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad -r 0f6467a7fbe8a78e0e07d88de95808398ce5ac20 setup.py --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.0.dev2', + version='2.3.0.dev3', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r 1c8d7b1e1d9f131def25466d8ff88e466754f2ad -r 0f6467a7fbe8a78e0e07d88de95808398ce5ac20 testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1826,4 +1826,17 @@ reprec = testdir.inline_run() reprec.assertoutcome(passed=1) - + def test_parametrize_and_scope(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.funcarg(scope="module", params=["a", "b", "c"]) + def pytest_funcarg__arg(request): + return request.param + l = [] + def test_param(arg): + l.append(arg) + def test_result(): + assert l == list("abc") + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=4) 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. From commits-noreply at bitbucket.org Mon Jul 23 10:55:07 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Mon, 23 Jul 2012 08:55:07 -0000 Subject: [py-svn] commit/pytest: hpk42: clarify and add to sort-by-session-scoped parametrized resources example Message-ID: <20120723085507.8078.6100@bitbucket15.managed.contegix.com> 1 new commit in pytest: https://bitbucket.org/hpk42/pytest/changeset/01d1731a8542/ changeset: 01d1731a8542 user: hpk42 date: 2012-07-23 10:54:57 summary: clarify and add to sort-by-session-scoped parametrized resources example affected #: 1 file diff -r 0f6467a7fbe8a78e0e07d88de95808398ce5ac20 -r 01d1731a8542b591ebb6ba0cdad31b2c497a0b6b doc/en/example/parametrize.txt --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.txt @@ -38,8 +38,8 @@ $ py.test -q collecting ... collected 3 items ..F - =================================== FAILURES =================================== - ______________________________ test_eval[6*9-42] _______________________________ + ================================= FAILURES ================================= + ____________________________ test_eval[6*9-42] _____________________________ input = '6*9', expected = 42 @@ -104,8 +104,8 @@ $ py.test -q --all collecting ... collected 5 items ....F - =================================== FAILURES =================================== - _______________________________ test_compute[4] ________________________________ + ================================= FAILURES ================================= + _____________________________ test_compute[4] ______________________________ param1 = 4 @@ -153,21 +153,21 @@ this is a fully self-contained example which you can run with:: $ py.test test_scenarios.py - ============================= test session starts ============================== - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev3 plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 2 items test_scenarios.py .. - =========================== 2 passed in 0.02 seconds =========================== + ========================= 2 passed in 0.02 seconds ========================= If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function:: $ py.test --collectonly test_scenarios.py - ============================= test session starts ============================== - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev3 plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 2 items @@ -176,7 +176,7 @@ - =============================== in 0.01 seconds =============================== + ============================= in 0.02 seconds ============================= Deferring the setup of parametrized resources --------------------------------------------------- @@ -223,25 +223,25 @@ Let's first see how it looks like at collection time:: $ py.test test_backends.py --collectonly - ============================= test session starts ============================== - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev3 plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 2 items - =============================== in 0.01 seconds =============================== + ============================= in 0.02 seconds ============================= And then when we run the test:: $ py.test -q test_backends.py collecting ... collected 2 items .F - =================================== FAILURES =================================== - ___________________________ test_db_initialized[d2] ____________________________ + ================================= FAILURES ================================= + _________________________ test_db_initialized[d2] __________________________ - db = + db = def test_db_initialized(db): # a dummy test @@ -295,10 +295,10 @@ $ py.test -q collecting ... collected 3 items F.. - =================================== FAILURES =================================== - __________________________ TestClass.test_equals[1-2] __________________________ + ================================= FAILURES ================================= + ________________________ TestClass.test_equals[1-2] ________________________ - self = , a = 1, b = 2 + self = , a = 1, b = 2 def test_equals(self, a, b): > assert a == b @@ -326,9 +326,9 @@ . $ py.test -rs -q multipython.py collecting ... collected 75 items ............sss............sss............sss............ssssssssssssssssss - =========================== short test summary info ============================ + ========================= short test summary info ========================== SKIP [27] /home/hpk/p/pytest/doc/en/example/multipython.py:36: 'python2.8' not found - 48 passed, 27 skipped in 1.89 seconds + 48 passed, 27 skipped in 1.70 seconds .. regendoc:wipe @@ -337,13 +337,13 @@ By default pytest will execute test functions by executing all its parametrized invocations. If you rather want to group execution by parameter, you can use something like the following ``conftest.py`` example. It uses -a parametrized "session" object:: +a parametrized "resource" object:: # content of conftest.py def pytest_collection_modifyitems(items): def cmp(item1, item2): - param1 = item1.callspec.getparam("session") - param2 = item2.callspec.getparam("session") + param1 = item1.callspec.getparam("resource") + param2 = item2.callspec.getparam("resource") if param1 < param2: return -1 elif param1 > param2: @@ -352,49 +352,64 @@ items.sort(cmp=cmp) def pytest_generate_tests(metafunc): - if "session" in metafunc.funcargnames: - metafunc.parametrize("session", [1,2], indirect=True) + if "resource" in metafunc.funcargnames: + metafunc.parametrize("resource", [1,2], indirect=True) - class Session: + class Resource: def __init__(self, num): self.num = num + def finalize(self): + print "finalize", self - def pytest_funcarg__session(request): - return Session(request.param) + def pytest_funcarg__resource(request): + return request.cached_setup(lambda: Resource(request.param), + teardown=lambda res: res.finalize(), + extrakey=request.param) -If you know have a test file like this:: +If you have a test file like this:: - # content of test_session.py - def test_hello(session): + # content of test_resource.py + def test_hello(resource): pass - def test_world(session): + def test_world(resource): pass class TestClass: - def test_method1(self, session): + def test_method1(self, resource): pass - def test_method2(self, session): + def test_method2(self, resource): pass then a subsequent execution will order the running of tests by parameter value:: - $ py.test -v - ============================= test session starts ============================== - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev2 -- /home/hpk/venv/1/bin/python - cachedir: /home/hpk/tmp/doc-exec-313/.cache + $ py.test -v -s + =========================== test session starts ============================ + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev3 -- /home/hpk/venv/1/bin/python + cachedir: /home/hpk/tmp/doc-exec-340/.cache plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 8 items - test_session.py:1: test_hello[1] PASSED - test_session.py:4: test_world[1] PASSED - test_session.py:8: TestClass.test_method1[1] PASSED - test_session.py:10: TestClass.test_method2[1] PASSED - test_session.py:1: test_hello[2] PASSED - test_session.py:4: test_world[2] PASSED - test_session.py:8: TestClass.test_method1[2] PASSED - test_session.py:10: TestClass.test_method2[2] PASSED + test_resource.py:1: test_hello[1] PASSED + test_resource.py:4: test_world[1] PASSED + test_resource.py:8: TestClass.test_method1[1] PASSED + test_resource.py:10: TestClass.test_method2[1] PASSED + test_resource.py:1: test_hello[2] PASSED + test_resource.py:4: test_world[2] PASSED + test_resource.py:8: TestClass.test_method1[2] PASSED + test_resource.py:10: TestClass.test_method2[2] PASSED - =========================== 8 passed in 0.02 seconds =========================== + ========================= 8 passed in 0.03 seconds ========================= + finalize + finalize + +.. note:: + Despite the per-session ordering the finalize() of the session-scoped + resource executes at the end of the whole test session. The life + cycle of the two parametrized instantiated resources will thus overlap. + One possible workaround is to make the resource instantiations be + aware of each other and teardown the other one before returning a new + resource. There are plans for future releases of pytest to offer an + out-of-the-box way to support session-ordering. 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. From commits-noreply at bitbucket.org Tue Jul 24 12:58:48 2012 From: commits-noreply at bitbucket.org (Bitbucket) Date: Tue, 24 Jul 2012 10:58:48 -0000 Subject: [py-svn] commit/pytest: 2 new changesets Message-ID: <20120724105848.10359.45192@bitbucket16.managed.contegix.com> 2 new commits in pytest: https://bitbucket.org/hpk42/pytest/changeset/9a022cbc021d/ changeset: 9a022cbc021d user: hpk42 date: 2012-07-23 10:55:09 summary: allow funcarg factories to receive funcargs affected #: 4 files diff -r 01d1731a8542b591ebb6ba0cdad31b2c497a0b6b -r 9a022cbc021de5518b3036877e852b314469a15b _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -444,17 +444,26 @@ self.pytest_plugin_registered(plugin) def pytest_generate_tests(self, metafunc): - for argname in metafunc.funcargnames: + funcargnames = list(metafunc.funcargnames) + seen = set() + while funcargnames: + argname = funcargnames.pop(0) + if argname in seen: + continue + seen.add(argname) faclist = self.getfactorylist(argname, metafunc.parentid, metafunc.function, raising=False) if faclist is None: - continue # will raise at setup time + continue # will raise FuncargLookupError at setup time for fac in faclist: marker = getattr(fac, "funcarg", None) if marker is not None: params = marker.kwargs.get("params") if params is not None: metafunc.parametrize(argname, params, indirect=True) + newfuncargnames = getfuncargnames(fac) + newfuncargnames.remove("request") + funcargnames.extend(newfuncargnames) def _parsefactories(self, holderobj, nodeid): if holderobj in self._holderobjseen: @@ -773,3 +782,15 @@ tw.line(" " + line.strip(), red=True) tw.line() tw.line("%s:%d" % (self.filename, self.firstlineno+1)) + +def getfuncargnames(function, startindex=None): + # XXX merge with main.py's varnames + argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] + if startindex is None: + startindex = py.std.inspect.ismethod(function) and 1 or 0 + defaults = getattr(function, 'func_defaults', + getattr(function, '__defaults__', None)) or () + numdefaults = len(defaults) + if numdefaults: + return argnames[startindex:-numdefaults] + return argnames[startindex:] diff -r 01d1731a8542b591ebb6ba0cdad31b2c497a0b6b -r 9a022cbc021de5518b3036877e852b314469a15b _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -3,7 +3,7 @@ import inspect import sys import pytest -from _pytest.main import getfslineno +from _pytest.main import getfslineno, getfuncargnames from _pytest.monkeypatch import monkeypatch import _pytest @@ -475,17 +475,6 @@ return True -def getfuncargnames(function, startindex=None): - # XXX merge with main.py's varnames - argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] - if startindex is None: - startindex = py.std.inspect.ismethod(function) and 1 or 0 - defaults = getattr(function, 'func_defaults', - getattr(function, '__defaults__', None)) or () - numdefaults = len(defaults) - if numdefaults: - return argnames[startindex:-numdefaults] - return argnames[startindex:] def fillfuncargs(function): """ fill missing funcargs. """ @@ -887,6 +876,7 @@ self.funcargnames = getfuncargnames(self.function) self.parentid = pyfuncitem.parent.nodeid self.scope = "function" + self._factorystack = [] def _getfaclist(self, argname): faclist = self._name2factory.get(argname, None) @@ -947,7 +937,8 @@ if self.funcargnames: assert not getattr(self._pyfuncitem, '_args', None), ( "yielded functions cannot have funcargs") - for argname in self.funcargnames: + while self.funcargnames: + argname = self.funcargnames.pop(0) if argname not in self._pyfuncitem.funcargs: self._pyfuncitem.funcargs[argname] = \ self.getfuncargvalue(argname) @@ -984,7 +975,10 @@ val = cache[cachekey] except KeyError: __tracebackhide__ = True - check_scope(self.scope, scope) + if scopemismatch(self.scope, scope): + raise ScopeMismatchError("You tried to access a %r scoped " + "resource with a %r scoped request object" %( + (scope, self.scope))) __tracebackhide__ = False val = setup() cache[cachekey] = val @@ -1010,6 +1004,21 @@ pass factorylist = self._getfaclist(argname) funcargfactory = factorylist.pop() + self._factorystack.append(funcargfactory) + try: + return self._getfuncargvalue(funcargfactory, argname) + finally: + self._factorystack.pop() + + def _getfuncargvalue(self, funcargfactory, argname): + # collect funcargs from the factory + newnames = getfuncargnames(funcargfactory) + newnames.remove("request") + factory_kwargs = {"request": self} + def fillfactoryargs(): + for newname in newnames: + factory_kwargs[newname] = self.getfuncargvalue(newname) + node = self._pyfuncitem mp = monkeypatch() mp.setattr(self, '_currentarg', argname) @@ -1027,16 +1036,30 @@ scope = marker.kwargs.get("scope") if scope is not None: __tracebackhide__ = True - check_scope(self.scope, scope) + if scopemismatch(self.scope, scope): + # try to report something helpful + lines = [] + for factory in self._factorystack: + fs, lineno = getfslineno(factory) + p = self._pyfuncitem.session.fspath.bestrelpath(fs) + args = inspect.formatargspec(*inspect.getargspec(factory)) + lines.append("%s:%d\n def %s%s" %( + p, lineno, factory.__name__, args)) + raise ScopeMismatchError("You tried to access the %r scoped " + "funcarg %r with a %r scoped request object, " + "involved factories\n%s" %( + (scope, argname, self.scope, "\n".join(lines)))) __tracebackhide__ = False mp.setattr(self, "scope", scope) kwargs = {} if hasattr(self, "param"): kwargs["extrakey"] = param - val = self.cached_setup(lambda: funcargfactory(request=self), + fillfactoryargs() + val = self.cached_setup(lambda: funcargfactory(**factory_kwargs), scope=scope, **kwargs) else: - val = funcargfactory(request=self) + fillfactoryargs() + val = funcargfactory(**factory_kwargs) mp.undo() self._funcargs[argname] = val return val @@ -1072,11 +1095,14 @@ """ A funcarg factory tries to access a funcargvalue/factory which has a lower scope (e.g. a Session one calls a function one) """ + scopes = "session module class function".split() -def check_scope(currentscope, newscope): - __tracebackhide__ = True - i_currentscope = scopes.index(currentscope) - i_newscope = scopes.index(newscope) - if i_newscope > i_currentscope: - raise ScopeMismatchError("You tried to access a %r scoped funcarg " - "from a %r scoped one." % (newscope, currentscope)) +def scopemismatch(currentscope, newscope): + return scopes.index(newscope) > scopes.index(currentscope) + +def slice_kwargs(names, kwargs): + new_kwargs = {} + for name in names: + new_kwargs[name] = kwargs[name] + return new_kwargs + diff -r 01d1731a8542b591ebb6ba0cdad31b2c497a0b6b -r 9a022cbc021de5518b3036877e852b314469a15b doc/en/resources.txt --- a/doc/en/resources.txt +++ b/doc/en/resources.txt @@ -248,10 +248,11 @@ def modes(tmpdir, request): # ... -This would execute the ``modes`` function once for each parameter. -In addition to normal funcargs you can also receive the "request" -funcarg which represents a takes on each of the values in the -``params=[1,2,3]`` decorator argument. +This would execute the ``modes`` function once for each parameter +which will be put at ``request.param``. This request object offers +the ``addfinalizer(func)`` helper which allows to register a function +which will be executed when test functions within the specified scope +finished execution. .. note:: diff -r 01d1731a8542b591ebb6ba0cdad31b2c497a0b6b -r 9a022cbc021de5518b3036877e852b314469a15b testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -619,7 +619,7 @@ def test_request_attributes_method(self, testdir): item, = testdir.getitems(""" class TestB: - def pytest_funcarg__something(request): + def pytest_funcarg__something(self, request): return 1 def test_func(self, something): pass @@ -1616,6 +1616,70 @@ "*1 passed*", ]) +class TestFuncargFactory: + def test_receives_funcargs(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.funcarg + def arg1(request): + return 1 + + @pytest.mark.funcarg + def arg2(request, arg1): + return arg1 + 1 + + def test_add(arg2): + assert arg2 == 2 + def test_all(arg1, arg2): + assert arg1 == 1 + assert arg2 == 2 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2) + + def test_receives_funcargs_scope_mismatch(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.funcarg(scope="function") + def arg1(request): + return 1 + + @pytest.mark.funcarg(scope="module") + def arg2(request, arg1): + return arg1 + 1 + + def test_add(arg2): + assert arg2 == 2 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ScopeMismatch*involved factories*", + "* def arg2*", + "* def arg1*", + "*1 error*" + ]) + + def test_funcarg_parametrized_and_used_twice(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.mark.funcarg(params=[1,2]) + def arg1(request): + l.append(1) + return request.param + + @pytest.mark.funcarg + def arg2(request, arg1): + return arg1 + 1 + + def test_add(arg1, arg2): + assert arg2 == arg1 + 1 + assert len(l) == arg1 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) class TestResourceIntegrationFunctional: @@ -1802,7 +1866,7 @@ result = testdir.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines([ - "*ScopeMismatch*You tried*function*from*session*", + "*ScopeMismatch*You tried*function*session*request*", ]) def test_register_only_with_mark(self, testdir): https://bitbucket.org/hpk42/pytest/changeset/7bbcf3b9fff3/ changeset: 7bbcf3b9fff3 user: hpk42 date: 2012-07-24 12:10:04 summary: introduce @pytest.mark.setup decorated function, extend newexamples.txt and draft a V4 resources API doc. affected #: 7 files diff -r 9a022cbc021de5518b3036877e852b314469a15b -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a _pytest/__init__.py --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.0.dev3' +__version__ = '2.3.0.dev4' diff -r 9a022cbc021de5518b3036877e852b314469a15b -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a _pytest/main.py --- a/_pytest/main.py +++ b/_pytest/main.py @@ -8,6 +8,8 @@ from _pytest.monkeypatch import monkeypatch from py._code.code import TerminalRepr +from _pytest.mark import MarkInfo + tracebackcutdir = py.path.local(_pytest.__file__).dirpath() # exitcodes for the command line @@ -422,6 +424,7 @@ self.arg2facspec = {} session.config.pluginmanager.register(self, "funcmanage") self._holderobjseen = set() + self.setuplist = [] ### XXX this hook should be called for historic events like pytest_configure ### so that we don't have to do the below pytest_collection hook @@ -445,6 +448,9 @@ def pytest_generate_tests(self, metafunc): funcargnames = list(metafunc.funcargnames) + setuplist, allargnames = self.getsetuplist(metafunc.parentid) + #print "setuplist, allargnames", setuplist, allargnames + funcargnames.extend(allargnames) seen = set() while funcargnames: argname = funcargnames.pop(0) @@ -465,6 +471,8 @@ newfuncargnames.remove("request") funcargnames.extend(newfuncargnames) + + def _parsefactories(self, holderobj, nodeid): if holderobj in self._holderobjseen: return @@ -473,20 +481,36 @@ for name in dir(holderobj): #print "check", holderobj, name obj = getattr(holderobj, name) + if not callable(obj): + continue # funcarg factories either have a pytest_funcarg__ prefix # or are "funcarg" marked if hasattr(obj, "funcarg"): - if name.startswith(self._argprefix): - argname = name[len(self._argprefix):] - else: - argname = name + assert not name.startswith(self._argprefix) + argname = name elif name.startswith(self._argprefix): argname = name[len(self._argprefix):] else: + # no funcargs. check if we have a setup function. + setup = getattr(obj, "setup", None) + if setup is not None and isinstance(setup, MarkInfo): + self.setuplist.append((nodeid, obj)) continue faclist = self.arg2facspec.setdefault(argname, []) faclist.append((nodeid, obj)) + def getsetuplist(self, nodeid): + l = [] + allargnames = set() + for baseid, setup in self.setuplist: + #print "check", baseid, setup + if nodeid.startswith(baseid): + funcargnames = getfuncargnames(setup) + l.append((setup, funcargnames)) + allargnames.update(funcargnames) + return l, allargnames + + def getfactorylist(self, argname, nodeid, function, raising=True): try: factorydef = self.arg2facspec[argname] diff -r 9a022cbc021de5518b3036877e852b314469a15b -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a _pytest/python.py --- a/_pytest/python.py +++ b/_pytest/python.py @@ -834,6 +834,8 @@ def setup(self): super(Function, self).setup() fillfuncargs(self) + if hasattr(self, "_request"): + self._request._callsetup() def __eq__(self, other): try: @@ -990,6 +992,22 @@ return val + def _callsetup(self): + setuplist, allnames = self.funcargmanager.getsetuplist( + self._pyfuncitem.nodeid) + for setupfunc, funcargnames in setuplist: + kwargs = {} + for name in funcargnames: + if name == "request": + kwargs[name] = self + else: + kwargs[name] = self.getfuncargvalue(name) + scope = readscope(setupfunc, "setup") + if scope is None: + setupfunc(**kwargs) + else: + self.cached_setup(lambda: setupfunc(**kwargs), scope=scope) + def getfuncargvalue(self, argname): """ Retrieve a function argument by name for this test function invocation. This allows one function argument factory @@ -1030,10 +1048,8 @@ mp.setattr(self, 'param', param, raising=False) # implemenet funcarg marker scope - marker = getattr(funcargfactory, "funcarg", None) - scope = None - if marker is not None: - scope = marker.kwargs.get("scope") + scope = readscope(funcargfactory, "funcarg") + if scope is not None: __tracebackhide__ = True if scopemismatch(self.scope, scope): @@ -1106,3 +1122,7 @@ new_kwargs[name] = kwargs[name] return new_kwargs +def readscope(func, markattr): + marker = getattr(func, markattr, None) + if marker is not None: + return marker.kwargs.get("scope") diff -r 9a022cbc021de5518b3036877e852b314469a15b -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a doc/en/example/newexamples.txt --- a/doc/en/example/newexamples.txt +++ b/doc/en/example/newexamples.txt @@ -48,7 +48,7 @@ ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -60,7 +60,7 @@ test_module.py:5: AssertionError ________________________________ test_noop _________________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -69,7 +69,7 @@ E assert 0 test_module.py:10: AssertionError - 2 failed in 0.14 seconds + 2 failed in 0.27 seconds you will see the two ``assert 0`` failing and can see that the same (session-scoped) object was passed into the two test functions. @@ -100,7 +100,7 @@ ================================= FAILURES ================================= __________________________ test_ehlo[merlinux.eu] __________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -112,7 +112,7 @@ test_module.py:5: AssertionError ________________________ test_ehlo[mail.python.org] ________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -123,7 +123,7 @@ test_module.py:4: AssertionError __________________________ test_noop[merlinux.eu] __________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -134,7 +134,7 @@ test_module.py:10: AssertionError ________________________ test_noop[mail.python.org] ________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -143,9 +143,9 @@ E assert 0 test_module.py:10: AssertionError - 4 failed in 5.70 seconds - closing - closing + 4 failed in 6.91 seconds + closing + closing We get four failures because we are running the two tests twice with different ``smtp`` instantiations as defined on the factory. @@ -157,7 +157,7 @@ $ py.test --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev3 + platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev4 plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov collecting ... collected 4 items @@ -174,10 +174,113 @@ collecting ... collected 4 items FFFF ================================= FAILURES ================================= - /home/hpk/tmp/doc-exec-330/test_module.py:5: assert 0 - /home/hpk/tmp/doc-exec-330/test_module.py:4: assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' - /home/hpk/tmp/doc-exec-330/test_module.py:10: assert 0 - /home/hpk/tmp/doc-exec-330/test_module.py:10: assert 0 - 4 failed in 6.02 seconds - closing - closing + /home/hpk/tmp/doc-exec-361/test_module.py:5: assert 0 + /home/hpk/tmp/doc-exec-361/test_module.py:4: assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + /home/hpk/tmp/doc-exec-361/test_module.py:10: assert 0 + /home/hpk/tmp/doc-exec-361/test_module.py:10: assert 0 + 4 failed in 6.83 seconds + closing + closing + +.. _`new_setup`: + +``@pytest.mark.setup``: xUnit on steroids +-------------------------------------------------------------------- + +.. regendoc:wipe + +.. versionadded:: 2.3 + +The ``@pytest.mark.setup`` marker allows + +* to mark a function as a setup/fixture method; the function can itself + receive funcargs +* to set a scope which determines the level of caching and how often + the setup function is going to be called. + +Here is a simple example which configures a global funcarg without +the test needing to have it in its signature:: + + # content of conftest.py + import pytest + + @pytest.mark.funcarg(scope="module") + def resource(request, tmpdir): + def fin(): + print "finalize", tmpdir + request.addfinalizer(fin) + print "created resource", tmpdir + return tmpdir + +And the test file contains a setup function using this resource:: + + # content of test_module.py + import pytest + + @pytest.mark.setup(scope="function") + def setresource(resource): + global myresource + myresource = resource + + def test_1(): + assert myresource + print "using myresource", myresource + + def test_2(): + assert myresource + print "using myresource", myresource + +Let's run this module:: + + $ py.test -qs + collecting ... collected 2 items + .. + 2 passed in 0.24 seconds + created resource /home/hpk/tmp/pytest-3715/test_10 + using myresource /home/hpk/tmp/pytest-3715/test_10 + using myresource /home/hpk/tmp/pytest-3715/test_10 + finalize /home/hpk/tmp/pytest-3715/test_10 + +The two test functions will see the same resource instance because it has +a module life cycle or scope. + +The resource funcarg can later add parametrization without any test +or setup code needing to change:: + + # content of conftest.py + import pytest + + @pytest.mark.funcarg(scope="module", params=["aaa", "bbb"]) + def resource(request, tmpdir): + newtmp = tmpdir.join(request.param) + def fin(): + print "finalize", newtmp + request.addfinalizer(fin) + print "created resource", newtmp + return newtmp + +Running this will run four tests:: + + $ py.test -qs + collecting ... collected 4 items + .... + 4 passed in 0.24 seconds + created resource /home/hpk/tmp/pytest-3716/test_1_aaa_0/aaa + using myresource /home/hpk/tmp/pytest-3716/test_1_aaa_0/aaa + created resource /home/hpk/tmp/pytest-3716/test_1_bbb_0/bbb + using myresource /home/hpk/tmp/pytest-3716/test_1_bbb_0/bbb + using myresource /home/hpk/tmp/pytest-3716/test_1_aaa_0/aaa + using myresource /home/hpk/tmp/pytest-3716/test_1_bbb_0/bbb + finalize /home/hpk/tmp/pytest-3716/test_1_bbb_0/bbb + finalize /home/hpk/tmp/pytest-3716/test_1_aaa_0/aaa + +Each parameter causes the creation of a respective resource and the +unchanged test module uses it in its ``@setup`` decorated method. + +.. note:: + + Currently, parametrized tests are sorted by test function location + so a test function will execute multiple times with different parametrized + funcargs. If you have class/module/session scoped funcargs and + they cause global side effects this can cause problems because the + code under test may not be prepared to deal with it. diff -r 9a022cbc021de5518b3036877e852b314469a15b -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a doc/en/resources.txt --- a/doc/en/resources.txt +++ b/doc/en/resources.txt @@ -1,43 +1,55 @@ -V3: Creating and working with parametrized test resources +V4: Creating and working with parametrized resources =============================================================== **Target audience**: Reading this document requires basic knowledge of python testing, xUnit setup methods and the basic pytest funcarg mechanism, see http://pytest.org/latest/funcargs.html -**Abstract**: pytest-2.X provides more powerful and more flexible funcarg -and setup machinery. It does so by introducing a new @funcarg and a -new @setup marker which allows to define scoping and parametrization -parameters. If using ``@funcarg``, following the ``pytest_funcarg__`` -naming pattern becomes optional. Functions decorated with ``@setup`` -are called independenlty from the definition of funcargs but can -access funcarg values if needed. This allows for ultimate flexibility -in designing your test fixtures and their parametrization. Also, -you can now use ``py.test --collectonly`` to inspect your fixture -setup. Nonwithstanding these extensions, pre-existing test suites -and plugins written to work for previous pytest versions shall run unmodified. +**Abstract**: pytest-2.X provides yet more powerful and flexible +fixture machinery by introducing: +* a new ``@pytest.mark.funcarg`` marker to define funcarg factories and their + scoping and parametrization. No special ``pytest_funcarg__`` naming there. -**Changes**: This V3 draft is based on incorporating and thinking about -feedback provided by Floris Bruynooghe, Carl Meyer and Samuele Pedroni. -It remains as draft documentation, pending further refinements and -changes according to implementation or backward compatibility issues. -The main changes to V2 are: +* a new ``@pytest.mark.setup`` marker to define setup functions and their + scoping. -* Collapse funcarg factory decorator into a single "@funcarg" one. - You can specify scopes and params with it. Moreover, if you supply - a "name" you do not need to follow the "pytest_funcarg__NAME" naming - pattern. Keeping with "funcarg" naming arguable now makes more - sense since the main interface using these resources are test and - setup functions. Keeping it probably causes the least semantic friction. +* directly use funcargs through funcarg factory signatures -* Drop setup_directory/setup_session and introduce a new @setup - decorator similar to the @funcarg one but accepting funcargs. +Both funcarg factories and setup functions can be defined in test modules, +classes, conftest.py files and installed plugins. -* cosnider the extended setup_X funcargs for dropping because - the new @setup decorator probably is more flexible and introduces - less implementation complexity. +The introduction of these two markers lifts several prior limitations +and allows to easily define and implement complex testing scenarios. + +Nonwithstanding these extensions, already existing test suites and plugins +written to work for previous pytest versions shall run unmodified. + + +**Changes**: This V4 draft is based on incorporating and thinking about +feedback on previous versions provided by Floris Bruynooghe, Carl Meyer, +Ronny Pfannschmidt and Samuele Pedroni. It remains as draft +documentation, pending further refinements and changes according to +implementation or backward compatibility issues. The main changes are: + +* Collapse funcarg factory decorators into a single "@funcarg" one. + You can specify scopes and params with it. When using the decorator + the "pytest_funcarg__" prefix becomes optional. + +* funcarg factories can now use funcargs themselves + +* Drop setup/directory scope from this draft + +* introduce a new @setup decorator similar to the @funcarg one + except that setup-markers cannot define parametriation themselves. + Instead they can easily depend on a parametrized funcarg (which + must not be visible at test function signatures). + +* drop consideration of setup_X support for funcargs because + it is less flexible and probably causes more implementation + troubles than the current @setup approach which can share + a lot of logic with the @funcarg one. .. currentmodule:: _pytest @@ -78,17 +90,13 @@ ``extrakey`` parameter containing ``request.param`` to the :py:func:`~python.Request.cached_setup` call. -3. the current implementation is inefficient: it performs factory discovery - each time a "db" argument is required. This discovery wrongly happens at - setup-time. +3. there is no way how you can make use of funcarg factories + in xUnit setup methods. -4. there is no way how you can use funcarg factories, let alone - parametrization, when your tests use the xUnit setup_X approach. +4. A non-parametrized funcarg factory cannot use a parametrized + funcarg resource if it isn't stated in the test function signature. -5. there is no way to specify a per-directory scope for caching. - -In the following sections, API extensions are presented to solve -each of these problems. +The following sections address the advances which solve all of these problems. Direct scoping of funcarg factories @@ -158,7 +166,7 @@ Direct usage of funcargs with funcargs factories ---------------------------------------------------------- -.. note:: Not Implemented - unclear if to. +.. note:: Implemented. You can now directly use funcargs in funcarg factories. Example:: @@ -168,33 +176,39 @@ Apart from convenience it also solves an issue when your factory depends on a parametrized funcarg. Previously, a call to -``request.getfuncargvalue()`` would not allow pytest to know -at collection time about the fact that a required resource is -actually parametrized. +``request.getfuncargvalue()`` happens at test execution time and +thus pytest would not know at collection time about the fact that +a required resource is parametrized. -The "pytest_funcarg__" prefix becomes optional ------------------------------------------------------ +No ``pytest_funcarg__`` prefix when using @funcarg decorator +------------------------------------------------------------------- + .. note:: Implemented -When using the ``@funcarg`` decorator you do not need to use -the ``pytest_funcarg__`` prefix any more:: +When using the ``@funcarg`` decorator the name of the function +does not need to (and in fact cannot) use the ``pytest_funcarg__`` +naming:: @pytest.mark.funcarg def db(request): ... The name under which the funcarg resource can be requested is ``db``. -Any ``pytest_funcarg__`` prefix will be stripped. Note that a an -unqualified funcarg-marker implies a scope of "function" meaning -that the funcarg factory will be called for each test function invocation. +You can also use the "old" non-decorator way of specifying funcarg factories +aka:: + def pytest_funcarg__db(request): + ... -support for a new @setup marker ------------------------------------------------------- +It is recommended to use the funcarg-decorator, however. -.. note:: Not-Implemented, still under consideration if to. + +solving per-session setup / the new @setup marker +-------------------------------------------------------------- + +.. note:: Implemented, at least working for basic situations. pytest for a long time offered a pytest_configure and a pytest_sessionstart hook which are often used to setup global resources. This suffers from @@ -212,9 +226,7 @@ fact that this hook is actually used for reporting, in particular the test-header with platform/custom information. -4. there is no direct way how you can restrict setup to a directory scope. - -Moreover, it is today not easy to define scoped setup from plugins or +Moreover, it is today not easy to define a scoped setup from plugins or conftest files other than to implement a ``pytest_runtest_setup()`` hook and caring for scoping/caching yourself. And it's virtually impossible to do this with parametrization as ``pytest_runtest_setup()`` is called @@ -222,222 +234,76 @@ It follows that pytest_configure/session/runtest_setup are often not appropriate for implementing common fixture needs. Therefore, -pytest-2.X introduces a new "@pytest.mark.setup" marker, accepting -the same parameters as the @funcargs decorator. The difference is -that the decorated function can accept function arguments itself -Example:: - - # content of conftest.py - import pytest - @pytest.mark.setup(scope="session") - def mysetup(db): - ... +pytest-2.X introduces a new "@pytest.mark.setup" marker which takes +an optional "scope" parameter. -This ``mysetup`` function is going to be executed when the first -test in the directory tree executes. It is going to be executed once -per-session and it receives the ``db`` funcarg which must be of same -of higher scope; you e. g. generally cannot use a per-module or per-function -scoped resource in a session-scoped setup function. - -You can also use ``@setup`` inside a test module or class:: - - # content of test_module.py - import pytest - - @pytest.mark.setup(scope="module", params=[1,2,3]) - def modes(tmpdir, request): - # ... - -This would execute the ``modes`` function once for each parameter -which will be put at ``request.param``. This request object offers -the ``addfinalizer(func)`` helper which allows to register a function -which will be executed when test functions within the specified scope -finished execution. - -.. note:: - - For each scope, the funcargs will be setup and then the setup functions - will be called. This allows @setup-decorated functions to depend - on already setup funcarg values by accessing ``request.funcargs``. - -Using funcarg resources in xUnit setup methods ------------------------------------------------------------- - -.. note:: Not implemented. Not clear if to. - -XXX Consider this feature in contrast to the @setup feature - probably -introducing one of them is better and the @setup decorator is more flexible. - -For a long time, pytest has recommended the usage of funcarg -factories as a primary means for managing resources in your test run. -It is a better approach than the jUnit-based approach in many cases, even -more with the new pytest-2.X features, because the funcarg resource factory -provides a single place to determine scoping and parametrization. Your tests -do not need to encode setup/teardown details in every test file's -setup_module/class/method. - -However, the jUnit methods originally introduced by pytest to Python, -remain popoular with nose and unittest-based test suites. Without question, -there are large existing test suites using this paradigm. pytest-2.X -recognizes this fact and now offers direct integration with funcarg resources. Here is a basic example for getting a per-module tmpdir:: - - def setup_module(mod, tmpdir): - mod.tmpdir = tmpdir - -This will trigger pytest's funcarg mechanism to create a value of -"tmpdir" which can then be used throughout the module as a global. - -The new extension to setup_X methods also works in case a resource is -parametrized. For example, let's consider an setup_class example using -our "db" resource:: - - class TestClass: - def setup_class(cls, db): - cls.db = db - # perform some extra things on db - # so that test methods can work with it - -With pytest-2.X the setup* methods will be discovered at collection-time, -allowing to seemlessly integrate this approach with parametrization, -allowing the factory specification to determine all details. The -setup_class itself does not itself need to be aware of the fact that -"db" might be a mysql/PG database. -Note that if the specified resource is provided only as a per-testfunction -resource, collection would early on report a ScopingMismatch error. - - -the "directory" caching scope --------------------------------------------- - -.. note:: Not implemented. - -All API accepting a scope (:py:func:`cached_setup()` and -the new funcarg/setup decorators) now also accept a "directory" -specification. This allows to restrict/cache resource values on a -per-directory level. +See :ref:`new_setup` for examples. funcarg and setup discovery now happens at collection time --------------------------------------------------------------------- -.. note:: Partially implemented - collectonly shows no extra information +.. note:: + Partially implemented - collectonly shows no extra information however. -pytest-2.X takes care to discover funcarg factories and setup_X methods +pytest-2.X takes care to discover funcarg factories and @setup methods at collection time. This is more efficient especially for large test suites. Moreover, a call to "py.test --collectonly" should be able to show a lot of setup-information and thus presents a nice method to get an overview of resource management in your project. -Implementation level -=================================================================== -To implement the above new features, pytest-2.X grows some new hooks and -methods. At the time of writing V2 and without actually implementing -it, it is not clear how much of this new internal API will also be -exposed and advertised e. g. for plugin writers. +Sorting tests by funcarg scopes +------------------------------------------- -The main effort, however, will lie in revising what is done at -collection and what at test setup time. All funcarg factories and -xUnit setup methods need to be discovered at collection time -for the above mechanism to work. Additionally all test function -signatures need to be parsed in order to know which resources are -used. On the plus side, all previously collected fixtures and -test functions only need to be called, no discovery is neccessary -is required anymore. +.. note:: Not implemented, Under consideration. -the "request" object incorporates scope-specific behaviour ------------------------------------------------------------------- +pytest by default sorts test items by their source location. +For class/module/session scoped funcargs it is not always +desirable to have multiple active funcargs. Sometimes, +the application under test may not even be able to handle it +because it relies on global state/side effects related to those +resources. -funcarg factories receive a request object to help with implementing -finalization and inspection of the requesting-context. If there is -no scoping is in effect, nothing much will change of the API behaviour. -However, with scoping the request object represents the according context. -Let's consider this example:: +Therefore, pytest-2.3 tries to minimize the number of active +resources and re-orders test items accordingly. Consider the following +example:: - @pytest.mark.factory_scope("class") - def pytest_funcarg__db(request): - # ... - request.getfuncargvalue(...) - # - request.addfinalizer(db) + @pytest.mark.funcarg(scope="module", params=[1,2]) + def arg(request): + ... + @pytest.mark.funcarg(scope="function", params=[1,2]) + def otherarg(request): + ... -Due to the class-scope, the request object will: + def test_0(otherarg): + pass + def test_1(arg): + pass + def test_2(arg, otherarg): + pass -- provide a ``None`` value for the ``request.function`` attribute. -- default to per-class finalization with the addfinalizer() call. -- raise a ScopeMismatchError if a more broadly scoped factory - wants to use a more tighly scoped factory (e.g. per-function) +if arg.1, arg.2, otherarg.1, otherarg.2 denote the respective +parametrized funcarg instances this will re-order test +execution like follows:: -In fact, the request object is likely going to provide a "node" -attribute, denoting the current collection node on which it internally -operates. (Prior to pytest-2.3 there already was an internal -_pyfuncitem). + test_0(otherarg.1) + test_0(otherarg.2) + test_1(arg.1) + test_2(arg.1, otherarg.1) + test_2(arg.1, otherarg.2) + test_1(arg.2) + test_2(arg.2, otherarg.1) + test_2(arg.2, otherarg.2) -As these are rather intuitive extensions, not much friction is expected -for test/plugin writers using the new scoping and parametrization mechanism. -It's, however, a serious internal effort to reorganize the pytest -implementation. +Moreover, test_2(arg.1) will execute any registered teardowns for +the arg.1 resource after the test finished execution. +.. note:: -node.register_factory/getresource() methods --------------------------------------------------------- + XXX it's quite unclear at the moment how to implement. + If we have a 1000 tests requiring different sets of parametrized + resources with different scopes, how to re-order accordingly? + It even seems difficult to express the expectation in a + concise manner. -In order to implement factory- and setup-method discovery at -collection time, a new node API will be introduced to allow -for factory registration and a getresource() call to obtain -created values. The exact details of this API remain subject -to experimentation. The basic idea is to introduce two new -methods to the Session class which is already available on all nodes -through the ``node.session`` attribute:: - class Session: - def register_resource_factory(self, name, factory_or_list, scope): - """ register a resource factory for the given name. - - :param name: Name of the resource. - :factory_or_list: a function or a list of functions creating - one or multiple resource values. - :param scope: a node instance. The factory will be only visisble - available for all descendant nodes. - specify the "session" instance for global availability - """ - - def getresource(self, name, node): - """ get a named resource for the give node. - - This method looks up a matching funcarg resource factory - and calls it. - """ - -.. todo:: - - XXX While this new API (or some variant of it) may suffices to implement - all of the described new usage-level features, it remains unclear how the - existing "@parametrize" or "metafunc.parametrize()" calls will map to it. - These parametrize-approaches tie resource parametrization to the - function/funcargs-usage rather than to the factories. - - - -ISSUES --------------------------- - -decorating a parametrized funcarg factory:: - - @pytest.mark.funcarg(scope="session", params=["mysql", "pg"]) - def db(request): - ... - class TestClass: - @pytest.mark.funcarg(scope="function") - def something(self, request): - session_db = request.getfuncargvalue("db") - ... - -Here the function-scoped "something" factory uses the session-scoped -"db" factory to perform some additional steps. The dependency, however, -is only visible at setup-time, when the factory actually gets called. - -In order to allow parametrization at collection-time I see two ways: - -- allow specifying dependencies in the funcarg-marker -- allow funcargs for factories as well - diff -r 9a022cbc021de5518b3036877e852b314469a15b -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a setup.py --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.0.dev3', + version='2.3.0.dev4', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff -r 9a022cbc021de5518b3036877e852b314469a15b -r 7bbcf3b9fff3ea413cc2f4d7f862ba1fe355c93a testing/test_python.py --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1746,12 +1746,121 @@ reprec = testdir.inline_run("-s") reprec.assertoutcome(passed=1) +class TestSetupDiscovery: + def pytest_funcarg__testdir(self, request): + testdir = request.getfuncargvalue("testdir") + testdir.makeconftest(""" + import pytest + @pytest.mark.setup + def perfunction(request): + pass + @pytest.mark.setup + def perfunction2(request): + pass + + def pytest_funcarg__fm(request): + return request.funcargmanager + + def pytest_funcarg__item(request): + return request._pyfuncitem + """) + return testdir + + def test_parsefactories_conftest(self, testdir): + testdir.makepyfile(""" + def test_check_setup(item, fm): + setuplist, allnames = fm.getsetuplist(item.nodeid) + assert len(setuplist) == 2 + assert setuplist[0][0].__name__ == "perfunction" + assert "request" in setuplist[0][1] + assert setuplist[1][0].__name__ == "perfunction2" + assert "request" in setuplist[1][1] + """) + reprec = testdir.inline_run("-s") + reprec.assertoutcome(passed=1) + + +class TestSetupManagement: + def test_funcarg_and_setup(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.mark.funcarg(scope="module") + def arg(request): + l.append(1) + return 0 + @pytest.mark.setup(scope="class") + def something(request, arg): + l.append(2) + + def test_hello(arg): + assert len(l) == 2 + assert l == [1,2] + assert arg == 0 + + def test_hello2(arg): + assert len(l) == 2 + assert l == [1,2] + assert arg == 0 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2) + + def test_setup_uses_parametrized_resource(self, testdir): + testdir.makepyfile(""" + import pytest + l = [] + @pytest.mark.funcarg(params=[1,2]) + def arg(request): + return request.param + + @pytest.mark.setup + def something(request, arg): + l.append(arg) + + def test_hello(): + if len(l) == 1: + assert l == [1] + elif len(l) == 2: + assert l == [1, 2] + else: + 0/0 + + """) + reprec = testdir.inline_run("-s") + reprec.assertoutcome(passed=2) + + def test_session_parametrized_function_setup(self, testdir): + testdir.makepyfile(""" + import pytest + + l = [] + + @pytest.mark.funcarg(scope="session", params=[1,2]) + def arg(request): + return request.param + + @pytest.mark.setup(scope="function") + def append(request, arg): + if request.function.__name__ == "test_some": + l.append(arg) + + def test_some(): + pass + + def test_result(arg): + assert len(l) == 2 + assert l == [1,2] + """) + reprec = testdir.inline_run("-s") + reprec.assertoutcome(passed=4) + class TestFuncargMarker: def test_parametrize(self, testdir): testdir.makepyfile(""" import pytest @pytest.mark.funcarg(params=["a", "b", "c"]) - def pytest_funcarg__arg(request): + def arg(request): return request.param l = [] def test_param(arg): @@ -1767,7 +1876,7 @@ import pytest l = [] @pytest.mark.funcarg(scope="module") - def pytest_funcarg__arg(request): + def arg(request): l.append(1) return 1 @@ -1789,7 +1898,7 @@ import pytest l = [] @pytest.mark.funcarg(scope="module") - def pytest_funcarg__arg(request): + def arg(request): l.append(1) return 1 @@ -1812,7 +1921,7 @@ finalized = [] created = [] @pytest.mark.funcarg(scope="module") - def pytest_funcarg__arg(request): + def arg(request): created.append(1) assert request.scope == "module" request.addfinalizer(lambda: finalized.append(1)) @@ -1851,14 +1960,14 @@ finalized = [] created = [] @pytest.mark.funcarg(scope="function") - def pytest_funcarg__arg(request): + def arg(request): pass """) testdir.makepyfile( test_mod1=""" import pytest @pytest.mark.funcarg(scope="session") - def pytest_funcarg__arg(request): + def arg(request): %s def test_1(arg): pass @@ -1894,7 +2003,7 @@ testdir.makepyfile(""" import pytest @pytest.mark.funcarg(scope="module", params=["a", "b", "c"]) - def pytest_funcarg__arg(request): + def arg(request): return request.param l = [] def test_param(arg): 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.