diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 36ecf79b8..737c6b86b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,16 @@ exclude: doc/en/example/py2py3/test_py2.py repos: - repo: https://github.com/ambv/black - rev: 18.9b0 + rev: 19.3b0 hooks: - id: black args: [--safe, --quiet] language_version: python3 - repo: https://github.com/asottile/blacken-docs - rev: v0.3.0 + rev: v0.5.0 hooks: - id: blacken-docs - additional_dependencies: [black==18.9b0] + additional_dependencies: [black==19.3b0] language_version: python3 - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.1.0 @@ -22,22 +22,22 @@ repos: exclude: _pytest/debugging.py language_version: python3 - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.0 + rev: 3.7.7 hooks: - id: flake8 language_version: python3 - repo: https://github.com/asottile/reorder_python_imports - rev: v1.3.5 + rev: v1.4.0 hooks: - id: reorder-python-imports args: ['--application-directories=.:src'] - repo: https://github.com/asottile/pyupgrade - rev: v1.11.1 + rev: v1.15.0 hooks: - id: pyupgrade args: [--keep-percent-format] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.2.0 + rev: v1.3.0 hooks: - id: rst-backticks - repo: local diff --git a/bench/bench_argcomplete.py b/bench/bench_argcomplete.py index 2b30add08..335733df7 100644 --- a/bench/bench_argcomplete.py +++ b/bench/bench_argcomplete.py @@ -16,4 +16,4 @@ run = 'fc("/d")' if __name__ == "__main__": print(timeit.timeit(run, setup=setup % imports[0], number=count)) - print((timeit.timeit(run, setup=setup % imports[1], number=count))) + print(timeit.timeit(run, setup=setup % imports[1], number=count)) diff --git a/changelog/5092.bugfix.rst b/changelog/5092.bugfix.rst new file mode 100644 index 000000000..3fd29677c --- /dev/null +++ b/changelog/5092.bugfix.rst @@ -0,0 +1 @@ +Produce a warning when unknown keywords are passed to ``pytest.param(...)``. diff --git a/changelog/5098.bugfix.rst b/changelog/5098.bugfix.rst new file mode 100644 index 000000000..f0104b24e --- /dev/null +++ b/changelog/5098.bugfix.rst @@ -0,0 +1 @@ +Invalidate import caches with ``monkeypatch.syspath_prepend``, which is required with namespace packages being used. diff --git a/doc/en/assert.rst b/doc/en/assert.rst index 9b26308c6..67ecb5348 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -12,12 +12,15 @@ Asserting with the ``assert`` statement ``pytest`` allows you to use the standard python ``assert`` for verifying expectations and values in Python tests. For example, you can write the -following:: +following: + +.. code-block:: python # content of test_assert1.py def f(): return 3 + def test_function(): assert f() == 4 @@ -52,7 +55,9 @@ operators. (See :ref:`tbreportdemo`). This allows you to use the idiomatic python constructs without boilerplate code while not losing introspection information. -However, if you specify a message with the assertion like this:: +However, if you specify a message with the assertion like this: + +.. code-block:: python assert a % 2 == 0, "value was odd, should be even" @@ -67,22 +72,29 @@ Assertions about expected exceptions ------------------------------------------ In order to write assertions about raised exceptions, you can use -``pytest.raises`` as a context manager like this:: +``pytest.raises`` as a context manager like this: + +.. code-block:: python import pytest + def test_zero_division(): with pytest.raises(ZeroDivisionError): 1 / 0 -and if you need to have access to the actual exception info you may use:: +and if you need to have access to the actual exception info you may use: + +.. code-block:: python def test_recursion_depth(): with pytest.raises(RuntimeError) as excinfo: + def f(): f() + f() - assert 'maximum recursion' in str(excinfo.value) + assert "maximum recursion" in str(excinfo.value) ``excinfo`` is a ``ExceptionInfo`` instance, which is a wrapper around the actual exception raised. The main attributes of interest are @@ -90,15 +102,19 @@ the actual exception raised. The main attributes of interest are You can pass a ``match`` keyword parameter to the context-manager to test that a regular expression matches on the string representation of an exception -(similar to the ``TestCase.assertRaisesRegexp`` method from ``unittest``):: +(similar to the ``TestCase.assertRaisesRegexp`` method from ``unittest``): + +.. code-block:: python import pytest + def myfunc(): raise ValueError("Exception 123 raised") + def test_match(): - with pytest.raises(ValueError, match=r'.* 123 .*'): + with pytest.raises(ValueError, match=r".* 123 .*"): myfunc() The regexp parameter of the ``match`` method is matched with the ``re.search`` @@ -107,7 +123,9 @@ well. There's an alternate form of the ``pytest.raises`` function where you pass a function that will be executed with the given ``*args`` and ``**kwargs`` and -assert that the given exception is raised:: +assert that the given exception is raised: + +.. code-block:: python pytest.raises(ExpectedException, func, *args, **kwargs) @@ -116,7 +134,9 @@ exception* or *wrong exception*. Note that it is also possible to specify a "raises" argument to ``pytest.mark.xfail``, which checks that the test is failing in a more -specific way than just having any exception raised:: +specific way than just having any exception raised: + +.. code-block:: python @pytest.mark.xfail(raises=IndexError) def test_f(): @@ -148,10 +168,13 @@ Making use of context-sensitive comparisons .. versionadded:: 2.0 ``pytest`` has rich support for providing context-sensitive information -when it encounters comparisons. For example:: +when it encounters comparisons. For example: + +.. code-block:: python # content of test_assert2.py + def test_set_comparison(): set1 = set("1308") set2 = set("8035") @@ -205,16 +228,21 @@ the ``pytest_assertrepr_compare`` hook. :noindex: As an example consider adding the following hook in a :ref:`conftest.py ` -file which provides an alternative explanation for ``Foo`` objects:: +file which provides an alternative explanation for ``Foo`` objects: + +.. code-block:: python # content of conftest.py from test_foocompare import Foo + + def pytest_assertrepr_compare(op, left, right): if isinstance(left, Foo) and isinstance(right, Foo) and op == "==": - return ['Comparing Foo instances:', - ' vals: %s != %s' % (left.val, right.val)] + return ["Comparing Foo instances:", " vals: %s != %s" % (left.val, right.val)] -now, given this test module:: +now, given this test module: + +.. code-block:: python # content of test_foocompare.py class Foo(object): @@ -224,6 +252,7 @@ now, given this test module:: def __eq__(self, other): return self.val == other.val + def test_compare(): f1 = Foo(1) f2 = Foo(2) diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index d207ef7e2..f5a37bdb3 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -9,18 +9,28 @@ Here are some example using the :ref:`mark` mechanism. Marking test functions and selecting them for a run ---------------------------------------------------- -You can "mark" a test function with custom metadata like this:: +You can "mark" a test function with custom metadata like this: + +.. code-block:: python # content of test_server.py import pytest + + @pytest.mark.webtest def test_send_http(): - pass # perform some webtest test for your app + pass # perform some webtest test for your app + + def test_something_quick(): pass + + def test_another(): pass + + class TestClass(object): def test_method(self): pass @@ -257,14 +267,19 @@ Marking whole classes or modules ---------------------------------------------------- You may use ``pytest.mark`` decorators with classes to apply markers to all of -its test methods:: +its test methods: + +.. code-block:: python # content of test_mark_classlevel.py import pytest + + @pytest.mark.webtest class TestClass(object): def test_startup(self): pass + def test_startup_and_more(self): pass @@ -272,17 +287,23 @@ This is equivalent to directly applying the decorator to the two test functions. To remain backward-compatible with Python 2.4 you can also set a -``pytestmark`` attribute on a TestClass like this:: +``pytestmark`` attribute on a TestClass like this: + +.. code-block:: python import pytest + class TestClass(object): pytestmark = pytest.mark.webtest -or if you need to use multiple markers you can use a list:: +or if you need to use multiple markers you can use a list: + +.. code-block:: python import pytest + class TestClass(object): pytestmark = [pytest.mark.webtest, pytest.mark.slowtest] @@ -305,18 +326,19 @@ Marking individual tests when using parametrize When using parametrize, applying a mark will make it apply to each individual test. However it is also possible to -apply a marker to an individual test instance:: +apply a marker to an individual test instance: + +.. code-block:: python import pytest + @pytest.mark.foo - @pytest.mark.parametrize(("n", "expected"), [ - (1, 2), - pytest.param((1, 3), marks=pytest.mark.bar), - (2, 3), - ]) + @pytest.mark.parametrize( + ("n", "expected"), [(1, 2), pytest.param((1, 3), marks=pytest.mark.bar), (2, 3)] + ) def test_increment(n, expected): - assert n + 1 == expected + assert n + 1 == expected In this example the mark "foo" will apply to each of the three tests, whereas the "bar" mark is only applied to the second test. @@ -332,31 +354,46 @@ Custom marker and command line option to control test runs Plugins can provide custom markers and implement specific behaviour based on it. This is a self-contained example which adds a command line option and a parametrized test function marker to run tests -specifies via named environments:: +specifies via named environments: + +.. code-block:: python # content of conftest.py import pytest + + def pytest_addoption(parser): - parser.addoption("-E", action="store", metavar="NAME", - help="only run tests matching the environment NAME.") + parser.addoption( + "-E", + action="store", + metavar="NAME", + help="only run tests matching the environment NAME.", + ) + def pytest_configure(config): # register an additional marker - config.addinivalue_line("markers", - "env(name): mark test to run only on named environment") + config.addinivalue_line( + "markers", "env(name): mark test to run only on named environment" + ) + def pytest_runtest_setup(item): - envnames = [mark.args[0] for mark in item.iter_markers(name='env')] + envnames = [mark.args[0] for mark in item.iter_markers(name="env")] if envnames: if item.config.getoption("-E") not in envnames: pytest.skip("test requires env in %r" % envnames) -A test file using this local plugin:: +A test file using this local plugin: + +.. code-block:: python # content of test_someenv.py import pytest + + @pytest.mark.env("stage1") def test_basic_db_operation(): pass @@ -423,25 +460,32 @@ Passing a callable to custom markers .. regendoc:wipe -Below is the config file that will be used in the next examples:: +Below is the config file that will be used in the next examples: + +.. code-block:: python # content of conftest.py import sys + def pytest_runtest_setup(item): - for marker in item.iter_markers(name='my_marker'): + for marker in item.iter_markers(name="my_marker"): print(marker) sys.stdout.flush() A custom marker can have its argument set, i.e. ``args`` and ``kwargs`` properties, defined by either invoking it as a callable or using ``pytest.mark.MARKER_NAME.with_args``. These two methods achieve the same effect most of the time. -However, if there is a callable as the single positional argument with no keyword arguments, using the ``pytest.mark.MARKER_NAME(c)`` will not pass ``c`` as a positional argument but decorate ``c`` with the custom marker (see :ref:`MarkDecorator `). Fortunately, ``pytest.mark.MARKER_NAME.with_args`` comes to the rescue:: +However, if there is a callable as the single positional argument with no keyword arguments, using the ``pytest.mark.MARKER_NAME(c)`` will not pass ``c`` as a positional argument but decorate ``c`` with the custom marker (see :ref:`MarkDecorator `). Fortunately, ``pytest.mark.MARKER_NAME.with_args`` comes to the rescue: + +.. code-block:: python # content of test_custom_marker.py import pytest + def hello_world(*args, **kwargs): - return 'Hello World' + return "Hello World" + @pytest.mark.my_marker.with_args(hello_world) def test_with_args(): @@ -467,12 +511,16 @@ Reading markers which were set from multiple places .. regendoc:wipe If you are heavily using markers in your test suite you may encounter the case where a marker is applied several times to a test function. From plugin -code you can read over all such settings. Example:: +code you can read over all such settings. Example: + +.. code-block:: python # content of test_mark_three_times.py import pytest + pytestmark = pytest.mark.glob("module", x=1) + @pytest.mark.glob("class", x=2) class TestClass(object): @pytest.mark.glob("function", x=3) @@ -480,13 +528,16 @@ code you can read over all such settings. Example:: pass Here we have the marker "glob" applied three times to the same -test function. From a conftest file we can read it like this:: +test function. From a conftest file we can read it like this: + +.. code-block:: python # content of conftest.py import sys + def pytest_runtest_setup(item): - for mark in item.iter_markers(name='glob'): + for mark in item.iter_markers(name="glob"): print("glob args=%s kwargs=%s" % (mark.args, mark.kwargs)) sys.stdout.flush() @@ -510,7 +561,9 @@ Consider you have a test suite which marks tests for particular platforms, namely ``pytest.mark.darwin``, ``pytest.mark.win32`` etc. and you also have tests that run on all platforms and have no specific marker. If you now want to have a way to only run the tests -for your particular platform, you could use the following plugin:: +for your particular platform, you could use the following plugin: + +.. code-block:: python # content of conftest.py # @@ -519,6 +572,7 @@ for your particular platform, you could use the following plugin:: ALL = set("darwin linux win32".split()) + def pytest_runtest_setup(item): supported_platforms = ALL.intersection(mark.name for mark in item.iter_markers()) plat = sys.platform @@ -526,24 +580,30 @@ for your particular platform, you could use the following plugin:: pytest.skip("cannot run on platform %s" % (plat)) then tests will be skipped if they were specified for a different platform. -Let's do a little test file to show how this looks like:: +Let's do a little test file to show how this looks like: + +.. code-block:: python # content of test_plat.py import pytest + @pytest.mark.darwin def test_if_apple_is_evil(): pass + @pytest.mark.linux def test_if_linux_works(): pass + @pytest.mark.win32 def test_if_win32_crashes(): pass + def test_runs_everywhere(): pass @@ -589,28 +649,38 @@ Automatically adding markers based on test names If you a test suite where test function names indicate a certain type of test, you can implement a hook that automatically defines markers so that you can use the ``-m`` option with it. Let's look -at this test module:: +at this test module: + +.. code-block:: python # content of test_module.py + def test_interface_simple(): assert 0 + def test_interface_complex(): assert 0 + def test_event_simple(): assert 0 + def test_something_else(): assert 0 We want to dynamically define two markers and can do it in a -``conftest.py`` plugin:: +``conftest.py`` plugin: + +.. code-block:: python # content of conftest.py import pytest + + def pytest_collection_modifyitems(items): for item in items: if "interface" in item.nodeid: diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 1b9f657fd..19de27af6 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -515,21 +515,25 @@ 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:: +For example: + +.. code-block:: python # 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'), - ]) + + + @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 diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 2a296dc10..2a2364c27 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -1,13 +1,9 @@ - .. _`tbreportdemo`: Demo of Python failure reports with pytest -================================================== +========================================== -Here is a nice run of several tens of failures -and how ``pytest`` presents things (unfortunately -not showing the nice colors here in the HTML that you -get on the terminal - we are working on that): +Here is a nice run of several failures and how ``pytest`` presents things: .. code-block:: pytest diff --git a/doc/en/historical-notes.rst b/doc/en/historical-notes.rst index 9462d700f..4dfec7f5e 100644 --- a/doc/en/historical-notes.rst +++ b/doc/en/historical-notes.rst @@ -57,14 +57,16 @@ Applying marks to ``@pytest.mark.parametrize`` parameters .. versionchanged:: 3.1 Prior to version 3.1 the supported mechanism for marking values -used the syntax:: +used the syntax: + +.. code-block:: python import pytest - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - pytest.mark.xfail(("6*9", 42),), - ]) + + + @pytest.mark.parametrize( + "test_input,expected", [("3+5", 8), ("2+4", 6), pytest.mark.xfail(("6*9", 42))] + ) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -105,9 +107,13 @@ Conditions as strings instead of booleans .. versionchanged:: 2.4 Prior to pytest-2.4 the only way to specify skipif/xfail conditions was -to use strings:: +to use strings: + +.. code-block:: python import sys + + @pytest.mark.skipif("sys.version_info >= (3,3)") def test_function(): ... @@ -139,17 +145,20 @@ dictionary which is constructed as follows: expression is applied. The pytest ``config`` object allows you to skip based on a test -configuration value which you might have added:: +configuration value which you might have added: + +.. code-block:: python @pytest.mark.skipif("not config.getvalue('db')") - def test_function(...): + def test_function(): ... -The equivalent with "boolean conditions" is:: +The equivalent with "boolean conditions" is: - @pytest.mark.skipif(not pytest.config.getvalue("db"), - reason="--db was not specified") - def test_function(...): +.. code-block:: python + + @pytest.mark.skipif(not pytest.config.getvalue("db"), reason="--db was not specified") + def test_function(): pass .. note:: @@ -164,12 +173,16 @@ The equivalent with "boolean conditions" is:: .. versionchanged:: 2.4 -Previous to version 2.4 to set a break point in code one needed to use ``pytest.set_trace()``:: +Previous to version 2.4 to set a break point in code one needed to use ``pytest.set_trace()``: + +.. code-block:: python import pytest + + def test_function(): ... - pytest.set_trace() # invoke PDB debugger and tracing + pytest.set_trace() # invoke PDB debugger and tracing This is no longer needed and one can use the native ``import pdb;pdb.set_trace()`` call directly. diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index c9bd09d1a..51481b227 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -36,15 +36,15 @@ pytest enables test parametrization at several levels: The builtin :ref:`pytest.mark.parametrize ref` decorator enables parametrization of arguments for a test function. Here is a typical example of a test function that implements checking that a certain input leads -to an expected output:: +to an expected output: + +.. code-block:: python # content of test_expectation.py import pytest - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - ("6*9", 42), - ]) + + + @pytest.mark.parametrize("test_input,expected", [("3+5", 8), ("2+4", 6), ("6*9", 42)]) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -104,16 +104,18 @@ Note that you could also use the parametrize marker on a class or a module (see :ref:`mark`) which would invoke several functions with the argument sets. It is also possible to mark individual test instances within parametrize, -for example with the builtin ``mark.xfail``:: +for example with the builtin ``mark.xfail``: + +.. code-block:: python # content of test_expectation.py import pytest - @pytest.mark.parametrize("test_input,expected", [ - ("3+5", 8), - ("2+4", 6), - pytest.param("6*9", 42, - marks=pytest.mark.xfail), - ]) + + + @pytest.mark.parametrize( + "test_input,expected", + [("3+5", 8), ("2+4", 6), pytest.param("6*9", 42, marks=pytest.mark.xfail)], + ) def test_eval(test_input, expected): assert eval(test_input) == expected @@ -140,9 +142,13 @@ example, if they're dynamically generated by some function - the behaviour of pytest is defined by the :confval:`empty_parameter_set_mark` option. To get all combinations of multiple parametrized arguments you can stack -``parametrize`` decorators:: +``parametrize`` decorators: + +.. code-block:: python import pytest + + @pytest.mark.parametrize("x", [0, 1]) @pytest.mark.parametrize("y", [2, 3]) def test_foo(x, y): @@ -166,26 +172,36 @@ parametrization. For example, let's say we want to run a test taking string inputs which we want to set via a new ``pytest`` command line option. Let's first write -a simple test accepting a ``stringinput`` fixture function argument:: +a simple test accepting a ``stringinput`` fixture function argument: + +.. code-block:: python # content of test_strings.py + def test_valid_string(stringinput): assert stringinput.isalpha() Now we add a ``conftest.py`` file containing the addition of a -command line option and the parametrization of our test function:: +command line option and the parametrization of our test function: + +.. code-block:: python # content of conftest.py + def pytest_addoption(parser): - parser.addoption("--stringinput", action="append", default=[], - help="list of stringinputs to pass to test functions") + parser.addoption( + "--stringinput", + action="append", + default=[], + help="list of stringinputs to pass to test functions", + ) + def pytest_generate_tests(metafunc): - if 'stringinput' in metafunc.fixturenames: - metafunc.parametrize("stringinput", - metafunc.config.getoption('stringinput')) + if "stringinput" in metafunc.fixturenames: + metafunc.parametrize("stringinput", metafunc.config.getoption("stringinput")) If we now pass two stringinput values, our test will run twice: diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 69d7940fc..871e6c3f0 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -84,32 +84,44 @@ It is also possible to skip the whole module using If you wish to skip something conditionally then you can use ``skipif`` instead. Here is an example of marking a test function to be skipped -when run on an interpreter earlier than Python3.6:: +when run on an interpreter earlier than Python3.6: + +.. code-block:: python import sys - @pytest.mark.skipif(sys.version_info < (3,6), - reason="requires python3.6 or higher") + + + @pytest.mark.skipif(sys.version_info < (3, 6), reason="requires python3.6 or higher") def test_function(): ... If the condition evaluates to ``True`` during collection, the test function will be skipped, with the specified reason appearing in the summary when using ``-rs``. -You can share ``skipif`` markers between modules. Consider this test module:: +You can share ``skipif`` markers between modules. Consider this test module: + +.. code-block:: python # content of test_mymodule.py import mymodule - minversion = pytest.mark.skipif(mymodule.__versioninfo__ < (1,1), - reason="at least mymodule-1.1 required") + + minversion = pytest.mark.skipif( + mymodule.__versioninfo__ < (1, 1), reason="at least mymodule-1.1 required" + ) + + @minversion def test_function(): ... -You can import the marker and reuse it in another test module:: +You can import the marker and reuse it in another test module: + +.. code-block:: python # test_myothermodule.py from test_mymodule import minversion + @minversion def test_anotherfunction(): ... @@ -128,12 +140,12 @@ so they are supported mainly for backward compatibility reasons. Skip all test functions of a class or module ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -You can use the ``skipif`` marker (as any other marker) on classes:: +You can use the ``skipif`` marker (as any other marker) on classes: - @pytest.mark.skipif(sys.platform == 'win32', - reason="does not run on windows") +.. code-block:: python + + @pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") class TestPosixCalls(object): - def test_function(self): "will not be setup or run under 'win32' platform" @@ -269,10 +281,11 @@ You can change the default value of the ``strict`` parameter using the ~~~~~~~~~~~~~~~~~~~~ As with skipif_ you can also mark your expectation of a failure -on a particular platform:: +on a particular platform: - @pytest.mark.xfail(sys.version_info >= (3,6), - reason="python3.6 api changes") +.. code-block:: python + + @pytest.mark.xfail(sys.version_info >= (3, 6), reason="python3.6 api changes") def test_function(): ... diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index c47817c9a..322cf631c 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -6,15 +6,19 @@ Warnings Capture .. versionadded:: 3.1 Starting from version ``3.1``, pytest now automatically catches warnings during test execution -and displays them at the end of the session:: +and displays them at the end of the session: + +.. code-block:: python # content of test_show_warnings.py import warnings + def api_v1(): warnings.warn(UserWarning("api v1, should use functions from v2")) return 1 + def test_one(): assert api_v1() == 1 @@ -195,28 +199,36 @@ Ensuring code triggers a deprecation warning You can also call a global helper for checking that a certain function call triggers a ``DeprecationWarning`` or -``PendingDeprecationWarning``:: +``PendingDeprecationWarning``: + +.. code-block:: python import pytest + def test_global(): pytest.deprecated_call(myfunction, 17) By default, ``DeprecationWarning`` and ``PendingDeprecationWarning`` will not be caught when using ``pytest.warns`` or ``recwarn`` because default Python warnings filters hide them. If you wish to record them in your own code, use the -command ``warnings.simplefilter('always')``:: +command ``warnings.simplefilter('always')``: + +.. code-block:: python import warnings import pytest + def test_deprecation(recwarn): - warnings.simplefilter('always') + warnings.simplefilter("always") warnings.warn("deprecated", DeprecationWarning) assert len(recwarn) == 1 assert recwarn.pop(DeprecationWarning) -You can also use it as a contextmanager:: +You can also use it as a contextmanager: + +.. code-block:: python def test_global(): with pytest.deprecated_call(): @@ -238,11 +250,14 @@ Asserting warnings with the warns function .. versionadded:: 2.8 You can check that code raises a particular warning using ``pytest.warns``, -which works in a similar manner to :ref:`raises `:: +which works in a similar manner to :ref:`raises `: + +.. code-block:: python import warnings import pytest + def test_warning(): with pytest.warns(UserWarning): warnings.warn("my warning", UserWarning) @@ -269,7 +284,9 @@ You can also call ``pytest.warns`` on a function or code string:: The function also returns a list of all raised warnings (as ``warnings.WarningMessage`` objects), which you can query for -additional information:: +additional information: + +.. code-block:: python with pytest.warns(RuntimeWarning) as record: warnings.warn("another warning", RuntimeWarning) @@ -297,7 +314,9 @@ You can record raised warnings either using ``pytest.warns`` or with the ``recwarn`` fixture. To record with ``pytest.warns`` without asserting anything about the warnings, -pass ``None`` as the expected warning type:: +pass ``None`` as the expected warning type: + +.. code-block:: python with pytest.warns(None) as record: warnings.warn("user", UserWarning) @@ -307,10 +326,13 @@ pass ``None`` as the expected warning type:: assert str(record[0].message) == "user" assert str(record[1].message) == "runtime" -The ``recwarn`` fixture will record warnings for the whole function:: +The ``recwarn`` fixture will record warnings for the whole function: + +.. code-block:: python import warnings + def test_hello(recwarn): warnings.warn("hello", UserWarning) assert len(recwarn) == 1 diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index e717431e6..ec0e96198 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -528,10 +528,13 @@ a :py:class:`Result ` instance which encapsulates a result or exception info. The yield point itself will thus typically not raise exceptions (unless there are bugs). -Here is an example definition of a hook wrapper:: +Here is an example definition of a hook wrapper: + +.. code-block:: python import pytest + @pytest.hookimpl(hookwrapper=True) def pytest_pyfunc_call(pyfuncitem): do_something_before_next_hook_executes() @@ -636,10 +639,13 @@ if you depend on a plugin that is not installed, validation will fail and the error message will not make much sense to your users. One approach is to defer the hook implementation to a new plugin instead of -declaring the hook functions directly in your plugin module, for example:: +declaring the hook functions directly in your plugin module, for example: + +.. code-block:: python # contents of myplugin.py + class DeferPlugin(object): """Simple plugin to defer pytest-xdist hook functions.""" @@ -647,8 +653,9 @@ declaring the hook functions directly in your plugin module, for example:: """standard xdist hook function. """ + def pytest_configure(config): - if config.pluginmanager.hasplugin('xdist'): + if config.pluginmanager.hasplugin("xdist"): config.pluginmanager.register(DeferPlugin()) This has the added benefit of allowing you to conditionally install hooks diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 6134ca77b..fa7e89364 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -93,3 +93,9 @@ PYTEST_WARNS_UNKNOWN_KWARGS = UnformattedWarning( "pytest.warns() got unexpected keyword arguments: {args!r}.\n" "This will be an error in future versions.", ) + +PYTEST_PARAM_UNKNOWN_KWARGS = UnformattedWarning( + PytestDeprecationWarning, + "pytest.param() got unexpected keyword arguments: {args!r}.\n" + "This will be an error in future versions.", +) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index d6bceba02..902904457 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -853,7 +853,9 @@ class FixtureDef(object): exceptions.append(sys.exc_info()) if exceptions: e = exceptions[0] - del exceptions # ensure we don't keep all frames alive because of the traceback + del ( + exceptions + ) # ensure we don't keep all frames alive because of the traceback six.reraise(*e) finally: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index f3602b2d5..8234b0549 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -10,6 +10,7 @@ from ..compat import ascii_escaped from ..compat import getfslineno from ..compat import MappingMixin from ..compat import NOTSET +from _pytest.deprecated import PYTEST_PARAM_UNKNOWN_KWARGS from _pytest.outcomes import fail from _pytest.warning_types import UnknownMarkWarning @@ -61,20 +62,25 @@ def get_empty_parameterset_mark(config, argnames, func): class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): @classmethod - def param(cls, *values, **kw): - marks = kw.pop("marks", ()) + def param(cls, *values, **kwargs): + marks = kwargs.pop("marks", ()) if isinstance(marks, MarkDecorator): marks = (marks,) else: assert isinstance(marks, (tuple, list, set)) - id_ = kw.pop("id", None) + id_ = kwargs.pop("id", None) if id_ is not None: if not isinstance(id_, six.string_types): raise TypeError( "Expected id to be a string, got {}: {!r}".format(type(id_), id_) ) id_ = ascii_escaped(id_) + + if kwargs: + warnings.warn( + PYTEST_PARAM_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=3 + ) return cls(values, marks, id_) @classmethod diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index f6c134664..3e221d3d9 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -271,6 +271,18 @@ class MonkeyPatch(object): # https://github.com/pypa/setuptools/blob/d8b901bc/docs/pkg_resources.txt#L162-L171 fixup_namespace_packages(str(path)) + # A call to syspathinsert() usually means that the caller wants to + # import some dynamically created files, thus with python3 we + # invalidate its import caches. + # This is especially important when any namespace package is in used, + # since then the mtime based FileFinder cache (that gets created in + # this case already) gets not invalidated when writing the new files + # quickly afterwards. + if sys.version_info >= (3, 3): + from importlib import invalidate_caches + + invalidate_caches() + def chdir(self, path): """ Change the current working directory to the specified path. Path can be a string or a py.path.local object. diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 6c2dfb5ae..f57983918 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -97,8 +97,7 @@ def skip(msg="", **kwargs): __tracebackhide__ = True allow_module_level = kwargs.pop("allow_module_level", False) if kwargs: - keys = [k for k in kwargs.keys()] - raise TypeError("unexpected keyword arguments: {}".format(keys)) + raise TypeError("unexpected keyword arguments: {}".format(sorted(kwargs))) raise Skipped(msg=msg, allow_module_level=allow_module_level) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 40b014dbd..a51478fbc 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -627,27 +627,10 @@ class Testdir(object): This is undone automatically when this object dies at the end of each test. """ - from pkg_resources import fixup_namespace_packages - if path is None: path = self.tmpdir - dirname = str(path) - sys.path.insert(0, dirname) - fixup_namespace_packages(dirname) - - # a call to syspathinsert() usually means that the caller wants to - # import some dynamically created files, thus with python3 we - # invalidate its import caches - self._possibly_invalidate_import_caches() - - def _possibly_invalidate_import_caches(self): - # invalidate caches if we can (py33 and above) - try: - from importlib import invalidate_caches - except ImportError: - return - invalidate_caches() + self.monkeypatch.syspath_prepend(str(path)) def mkdir(self, name): """Create a new (sub)directory.""" @@ -1361,7 +1344,7 @@ class LineMatcher(object): raise ValueError("line %r not found in output" % fnline) def _log(self, *args): - self._log_output.append(" ".join((str(x) for x in args))) + self._log_output.append(" ".join(str(x) for x in args)) @property def _log_text(self): diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 1b643d430..66de85468 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -682,7 +682,7 @@ def raises(expected_exception, *args, **kwargs): match_expr = kwargs.pop("match") if kwargs: msg = "Unexpected keyword arguments passed to pytest.raises: " - msg += ", ".join(kwargs.keys()) + msg += ", ".join(sorted(kwargs)) raise TypeError(msg) return RaisesContext(expected_exception, message, match_expr) elif isinstance(args[0], str): diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index e262152e4..48f8028e6 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1071,9 +1071,7 @@ class TestFixtureUsages(object): ) result = testdir.runpytest_inprocess() result.stdout.fnmatch_lines( - ( - "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" - ) + "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" ) def test_funcarg_parametrized_and_used_twice(self, testdir): diff --git a/testing/test_mark.py b/testing/test_mark.py index cb20658b5..c972b9c7a 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -13,6 +13,7 @@ from _pytest.mark import EMPTY_PARAMETERSET_OPTION from _pytest.mark import MarkGenerator as Mark from _pytest.nodes import Collector from _pytest.nodes import Node +from _pytest.warning_types import PytestDeprecationWarning from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG try: @@ -991,3 +992,15 @@ def test_pytest_param_id_requires_string(): @pytest.mark.parametrize("s", (None, "hello world")) def test_pytest_param_id_allows_none_or_string(s): assert pytest.param(id=s) + + +def test_pytest_param_warning_on_unknown_kwargs(): + with pytest.warns(PytestDeprecationWarning) as warninfo: + # typo, should be marks= + pytest.param(1, 2, mark=pytest.mark.xfail()) + assert warninfo[0].filename == __file__ + msg, = warninfo[0].message.args + assert msg == ( + "pytest.param() got unexpected keyword arguments: ['mark'].\n" + "This will be an error in future versions." + ) diff --git a/testing/test_monkeypatch.py b/testing/test_monkeypatch.py index d43fb6bab..9e45e05c8 100644 --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -462,3 +462,10 @@ def test_syspath_prepend_with_namespace_packages(testdir, monkeypatch): import ns_pkg.world assert ns_pkg.world.check() == "world" + + # Should invalidate caches via importlib.invalidate_caches. + tmpdir = testdir.tmpdir + modules_tmpdir = tmpdir.mkdir("modules_tmpdir") + monkeypatch.syspath_prepend(str(modules_tmpdir)) + modules_tmpdir.join("main_app.py").write("app = True") + from main_app import app # noqa: F401