diff --git a/AUTHORS b/AUTHORS index ea3b7f50e..34c6f6437 100644 --- a/AUTHORS +++ b/AUTHORS @@ -47,6 +47,7 @@ Dave Hunt David Díaz-Barquero David Mohr David Vierra +Daw-Ran Liou Denis Kirisov Diego Russo Dmitry Dygalo diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 6800f82e6..34fc0c8f6 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -591,23 +591,26 @@ class AssertionRewriter(ast.NodeVisitor): # docstrings and __future__ imports. aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"), ast.alias("_pytest.assertion.rewrite", "@pytest_ar")] - expect_docstring = True + doc = getattr(mod, "docstring", None) + expect_docstring = doc is None + if doc is not None and self.is_rewrite_disabled(doc): + return pos = 0 - lineno = 0 + lineno = 1 for item in mod.body: if (expect_docstring and isinstance(item, ast.Expr) and isinstance(item.value, ast.Str)): doc = item.value.s - if "PYTEST_DONT_REWRITE" in doc: - # The module has disabled assertion rewriting. + if self.is_rewrite_disabled(doc): return - lineno += len(doc) - 1 expect_docstring = False elif (not isinstance(item, ast.ImportFrom) or item.level > 0 or item.module != "__future__"): lineno = item.lineno break pos += 1 + else: + lineno = item.lineno imports = [ast.Import([alias], lineno=lineno, col_offset=0) for alias in aliases] mod.body[pos:pos] = imports @@ -633,6 +636,9 @@ class AssertionRewriter(ast.NodeVisitor): not isinstance(field, ast.expr)): nodes.append(field) + def is_rewrite_disabled(self, docstring): + return "PYTEST_DONT_REWRITE" in docstring + def variable(self): """Get a new variable.""" # Use a character invalid in python identifiers to avoid clashing. diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 6016265a5..bba90e551 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -127,7 +127,7 @@ class DoctestItem(pytest.Item): lines = ["%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines)] # trim docstring error lines to 10 - lines = lines[example.lineno - 9:example.lineno + 1] + lines = lines[max(example.lineno - 9, 0):example.lineno + 1] else: lines = ['EXAMPLE LOCATION UNKNOWN, not showing all tests of that example'] indent = '>>>' diff --git a/_pytest/mark.py b/_pytest/mark.py index 03b058d95..8879b9fff 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -268,8 +268,9 @@ class MarkGenerator: pass self._markers = l = set() for line in self._config.getini("markers"): - beginning = line.split(":", 1) - x = beginning[0].split("(", 1)[0] + marker, _ = line.split(":", 1) + marker = marker.rstrip() + x = marker.split("(", 1)[0] l.add(x) if name not in self._markers: raise AttributeError("%r not a registered marker" % (name,)) diff --git a/changelog/1505.doc b/changelog/1505.doc new file mode 100644 index 000000000..1b303d1bd --- /dev/null +++ b/changelog/1505.doc @@ -0,0 +1 @@ +Introduce a dedicated section about conftest.py. diff --git a/changelog/2658.doc b/changelog/2658.doc new file mode 100644 index 000000000..2da7f3d6c --- /dev/null +++ b/changelog/2658.doc @@ -0,0 +1 @@ +Append example for pytest.param in the example/parametrize document. \ No newline at end of file diff --git a/changelog/2856.bugfix b/changelog/2856.bugfix new file mode 100644 index 000000000..7e5fc8fc7 --- /dev/null +++ b/changelog/2856.bugfix @@ -0,0 +1 @@ +Strip whitespace from marker names when reading them from INI config. diff --git a/changelog/2882.bugfix b/changelog/2882.bugfix new file mode 100644 index 000000000..2bda24c01 --- /dev/null +++ b/changelog/2882.bugfix @@ -0,0 +1 @@ +Show full context of doctest source in the pytest output, if the lineno of failed example in the docstring is < 9. \ No newline at end of file diff --git a/doc/en/assert.rst b/doc/en/assert.rst index a8ddaecd8..d9e044356 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -209,8 +209,8 @@ the ``pytest_assertrepr_compare`` hook. .. autofunction:: _pytest.hookspec.pytest_assertrepr_compare :noindex: -As an example consider adding the following hook in a conftest.py which -provides an alternative explanation for ``Foo`` objects:: +As an example consider adding the following hook in a :ref:`conftest.py ` +file which provides an alternative explanation for ``Foo`` objects:: # content of conftest.py from test_foocompare import Foo diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index ffeb5a951..1a8de235a 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -485,4 +485,54 @@ of our ``test_func1`` was skipped. A few notes: values as well. +Set marks or test ID for individual parametrized test +-------------------------------------------------------------------- +Use ``pytest.param`` to apply marks or set test ID to individual parametrized test. +For example:: + + # content of test_pytest_param_example.py + import pytest + @pytest.mark.parametrize('test_input,expected', [ + ('3+5', 8), + pytest.param('1+7', 8, + marks=pytest.mark.basic), + pytest.param('2+4', 6, + marks=pytest.mark.basic, + id='basic_2+4'), + pytest.param('6*9', 42, + marks=[pytest.mark.basic, pytest.mark.xfail], + id='basic_6*9'), + ]) + def test_eval(test_input, expected): + assert eval(test_input) == expected + +In this example, we have 4 parametrized tests. Except for the first test, +we mark the rest three parametrized tests with the custom marker ``basic``, +and for the fourth test we also use the built-in mark ``xfail`` to indicate this +test is expected to fail. For explicitness, we set test ids for some tests. + +Then run ``pytest`` with verbose mode and with only the ``basic`` marker:: + + pytest -v -m basic + ============================================ test session starts ============================================= + platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y + rootdir: $REGENDOC_TMPDIR, inifile: + collected 4 items + + test_pytest_param_example.py::test_eval[1+7-8] PASSED + test_pytest_param_example.py::test_eval[basic_2+4] PASSED + test_pytest_param_example.py::test_eval[basic_6*9] xfail + ========================================== short test summary info =========================================== + XFAIL test_pytest_param_example.py::test_eval[basic_6*9] + + ============================================= 1 tests deselected ============================================= + +As the result: + +- Four tests were collected +- One test was deselected because it doesn't have the ``basic`` mark. +- Three tests with the ``basic`` mark was selected. +- The test ``test_eval[1+7-8]`` passed, but the name is autogenerated and confusing. +- The test ``test_eval[basic_2+4]`` passed. +- The test ``test_eval[basic_6*9]`` was expected to fail and did fail. diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index 8d36c2e37..5fb63035a 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -175,21 +175,23 @@ You can always peek at the collection tree without running tests like this:: ======= no tests ran in 0.12 seconds ======== -customizing test collection to find all .py files ---------------------------------------------------------- +.. _customizing-test-collection: + +Customizing test collection +--------------------------- .. regendoc:wipe -You can easily instruct ``pytest`` to discover tests from every python file:: - +You can easily instruct ``pytest`` to discover tests from every Python file:: # content of pytest.ini [pytest] python_files = *.py -However, many projects will have a ``setup.py`` which they don't want to be imported. Moreover, there may files only importable by a specific python version. -For such cases you can dynamically define files to be ignored by listing -them in a ``conftest.py`` file:: +However, many projects will have a ``setup.py`` which they don't want to be +imported. Moreover, there may files only importable by a specific python +version. For such cases you can dynamically define files to be ignored by +listing them in a ``conftest.py`` file:: # content of conftest.py import sys @@ -198,7 +200,7 @@ them in a ``conftest.py`` file:: if sys.version_info[0] > 2: collect_ignore.append("pkg/module_py2.py") -And then if you have a module file like this:: +and then if you have a module file like this:: # content of pkg/module_py2.py def test_only_on_python2(): @@ -207,13 +209,13 @@ And then if you have a module file like this:: except Exception, e: pass -and a setup.py dummy file like this:: +and a ``setup.py`` dummy file like this:: # content of setup.py 0/0 # will raise exception if imported -then a pytest run on Python2 will find the one test and will leave out the -setup.py file:: +If you run with a Python 2 interpreter then you will find the one test and will +leave out the ``setup.py`` file:: #$ pytest --collect-only ====== test session starts ====== @@ -225,13 +227,13 @@ setup.py file:: ====== no tests ran in 0.04 seconds ====== -If you run with a Python3 interpreter both the one test and the setup.py file -will be left out:: +If you run with a Python 3 interpreter both the one test and the ``setup.py`` +file will be left out:: $ pytest --collect-only ======= test session starts ======== platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items - + ======= no tests ran in 0.12 seconds ======== diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index dace0514e..1d7ba8640 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -127,10 +127,39 @@ It's a prime example of `dependency injection`_ where fixture functions take the role of the *injector* and test functions are the *consumers* of fixture objects. +.. _`conftest.py`: +.. _`conftest`: + +``conftest.py``: sharing fixture functions +------------------------------------------ + +If during implementing your tests you realize that you +want to use a fixture function from multiple test files you can move it +to a ``conftest.py`` file. +You don't need to import the fixture you want to use in a test, it +automatically gets discovered by pytest. The discovery of +fixture functions starts at test classes, then test modules, then +``conftest.py`` files and finally builtin and third party plugins. + +You can also use the ``conftest.py`` file to implement +:ref:`local per-directory plugins `. + +Sharing test data +----------------- + +If you want to make test data from files available to your tests, a good way +to do this is by loading these data in a fixture for use by your tests. +This makes use of the automatic caching mechanisms of pytest. + +Another good approach is by adding the data files in the ``tests`` folder. +There are also community plugins available to help managing this aspect of +testing, e.g. `pytest-datadir `__ +and `pytest-datafiles `__. + .. _smtpshared: -Scope: Sharing a fixture across tests in a class, module or session -------------------------------------------------------------------- +Scope: sharing a fixture instance across tests in a class, module or session +---------------------------------------------------------------------------- .. regendoc:wipe @@ -878,17 +907,6 @@ All test methods in this TestClass will use the transaction fixture while other test classes or functions in the module will not use it unless they also add a ``transact`` reference. - -Shifting (visibility of) fixture functions ----------------------------------------------------- - -If during implementing your tests you realize that you -want to use a fixture function from multiple test files you can move it -to a :ref:`conftest.py ` file or even separately installable -:ref:`plugins ` without changing test code. The discovery of -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 ------------------------------------- diff --git a/doc/en/plugins.rst b/doc/en/plugins.rst index bba7d3ecd..400418aee 100644 --- a/doc/en/plugins.rst +++ b/doc/en/plugins.rst @@ -91,7 +91,7 @@ environment you can type:: and will get an extended test header which shows activated plugins and their names. It will also print local plugins aka -:ref:`conftest.py ` files when they are loaded. +:ref:`conftest.py ` files when they are loaded. .. _`cmdunregister`: @@ -152,4 +152,3 @@ in the `pytest repository `_. _pytest.terminal _pytest.tmpdir _pytest.unittest - diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index dbe9c7f8d..3159d2083 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -3,7 +3,7 @@ .. _skipping: Skip and xfail: dealing with tests that cannot succeed -===================================================================== +====================================================== You can mark test functions that cannot be run on certain platforms or that you expect to fail so pytest can deal with them accordingly and @@ -152,6 +152,16 @@ will be skipped if any of the skip conditions is true. .. _`whole class- or module level`: mark.html#scoped-marking +Skipping files or directories +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sometimes you may need to skip an entire file or directory, for example if the +tests rely on Python version-specific features or contain code that you do not +wish pytest to run. In this case, you must exclude the files and directories +from collection. Refer to :ref:`customizing-test-collection` for more +information. + + Skipping on a missing import dependency ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 5f151b4bb..53a14cb0d 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -57,9 +57,7 @@ Plugin discovery order at tool startup .. _`pytest/plugin`: http://bitbucket.org/pytest-dev/pytest/src/tip/pytest/plugin/ .. _`conftest.py plugins`: -.. _`conftest.py`: .. _`localplugin`: -.. _`conftest`: .. _`local conftest plugins`: conftest.py: local per-directory plugins diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 07912f0b7..467f231a6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -65,13 +65,18 @@ class TestAssertionRewrite(object): def test_place_initial_imports(self): s = """'Doc string'\nother = stuff""" m = rewrite(s) - assert isinstance(m.body[0], ast.Expr) - assert isinstance(m.body[0].value, ast.Str) - for imp in m.body[1:3]: + # Module docstrings in 3.7 are part of Module node, it's not in the body + # so we remove it so the following body items have the same indexes on + # all Python versions + if sys.version_info < (3, 7): + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + del m.body[0] + for imp in m.body[0:2]: assert isinstance(imp, ast.Import) assert imp.lineno == 2 assert imp.col_offset == 0 - assert isinstance(m.body[3], ast.Assign) + assert isinstance(m.body[2], ast.Assign) s = """from __future__ import with_statement\nother_stuff""" m = rewrite(s) assert isinstance(m.body[0], ast.ImportFrom) @@ -80,16 +85,29 @@ class TestAssertionRewrite(object): assert imp.lineno == 2 assert imp.col_offset == 0 assert isinstance(m.body[3], ast.Expr) + s = """'doc string'\nfrom __future__ import with_statement""" + m = rewrite(s) + if sys.version_info < (3, 7): + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + del m.body[0] + assert isinstance(m.body[0], ast.ImportFrom) + for imp in m.body[1:3]: + assert isinstance(imp, ast.Import) + assert imp.lineno == 2 + assert imp.col_offset == 0 s = """'doc string'\nfrom __future__ import with_statement\nother""" m = rewrite(s) - assert isinstance(m.body[0], ast.Expr) - assert isinstance(m.body[0].value, ast.Str) - assert isinstance(m.body[1], ast.ImportFrom) - for imp in m.body[2:4]: + if sys.version_info < (3, 7): + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + del m.body[0] + assert isinstance(m.body[0], ast.ImportFrom) + for imp in m.body[1:3]: assert isinstance(imp, ast.Import) assert imp.lineno == 3 assert imp.col_offset == 0 - assert isinstance(m.body[4], ast.Expr) + assert isinstance(m.body[3], ast.Expr) s = """from . import relative\nother_stuff""" m = rewrite(s) for imp in m.body[0:2]: @@ -101,10 +119,14 @@ class TestAssertionRewrite(object): def test_dont_rewrite(self): s = """'PYTEST_DONT_REWRITE'\nassert 14""" m = rewrite(s) - assert len(m.body) == 2 - assert isinstance(m.body[0].value, ast.Str) - assert isinstance(m.body[1], ast.Assert) - assert m.body[1].msg is None + if sys.version_info < (3, 7): + assert len(m.body) == 2 + assert isinstance(m.body[0], ast.Expr) + assert isinstance(m.body[0].value, ast.Str) + del m.body[0] + else: + assert len(m.body) == 1 + assert m.body[0].msg is None def test_name(self): def f(): diff --git a/testing/test_doctest.py b/testing/test_doctest.py index b8fa1fb77..b15067f15 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -173,7 +173,7 @@ class TestDoctests(object): "*UNEXPECTED*ZeroDivision*", ]) - def test_docstring_context_around_error(self, testdir): + def test_docstring_partial_context_around_error(self, testdir): """Test that we show some context before the actual line of a failing doctest. """ @@ -199,7 +199,7 @@ class TestDoctests(object): ''') result = testdir.runpytest('--doctest-modules') result.stdout.fnmatch_lines([ - '*docstring_context_around_error*', + '*docstring_partial_context_around_error*', '005*text-line-3', '006*text-line-4', '013*text-line-11', @@ -213,6 +213,32 @@ class TestDoctests(object): assert 'text-line-2' not in result.stdout.str() assert 'text-line-after' not in result.stdout.str() + def test_docstring_full_context_around_error(self, testdir): + """Test that we show the whole context before the actual line of a failing + doctest, provided that the context is up to 10 lines long. + """ + testdir.makepyfile(''' + def foo(): + """ + text-line-1 + text-line-2 + + >>> 1 + 1 + 3 + """ + ''') + result = testdir.runpytest('--doctest-modules') + result.stdout.fnmatch_lines([ + '*docstring_full_context_around_error*', + '003*text-line-1', + '004*text-line-2', + '006*>>> 1 + 1', + 'Expected:', + ' 3', + 'Got:', + ' 2', + ]) + def test_doctest_linedata_missing(self, testdir): testdir.tmpdir.join('hello.py').write(_pytest._code.Source(""" class Fun(object): diff --git a/testing/test_mark.py b/testing/test_mark.py index dc51bbac0..9ae88a665 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -169,6 +169,23 @@ def test_markers_option(testdir): ]) +def test_ini_markers_whitespace(testdir): + testdir.makeini(""" + [pytest] + markers = + a1 : this is a whitespace marker + """) + testdir.makepyfile(""" + import pytest + + @pytest.mark.a1 + def test_markers(): + assert True + """) + rec = testdir.inline_run("--strict", "-m", "a1") + rec.assertoutcome(passed=1) + + def test_markers_option_with_plugin_in_current_dir(testdir): testdir.makeconftest('pytest_plugins = "flip_flop"') testdir.makepyfile(flip_flop="""\ diff --git a/tox.ini b/tox.ini index aaf39026b..09f59f5e3 100644 --- a/tox.ini +++ b/tox.ini @@ -54,8 +54,9 @@ deps = mock nose hypothesis>=3.5.2 +changedir=testing commands = - pytest -n1 -ra {posargs:testing} + pytest -n1 -ra {posargs:.} [testenv:py36-xdist] deps = {[testenv:py27-xdist]deps} @@ -81,10 +82,11 @@ deps = pytest-xdist>=1.13 hypothesis>=3.5.2 distribute = true +changedir=testing setenv = PYTHONDONTWRITEBYTECODE=1 commands = - pytest -n3 -ra {posargs:testing} + pytest -n3 -ra {posargs:.} [testenv:py27-trial] deps = twisted