diff --git a/CHANGELOG b/CHANGELOG index 87f46fff8..20a86f0f1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,10 +1,10 @@ 2.7.0.dev (compared to 2.6.4) ----------------------------- -- fix issue616: conftest.py files and their contained fixutres are now +- fix issue616: conftest.py files and their contained fixutres are now properly considered for visibility, independently from the exact current working directory and test arguments that are used. - Many thanks to Eric Siegerman and his PR235 which contains + Many thanks to Eric Siegerman and his PR235 which contains systematic tests for conftest visibility and now passes. This change also introduces the concept of a ``rootdir`` which is printed as a new pytest header and documented in the pytest @@ -12,7 +12,7 @@ - change reporting of "diverted" tests, i.e. tests that are collected in one file but actually come from another (e.g. when tests in a test class - come from a base class in a different file). We now show the nodeid + come from a base class in a different file). We now show the nodeid and indicate via a postfix the other file. - add ability to set command line options by environment variable PYTEST_ADDOPTS. @@ -24,7 +24,7 @@ - fix issue650: new option ``--docttest-ignore-import-errors`` which will turn import errors in doctests into skips. Thanks Charles Cloud for the complete PR. - + - fix issue655: work around different ways that cause python2/3 to leak sys.exc_info into fixtures/tests causing failures in 3rd party code @@ -55,6 +55,7 @@ - "python_classes" and "python_functions" options now support glob-patterns for test discovery, as discussed in issue600. Thanks Ldiary Translations. +- allow to override parametrized fixtures with non-parametrized ones and vice versa (bubenkoff). 2.6.4 ---------- diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst index 8dec1f041..6a56d74b2 100644 --- a/CONTRIBUTING.rst +++ b/CONTRIBUTING.rst @@ -30,8 +30,8 @@ You can submit your plugin by subscribing to the `pytest-dev mail list mail pointing to your existing pytest plugin repository which must have the following: -- PyPI presence with a ``setup.py`` that contains a license, ``pytest-`` - prefixed, version number, authors, short and long description. +- PyPI presence with a ``setup.py`` that contains a license, ``pytest-`` + prefixed, version number, authors, short and long description. - a ``tox.ini`` for running tests using `tox `_. @@ -43,7 +43,7 @@ the following: If no contributor strongly objects and two agree, the repo will be transferred to the ``pytest-dev`` organisation and you'll become a -member of the ``pytest-dev`` team, with commit rights to all projects. +member of the ``pytest-dev`` team, with commit rights to all projects. We recommend that each plugin has at least three people who have the right to release to pypi. @@ -128,22 +128,18 @@ Preparing Pull Requests on Bitbucket The primary development platform for pytest is BitBucket. You can find all the issues there and submit your pull requests. -1. Fork the +#. Fork the `pytest BitBucket repository `__. It's fine to use ``pytest`` as your fork repository name because it will live under your user. -.. _virtualenvactivate: +#. Create a development environment + (will implicitly use http://www.virtualenv.org/en/latest/):: -2. Create and activate a fork-specific virtualenv - (http://www.virtualenv.org/en/latest/):: + $ make develop + $ source .env/bin/activate - $ virtualenv pytest-venv - $ source pytest-venv/bin/activate - -.. _checkout: - -3. Clone your fork locally using `Mercurial `_ +#. Clone your fork locally using `Mercurial `_ (``hg``) and create a branch:: $ hg clone ssh://hg@bitbucket.org/YOUR_BITBUCKET_USERNAME/pytest @@ -153,45 +149,46 @@ the issues there and submit your pull requests. If you need some help with Mercurial, follow this quick start guide: http://mercurial.selenic.com/wiki/QuickStart -.. _testing-pytest: +#. Create a development environment + (will implicitly use http://www.virtualenv.org/en/latest/):: -4. You can now edit your local working copy. To test you need to - install the "tox" tool into your virtualenv:: + $ make develop + $ source .env/bin/activate - $ pip install tox +#. You can now edit your local working copy. - You need to have Python 2.7 and 3.3 available in your system. Now - running tests is as simple as issuing this command:: + You need to have Python 2.7 and 3.4 available in your system. Now + running tests is as simple as issuing this command:: - $ python runtox.py -e py27,py33,flakes + $ python runtox.py -e py27,py34,flakes - This command will run tests via the "tox" tool against Python 2.7 and 3.3 - and also perform "flakes" coding-style checks. ``runtox.py`` is - a thin wrapper around ``tox`` which installs from a development package - index where newer (not yet released to pypi) versions of dependencies - (especially ``py``) might be present. + This command will run tests via the "tox" tool against Python 2.7 and 3.4 + and also perform "flakes" coding-style checks. ``runtox.py`` is + a thin wrapper around ``tox`` which installs from a development package + index where newer (not yet released to pypi) versions of dependencies + (especially ``py``) might be present. - To run tests on py27 and pass options (e.g. enter pdb on failure) - to pytest you can do:: + To run tests on py27 and pass options (e.g. enter pdb on failure) + to pytest you can do:: $ python runtox.py -e py27 -- --pdb - or to only run tests in a particular test module on py33:: + or to only run tests in a particular test module on py34:: - $ python runtox.py -e py33 -- testing/test_config.py + $ python runtox.py -e py34 -- testing/test_config.py -5. Commit and push once your tests pass and you are happy with your change(s):: +#. Commit and push once your tests pass and you are happy with your change(s):: $ hg commit -m"" $ hg push -b . -6. Finally, submit a pull request through the BitBucket website: +#. Finally, submit a pull request through the BitBucket website: - .. image:: img/pullrequest.png - :width: 700px - :align: center + .. image:: img/pullrequest.png + :width: 700px + :align: center - :: + :: source: YOUR_BITBUCKET_USERNAME/pytest branch: your-branch-name @@ -214,5 +211,3 @@ original repository. If you insist on using git with bitbucket/hg you may try `gitifyhg `_ but are on your own and need to submit pull requests through the respective platform, nevertheless. - - diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..ddf287418 --- /dev/null +++ b/Makefile @@ -0,0 +1,25 @@ +# Set of targets useful for development/release process +PYTHON = python2.7 +PATH := $(PWD)/.env/bin:$(PATH) + +# prepare virtual python environment +.env: + virtualenv .env -p $(PYTHON) + +# install all needed for development +develop: .env + pip install -e . tox -r requirements-docs.txt + +# clean the development envrironment +clean: + -rm -rf .env + +# generate documentation +docs: develop + find doc/en -name '*.txt' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc + cd doc/en; make html + +# upload documentation +upload-docs: develop + find doc/en -name '*.txt' -not -path 'doc/en/_build/*' | xargs .env/bin/regendoc --update + cd doc/en; make install diff --git a/_pytest/python.py b/_pytest/python.py index 8b32ccee1..318038771 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1712,13 +1712,17 @@ class FixtureManager: def pytest_generate_tests(self, metafunc): for argname in metafunc.fixturenames: faclist = metafunc._arg2fixturedefs.get(argname) - if faclist is None: - continue # will raise FixtureLookupError at setup time - for fixturedef in faclist: + if faclist: + fixturedef = faclist[-1] if fixturedef.params is not None: - metafunc.parametrize(argname, fixturedef.params, - indirect=True, scope=fixturedef.scope, - ids=fixturedef.ids) + func_params = getattr(getattr(metafunc.function, 'parametrize', None), 'args', [[None]]) + # skip directly parametrized arguments + if argname not in func_params and argname not in func_params[0]: + metafunc.parametrize(argname, fixturedef.params, + indirect=True, scope=fixturedef.scope, + ids=fixturedef.ids) + else: + continue # will raise FixtureLookupError at setup time def pytest_collection_modifyitems(self, items): # separate parametrized setups diff --git a/doc/en/fixture.txt b/doc/en/fixture.txt index 0de85a7c7..f2982dc9a 100644 --- a/doc/en/fixture.txt +++ b/doc/en/fixture.txt @@ -78,20 +78,20 @@ marked ``smtp`` fixture function. Running the test looks like this:: =========================== test session starts ============================ platform linux -- Python 3.4.0 -- py-1.4.26 -- pytest-2.6.4 collected 1 items - + test_smtpsimple.py F - + ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - + smtp = - + def test_ehlo(smtp): response, msg = smtp.ehlo() assert response == 250 > assert "merlinux" in msg E TypeError: Type str doesn't support the buffer API - + test_smtpsimple.py:11: TypeError ========================= 1 failed in 0.28 seconds ========================= @@ -195,31 +195,31 @@ inspect what is going on and can now run the tests:: =========================== test session starts ============================ platform linux -- Python 3.4.0 -- py-1.4.26 -- pytest-2.6.4 collected 2 items - + test_module.py FF - + ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - + smtp = - + def test_ehlo(smtp): response = smtp.ehlo() assert response[0] == 250 > assert "merlinux" in response[1] E TypeError: Type str doesn't support the buffer API - + test_module.py:5: TypeError ________________________________ 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:11: AssertionError ========================= 2 failed in 0.28 seconds ========================= @@ -268,7 +268,7 @@ Let's execute it:: $ py.test -s -q --tb=no FFteardown smtp - + 2 failed in 0.21 seconds We see that the ``smtp`` instance is finalized after the two @@ -377,50 +377,50 @@ So let's just do another run:: FFFF ================================= FAILURES ================================= __________________________ test_ehlo[merlinux.eu] __________________________ - + smtp = - + def test_ehlo(smtp): response = smtp.ehlo() assert response[0] == 250 > assert "merlinux" in response[1] E TypeError: Type str doesn't support the buffer API - + test_module.py:5: TypeError __________________________ 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:11: AssertionError ________________________ test_ehlo[mail.python.org] ________________________ - + smtp = - + def test_ehlo(smtp): response = smtp.ehlo() assert response[0] == 250 > assert "merlinux" in response[1] E TypeError: Type str doesn't support the buffer API - + test_module.py:5: TypeError -------------------------- Captured stdout setup --------------------------- finalizing ________________________ 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:11: AssertionError 4 failed in 7.02 seconds @@ -519,10 +519,10 @@ Here we declare an ``app`` fixture which receives the previously defined =========================== test session starts ============================ platform linux -- Python 3.4.0 -- py-1.4.26 -- pytest-2.6.4 -- /home/hpk/p/pytest/.tox/regen/bin/python3.4 collecting ... collected 2 items - + test_appsetup.py::test_smtp_exists[merlinux.eu] PASSED test_appsetup.py::test_smtp_exists[mail.python.org] PASSED - + ========================= 2 passed in 6.63 seconds ========================= Due to the parametrization of ``smtp`` the test will run twice with two @@ -583,7 +583,7 @@ Let's run the tests in verbose mode and with looking at the print-output:: =========================== test session starts ============================ platform linux -- Python 3.4.0 -- py-1.4.26 -- pytest-2.6.4 -- /home/hpk/p/pytest/.tox/regen/bin/python3.4 collecting ... collected 8 items - + test_module.py::test_0[1] test0 1 PASSED test_module.py::test_0[2] test0 2 @@ -602,7 +602,7 @@ Let's run the tests in verbose mode and with looking at the print-output:: PASSED test_module.py::test_2[2-mod2] test2 2 mod2 PASSED - + ========================= 8 passed in 0.01 seconds ========================= You can see that the parametrized module-scoped ``modarg`` resource caused @@ -780,4 +780,182 @@ to a :ref:`conftest.py ` file or even separately installable fixtures functions starts at test classes, then test modules, then ``conftest.py`` files and finally builtin and third party plugins. +Overriding fixtures on various levels +------------------------------------- +In relatively large test suite, you most likely need to ``override`` a ``global`` or ``root`` fixture with a ``locally`` +defined one, keeping the test code readable and maintainable. + +Override a fixture on a folder (conftest) level +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Given the tests file structure is: + +:: + + tests/ + __init__.py + + conftest.py + # content of tests/conftest.py + import pytest + + @pytest.fixture + def username(): + return 'username' + + test_something.py + # content of tests/test_something.py + def test_username(username): + assert username == 'username' + + subfolder/ + __init__.py + + conftest.py + # content of tests/subfolder/conftest.py + import pytest + + @pytest.fixture + def username(username): + return 'overridden-' + username + + test_something.py + # content of tests/subfolder/test_something.py + def test_username(username): + assert username == 'overridden-username' + +As you can see, a fixture with the same name can be overridden for certain test folder level. +Note that the ``base`` or ``super`` fixture can be accessed from the ``overriding`` +fixture easily - used in the example above. + +Override a fixture on a test module level +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Given the tests file structure is: + +:: + + tests/ + __init__.py + + conftest.py + # content of tests/conftest.py + @pytest.fixture + def username(): + return 'username' + + test_something.py + # content of tests/test_something.py + import pytest + + @pytest.fixture + def username(username): + return 'overridden-' + username + + def test_username(username): + assert username == 'overridden-username' + + test_something_else.py + # content of tests/test_something_else.py + import pytest + + @pytest.fixture + def username(username): + return 'overridden-else-' + username + + def test_username(username): + assert username == 'overridden-else-username' + +In the example above, a fixture with the same name can be overridden for certain test module. + + +Override a fixture with direct test parametrization +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Given the tests file structure is: + +:: + + tests/ + __init__.py + + conftest.py + # content of tests/conftest.py + import pytest + + @pytest.fixture + def username(): + return 'username' + + @pytest.fixture + def other_username(username): + return 'other-' + username + + test_something.py + # content of tests/test_something.py + import pytest + + @pytest.mark.parametrize('username', ['directly-overridden-username']) + def test_username(username): + assert username == 'directly-overridden-username' + + @pytest.mark.parametrize('username', ['directly-overridden-username-other']) + def test_username_other(other_username): + assert username == 'other-directly-overridden-username-other' + +In the example above, a fixture value is overridden by the test parameter value. Note that the value of the fixture +can be overridden this way even if the test doesn't use it directly (doesn't mention it in the function prototype). + + +Override a parametrized fixture with non-parametrized one and vice versa +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Given the tests file structure is: + +:: + + tests/ + __init__.py + + conftest.py + # content of tests/conftest.py + import pytest + + @pytest.fixture(params=['one', 'two', 'three']) + def parametrized_username(request): + return request.param + + @pytest.fixture + def non_parametrized_username(request): + return 'username' + + test_something.py + # content of tests/test_something.py + import pytest + + @pytest.fixture + def parametrized_username(): + return 'overridden-username' + + @pytest.fixture(params=['one', 'two', 'three']) + def non_parametrized_username(request): + return request.param + + def test_username(parametrized_username): + assert parametrized_username == 'overridden-username' + + def test_parametrized_username(non_parametrized_username): + assert non_parametrized_username in ['one', 'two', 'three'] + + test_something_else.py + # content of tests/test_something_else.py + def test_username(parametrized_username): + assert parametrized_username in ['one', 'two', 'three'] + + def test_username(non_parametrized_username): + assert non_parametrized_username == 'username' + +In the example above, a parametrized fixture is overridden with a non-parametrized version, and +a non-parametrized fixture is overridden with a parametrized version for certain test module. +The same applies for the test folder level obviously. diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 000000000..9a7a1049f --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,2 @@ +sphinx==1.2.3 +hg+ssh://hg@bitbucket.org/RonnyPfannschmidt/regendoc#egg=regendoc diff --git a/setup.py b/setup.py index 7a1cc281e..b643e7ca7 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,8 @@ classifiers = ['Development Status :: 6 - Mature', ('Programming Language :: Python :: %s' % x) for x in '2 2.6 2.7 3 3.2 3.3 3.4'.split()] -long_description = open('README.rst').read() +with open('README.rst') as fd: + long_description = fd.read() def main(): diff --git a/testing/python/collect.py b/testing/python/collect.py index 15ff2b62b..bdea33a7f 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -393,14 +393,31 @@ class TestFunction: return 'value' @pytest.mark.parametrize('value', - ['overrided']) - def test_overrided_via_param(value): - assert value == 'overrided' + ['overridden']) + def test_overridden_via_param(value): + assert value == 'overridden' """) rec = testdir.inline_run() rec.assertoutcome(passed=1) + def test_parametrize_overrides_parametrized_fixture(self, testdir): + """Test parametrization when parameter overrides existing parametrized fixture with same name.""" + testdir.makepyfile(""" + import pytest + + @pytest.fixture(params=[1, 2]) + def value(request): + return request.param + + @pytest.mark.parametrize('value', + ['overridden']) + def test_overridden_via_param(value): + assert value == 'overridden' + """) + rec = testdir.inline_run() + rec.assertoutcome(passed=1) + def test_parametrize_with_mark(selfself, testdir): items = testdir.getitems(""" import pytest diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 4d27fb1fa..3e7ef8c4f 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -226,6 +226,114 @@ class TestFillFixtures: result = testdir.runpytest() assert result.ret == 0 + def test_override_parametrized_fixture_conftest_module(self, testdir): + """Test override of the parametrized fixture with non-parametrized one on the test module level.""" + testdir.makeconftest(""" + import pytest + + @pytest.fixture(params=[1, 2, 3]) + def spam(request): + return request.param + """) + testfile = testdir.makepyfile(""" + import pytest + + @pytest.fixture + def spam(): + return 'spam' + + def test_spam(spam): + assert spam == 'spam' + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*1 passed*"]) + result = testdir.runpytest(testfile) + result.stdout.fnmatch_lines(["*1 passed*"]) + + def test_override_parametrized_fixture_conftest_conftest(self, testdir): + """Test override of the parametrized fixture with non-parametrized one on the conftest level.""" + testdir.makeconftest(""" + import pytest + + @pytest.fixture(params=[1, 2, 3]) + def spam(request): + return request.param + """) + subdir = testdir.mkpydir('subdir') + subdir.join("conftest.py").write(py.code.Source(""" + import pytest + + @pytest.fixture + def spam(): + return 'spam' + """)) + testfile = subdir.join("test_spam.py") + testfile.write(py.code.Source(""" + def test_spam(spam): + assert spam == "spam" + """)) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*1 passed*"]) + result = testdir.runpytest(testfile) + result.stdout.fnmatch_lines(["*1 passed*"]) + + def test_override_non_parametrized_fixture_conftest_module(self, testdir): + """Test override of the non-parametrized fixture with parametrized one on the test module level.""" + testdir.makeconftest(""" + import pytest + + @pytest.fixture + def spam(): + return 'spam' + """) + testfile = testdir.makepyfile(""" + import pytest + + @pytest.fixture(params=[1, 2, 3]) + def spam(request): + return request.param + + params = {'spam': 1} + + def test_spam(spam): + assert spam == params['spam'] + params['spam'] += 1 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*3 passed*"]) + result = testdir.runpytest(testfile) + result.stdout.fnmatch_lines(["*3 passed*"]) + + def test_override_non_parametrized_fixture_conftest_conftest(self, testdir): + """Test override of the non-parametrized fixture with parametrized one on the conftest level.""" + testdir.makeconftest(""" + import pytest + + @pytest.fixture + def spam(): + return 'spam' + """) + subdir = testdir.mkpydir('subdir') + subdir.join("conftest.py").write(py.code.Source(""" + import pytest + + @pytest.fixture(params=[1, 2, 3]) + def spam(request): + return request.param + """)) + testfile = subdir.join("test_spam.py") + testfile.write(py.code.Source(""" + params = {'spam': 1} + + def test_spam(spam): + assert spam == params['spam'] + params['spam'] += 1 + """)) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*3 passed*"]) + result = testdir.runpytest(testfile) + result.stdout.fnmatch_lines(["*3 passed*"]) + def test_autouse_fixture_plugin(self, testdir): # A fixture from a plugin has no baseid set, which screwed up # the autouse fixture handling. diff --git a/tox.ini b/tox.ini index 232ac951e..827125dfb 100644 --- a/tox.ini +++ b/tox.ini @@ -136,7 +136,7 @@ commands= minversion=2.0 plugins=pytester #--pyargs --doctest-modules --ignore=.tox -addopts= -rxsX +addopts= -rxsX -vl rsyncdirs=tox.ini pytest.py _pytest testing python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance