From be91c4d932820d9e8b69ce63b480dfbcc2e3fa33 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 11:02:46 -0300 Subject: [PATCH 01/70] Remove Request.getfuncargvalue --- changelog/5180.removal.rst | 4 ++++ src/_pytest/deprecated.py | 3 --- src/_pytest/fixtures.py | 7 ------ testing/deprecated_test.py | 4 ---- testing/python/fixtures.py | 46 +++++++++++++------------------------- 5 files changed, 20 insertions(+), 44 deletions(-) create mode 100644 changelog/5180.removal.rst diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst new file mode 100644 index 000000000..952a75c82 --- /dev/null +++ b/changelog/5180.removal.rst @@ -0,0 +1,4 @@ +As per our policy, the following features have been deprecated in the 4.X series and are now being +removed: + +* ``Request.getfuncargvalue``: use ``Request.getfixturevalue`` instead. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index e31b9eb0e..4b68fd107 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -36,9 +36,6 @@ FIXTURE_NAMED_REQUEST = PytestDeprecationWarning( CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." -GETFUNCARGVALUE = RemovedInPytest4Warning( - "getfuncargvalue is deprecated, use getfixturevalue" -) FUNCARGNAMES = PytestDeprecationWarning( "The `funcargnames` attribute was an alias for `fixturenames`, " diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 3262b65bb..2cde5725c 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -470,13 +470,6 @@ class FixtureRequest(FuncargnamesCompatAttr): """ return self._get_active_fixturedef(argname).cached_result[0] - def getfuncargvalue(self, argname): - """ Deprecated, use getfixturevalue. """ - from _pytest import deprecated - - warnings.warn(deprecated.GETFUNCARGVALUE, stacklevel=2) - return self.getfixturevalue(argname) - def _get_active_fixturedef(self, argname): try: return self._fixture_defs[argname] diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 5cbb694b1..4ce89113d 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -32,10 +32,6 @@ def test_pytest_custom_cfg_unsupported(testdir): testdir.runpytest("-c", "custom.cfg") -def test_getfuncargvalue_is_deprecated(request): - pytest.deprecated_call(request.getfuncargvalue, "tmpdir") - - @pytest.mark.filterwarnings("default") def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--help") diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index c0c230ccf..63fb6294d 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -599,8 +599,7 @@ class TestRequestBasic: result = testdir.runpytest() result.stdout.fnmatch_lines(["* 2 passed in *"]) - @pytest.mark.parametrize("getfixmethod", ("getfixturevalue", "getfuncargvalue")) - def test_getfixturevalue(self, testdir, getfixmethod): + def test_getfixturevalue(self, testdir): item = testdir.getitem( """ import pytest @@ -613,35 +612,22 @@ class TestRequestBasic: def test_func(something): pass """ ) - import contextlib - - if getfixmethod == "getfuncargvalue": - warning_expectation = pytest.warns(DeprecationWarning) - else: - # see #1830 for a cleaner way to accomplish this - @contextlib.contextmanager - def expecting_no_warning(): - yield - - warning_expectation = expecting_no_warning() - req = item._request - with warning_expectation: - fixture_fetcher = getattr(req, getfixmethod) - with pytest.raises(FixtureLookupError): - fixture_fetcher("notexists") - val = fixture_fetcher("something") - assert val == 1 - val = fixture_fetcher("something") - assert val == 1 - val2 = fixture_fetcher("other") - assert val2 == 2 - val2 = fixture_fetcher("other") # see about caching - assert val2 == 2 - pytest._fillfuncargs(item) - assert item.funcargs["something"] == 1 - assert len(get_public_names(item.funcargs)) == 2 - assert "request" in item.funcargs + + with pytest.raises(FixtureLookupError): + req.getfixturevalue("notexists") + val = req.getfixturevalue("something") + assert val == 1 + val = req.getfixturevalue("something") + assert val == 1 + val2 = req.getfixturevalue("other") + assert val2 == 2 + val2 = req.getfixturevalue("other") # see about caching + assert val2 == 2 + pytest._fillfuncargs(item) + assert item.funcargs["something"] == 1 + assert len(get_public_names(item.funcargs)) == 2 + assert "request" in item.funcargs def test_request_addfinalizer(self, testdir): item = testdir.getitem( From 279733a30b700bc30848c4d4ff10f65c454e952b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 11:40:24 -0300 Subject: [PATCH 02/70] Remove support for 'code as string' from pytest.raises and pytest.warns --- changelog/5180.removal.rst | 6 +++++ doc/en/deprecations.rst | 46 ++++++++++++++++++++------------------ src/_pytest/deprecated.py | 8 ------- src/_pytest/python_api.py | 20 ++++------------- src/_pytest/recwarn.py | 18 ++++----------- testing/python/raises.py | 30 +++++++------------------ testing/test_mark.py | 3 ++- testing/test_recwarn.py | 26 +++++++++------------ 8 files changed, 58 insertions(+), 99 deletions(-) diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst index 952a75c82..bde3e03ff 100644 --- a/changelog/5180.removal.rst +++ b/changelog/5180.removal.rst @@ -2,3 +2,9 @@ As per our policy, the following features have been deprecated in the 4.X series removed: * ``Request.getfuncargvalue``: use ``Request.getfixturevalue`` instead. + +* ``pytest.raises`` and ``pytest.warns`` no longer support strings as the second argument. + + +For more information consult +`Deprecations and Removals `__ in the docs. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index e2399dd41..344e1dd72 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -80,12 +80,35 @@ The ``pytest.config`` global object is deprecated. Instead use use the ``pytest_configure(config)`` hook. Note that many hooks can also access the ``config`` object indirectly, through ``session.config`` or ``item.config`` for example. + +Result log (``--result-log``) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 4.0 + +The ``--result-log`` option produces a stream of test reports which can be +analysed at runtime. It uses a custom format which requires users to implement their own +parser, but the team believes using a line-based format that can be parsed using standard +tools would provide a suitable and better alternative. + +The current plan is to provide an alternative in the pytest 5.0 series and remove the ``--result-log`` +option in pytest 6.0 after the new implementation proves satisfactory to all users and is deemed +stable. + +The actual alternative is still being discussed in issue `#4488 `__. + +Removed Features +---------------- + +As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after +an appropriate period of deprecation has passed. + .. _raises-warns-exec: ``raises`` / ``warns`` with a string as the second argument ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. deprecated:: 4.1 +.. versionremoved:: 5.0 Use the context manager form of these instead. When necessary, invoke ``exec`` directly. @@ -116,27 +139,6 @@ Becomes: -Result log (``--result-log``) -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 4.0 - -The ``--result-log`` option produces a stream of test reports which can be -analysed at runtime. It uses a custom format which requires users to implement their own -parser, but the team believes using a line-based format that can be parsed using standard -tools would provide a suitable and better alternative. - -The current plan is to provide an alternative in the pytest 5.0 series and remove the ``--result-log`` -option in pytest 6.0 after the new implementation proves satisfactory to all users and is deemed -stable. - -The actual alternative is still being discussed in issue `#4488 `__. - -Removed Features ----------------- - -As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after -an appropriate period of deprecation has passed. Using ``Class`` in custom Collectors ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 4b68fd107..77c7c30ff 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -54,14 +54,6 @@ RESULT_LOG = PytestDeprecationWarning( "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." ) -RAISES_EXEC = PytestDeprecationWarning( - "raises(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly\n\n" - "See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec" -) -WARNS_EXEC = PytestDeprecationWarning( - "warns(..., 'code(as_a_string)') is deprecated, use the context manager form or use `exec()` directly.\n\n" - "See https://docs.pytest.org/en/latest/deprecations.html#raises-warns-exec" -) PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = ( "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported " diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 374fa598f..f709057a3 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,7 +1,6 @@ import inspect import math import pprint -import sys import warnings from collections.abc import Iterable from collections.abc import Mapping @@ -667,23 +666,12 @@ def raises(expected_exception, *args, **kwargs): msg += ", ".join(sorted(kwargs)) raise TypeError(msg) return RaisesContext(expected_exception, message, match_expr) - elif isinstance(args[0], str): - warnings.warn(deprecated.RAISES_EXEC, stacklevel=2) - code, = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - # print "raises frame scope: %r" % frame.f_locals - try: - code = _pytest._code.Source(code).compile(_genframe=frame) - exec(code, frame.f_globals, loc) - # XXX didn't mean f_globals == f_locals something special? - # this is destroyed here ... - except expected_exception: - return _pytest._code.ExceptionInfo.from_current() else: func = args[0] + if not callable(func): + raise TypeError( + "{!r} object (type: {}) must be callable".format(func, type(func)) + ) try: func(*args[1:], **kwargs) except expected_exception: diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 006d97e7f..8733a893a 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -1,12 +1,9 @@ """ recording warnings during test function execution. """ import inspect import re -import sys import warnings -import _pytest._code from _pytest.deprecated import PYTEST_WARNS_UNKNOWN_KWARGS -from _pytest.deprecated import WARNS_EXEC from _pytest.fixtures import yield_fixture from _pytest.outcomes import fail @@ -86,19 +83,12 @@ def warns(expected_warning, *args, **kwargs): PYTEST_WARNS_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=2 ) return WarningsChecker(expected_warning, match_expr=match_expr) - elif isinstance(args[0], str): - warnings.warn(WARNS_EXEC, stacklevel=2) - code, = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - - with WarningsChecker(expected_warning): - code = _pytest._code.Source(code).compile() - exec(code, frame.f_globals, loc) else: func = args[0] + if not callable(func): + raise TypeError( + "{!r} object (type: {}) must be callable".format(func, type(func)) + ) with WarningsChecker(expected_warning): return func(*args[1:], **kwargs) diff --git a/testing/python/raises.py b/testing/python/raises.py index c9ede412a..3ace96e7a 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -6,31 +6,17 @@ from _pytest.warning_types import PytestDeprecationWarning class TestRaises: + def test_check_callable(self): + with pytest.raises(TypeError, match=r".* must be callable"): + pytest.raises(RuntimeError, "int('qwe')") + def test_raises(self): - source = "int('qwe')" - with pytest.warns(PytestDeprecationWarning): - excinfo = pytest.raises(ValueError, source) - code = excinfo.traceback[-1].frame.code - s = str(code.fullsource) - assert s == source - - def test_raises_exec(self): - with pytest.warns(PytestDeprecationWarning) as warninfo: - pytest.raises(ValueError, "a,x = []") - assert warninfo[0].filename == __file__ - - def test_raises_exec_correct_filename(self): - with pytest.warns(PytestDeprecationWarning): - excinfo = pytest.raises(ValueError, 'int("s")') - assert __file__ in excinfo.traceback[-1].path - - def test_raises_syntax_error(self): - with pytest.warns(PytestDeprecationWarning) as warninfo: - pytest.raises(SyntaxError, "qwe qwe qwe") - assert warninfo[0].filename == __file__ + excinfo = pytest.raises(ValueError, int, "qwe") + assert "invalid literal" in str(excinfo.value) def test_raises_function(self): - pytest.raises(ValueError, int, "hello") + excinfo = pytest.raises(ValueError, int, "hello") + assert "invalid literal" in str(excinfo.value) def test_raises_callable_no_exception(self): class A: diff --git a/testing/test_mark.py b/testing/test_mark.py index c22e9dbb5..1544ffe56 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -25,7 +25,8 @@ class TestMark: def test_pytest_mark_notcallable(self): mark = Mark() - pytest.raises((AttributeError, TypeError), mark) + with pytest.raises(TypeError): + mark() def test_mark_with_param(self): def some_function(abc): diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 1c68b3787..65fdd1682 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -3,7 +3,6 @@ import warnings import pytest from _pytest.recwarn import WarningsRecorder -from _pytest.warning_types import PytestDeprecationWarning def test_recwarn_stacklevel(recwarn): @@ -206,22 +205,17 @@ class TestDeprecatedCall: class TestWarns: - def test_strings(self): + def test_check_callable(self): + source = "warnings.warn('w1', RuntimeWarning)" + with pytest.raises(TypeError, match=r".* must be callable"): + pytest.warns(RuntimeWarning, source) + + def test_several_messages(self): # different messages, b/c Python suppresses multiple identical warnings - source1 = "warnings.warn('w1', RuntimeWarning)" - source2 = "warnings.warn('w2', RuntimeWarning)" - source3 = "warnings.warn('w3', RuntimeWarning)" - with pytest.warns(PytestDeprecationWarning) as warninfo: # yo dawg - pytest.warns(RuntimeWarning, source1) - pytest.raises( - pytest.fail.Exception, lambda: pytest.warns(UserWarning, source2) - ) - pytest.warns(RuntimeWarning, source3) - assert len(warninfo) == 3 - for w in warninfo: - assert w.filename == __file__ - msg, = w.message.args - assert msg.startswith("warns(..., 'code(as_a_string)') is deprecated") + pytest.warns(RuntimeWarning, lambda: warnings.warn("w1", RuntimeWarning)) + with pytest.raises(pytest.fail.Exception): + pytest.warns(UserWarning, lambda: warnings.warn("w2", RuntimeWarning)) + pytest.warns(RuntimeWarning, lambda: warnings.warn("w3", RuntimeWarning)) def test_function(self): pytest.warns( From 13f7f27fd2f99db956de03112add27288d2ce6af Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 11:48:27 -0300 Subject: [PATCH 03/70] Remove 'message' parameter from pytest.raises --- changelog/5180.removal.rst | 4 +- doc/en/deprecations.rst | 75 ++++++++++++++++++++------------------ src/_pytest/deprecated.py | 6 --- src/_pytest/python_api.py | 7 ---- testing/deprecated_test.py | 6 --- testing/python/raises.py | 12 ------ 6 files changed, 42 insertions(+), 68 deletions(-) diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst index bde3e03ff..2417672be 100644 --- a/changelog/5180.removal.rst +++ b/changelog/5180.removal.rst @@ -1,10 +1,12 @@ -As per our policy, the following features have been deprecated in the 4.X series and are now being +As per our policy, the following features have been deprecated in the 4.X series and are now removed: * ``Request.getfuncargvalue``: use ``Request.getfixturevalue`` instead. * ``pytest.raises`` and ``pytest.warns`` no longer support strings as the second argument. +* ``message`` parameter of ``pytest.raises``. + For more information consult `Deprecations and Removals `__ in the docs. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 344e1dd72..c97061f09 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -20,8 +20,8 @@ Below is a complete list of all pytest features which are considered deprecated. :ref:`standard warning filters `. -Removal of ``funcargnames`` alias for ``fixturenames`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +``funcargnames`` alias for ``fixturenames`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. deprecated:: 5.0 @@ -34,40 +34,6 @@ in places where we or plugin authors must distinguish between fixture names and names supplied by non-fixture things such as ``pytest.mark.parametrize``. -.. _`raises message deprecated`: - -``"message"`` parameter of ``pytest.raises`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 4.1 - -It is a common mistake to think this parameter will match the exception message, while in fact -it only serves to provide a custom message in case the ``pytest.raises`` check fails. To prevent -users from making this mistake, and because it is believed to be little used, pytest is -deprecating it without providing an alternative for the moment. - -If you have a valid use case for this parameter, consider that to obtain the same results -you can just call ``pytest.fail`` manually at the end of the ``with`` statement. - -For example: - -.. code-block:: python - - with pytest.raises(TimeoutError, message="Client got unexpected message"): - wait_for(websocket.recv(), 0.5) - - -Becomes: - -.. code-block:: python - - with pytest.raises(TimeoutError): - wait_for(websocket.recv(), 0.5) - pytest.fail("Client got unexpected message") - - -If you still have concerns about this deprecation and future removal, please comment on -`issue #3974 `__. ``pytest.config`` global @@ -103,6 +69,43 @@ Removed Features As stated in our :ref:`backwards-compatibility` policy, deprecated features are removed only in major releases after an appropriate period of deprecation has passed. + +.. _`raises message deprecated`: + +``"message"`` parameter of ``pytest.raises`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 5.0 + +It is a common mistake to think this parameter will match the exception message, while in fact +it only serves to provide a custom message in case the ``pytest.raises`` check fails. To prevent +users from making this mistake, and because it is believed to be little used, pytest is +deprecating it without providing an alternative for the moment. + +If you have a valid use case for this parameter, consider that to obtain the same results +you can just call ``pytest.fail`` manually at the end of the ``with`` statement. + +For example: + +.. code-block:: python + + with pytest.raises(TimeoutError, message="Client got unexpected message"): + wait_for(websocket.recv(), 0.5) + + +Becomes: + +.. code-block:: python + + with pytest.raises(TimeoutError): + wait_for(websocket.recv(), 0.5) + pytest.fail("Client got unexpected message") + + +If you still have concerns about this deprecation and future removal, please comment on +`issue #3974 `__. + + .. _raises-warns-exec: ``raises`` / ``warns`` with a string as the second argument diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 77c7c30ff..bbb073520 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -42,12 +42,6 @@ FUNCARGNAMES = PytestDeprecationWarning( "since pytest 2.3 - use the newer attribute instead." ) -RAISES_MESSAGE_PARAMETER = PytestDeprecationWarning( - "The 'message' parameter is deprecated.\n" - "(did you mean to use `match='some regex'` to check the exception message?)\n" - "Please see:\n" - " https://docs.pytest.org/en/4.6-maintenance/deprecations.html#message-parameter-of-pytest-raises" -) RESULT_LOG = PytestDeprecationWarning( "--result-log is deprecated and scheduled for removal in pytest 6.0.\n" diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index f709057a3..9f5220766 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -1,7 +1,6 @@ import inspect import math import pprint -import warnings from collections.abc import Iterable from collections.abc import Mapping from collections.abc import Sized @@ -12,7 +11,6 @@ from numbers import Number from more_itertools.more import always_iterable import _pytest._code -from _pytest import deprecated from _pytest.compat import STRING_TYPES from _pytest.outcomes import fail @@ -538,8 +536,6 @@ def raises(expected_exception, *args, **kwargs): __ https://docs.python.org/3/library/re.html#regular-expression-syntax - :kwparam message: **(deprecated since 4.1)** if specified, provides a custom failure message - if the exception is not raised. See :ref:`the deprecation docs ` for a workaround. .. currentmodule:: _pytest._code @@ -656,9 +652,6 @@ def raises(expected_exception, *args, **kwargs): match_expr = None if not args: - if "message" in kwargs: - message = kwargs.pop("message") - warnings.warn(deprecated.RAISES_MESSAGE_PARAMETER, stacklevel=2) if "match" in kwargs: match_expr = kwargs.pop("match") if kwargs: diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 4ce89113d..4db4a9c98 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -76,12 +76,6 @@ def test_external_plugins_integrated(testdir, plugin): testdir.parseconfig("-p", plugin) -def test_raises_message_argument_deprecated(): - with pytest.warns(pytest.PytestDeprecationWarning): - with pytest.raises(RuntimeError, message="foobar"): - raise RuntimeError - - def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST diff --git a/testing/python/raises.py b/testing/python/raises.py index 3ace96e7a..4f514e322 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -2,7 +2,6 @@ import sys import pytest from _pytest.outcomes import Failed -from _pytest.warning_types import PytestDeprecationWarning class TestRaises: @@ -155,17 +154,6 @@ class TestRaises: else: assert False, "Expected pytest.raises.Exception" - def test_custom_raise_message(self): - message = "TEST_MESSAGE" - try: - with pytest.warns(PytestDeprecationWarning): - with pytest.raises(ValueError, message=message): - pass - except pytest.raises.Exception as e: - assert e.msg == message - else: - assert False, "Expected pytest.raises.Exception" - @pytest.mark.parametrize("method", ["function", "with"]) def test_raises_cyclic_reference(self, method): """ From 683b2632b4e32ed935854e42bafd18b0178b4368 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 12:02:11 -0300 Subject: [PATCH 04/70] Remove explicit kwargs handling from raises, warns and ParameterSet.param --- changelog/5180.removal.rst | 4 ++++ src/_pytest/deprecated.py | 13 ------------- src/_pytest/mark/structures.py | 20 ++++++-------------- src/_pytest/python_api.py | 8 +++----- src/_pytest/recwarn.py | 13 ++++++------- testing/deprecated_test.py | 9 --------- testing/test_mark.py | 13 ------------- 7 files changed, 19 insertions(+), 61 deletions(-) diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst index 2417672be..e72b76d5a 100644 --- a/changelog/5180.removal.rst +++ b/changelog/5180.removal.rst @@ -7,6 +7,10 @@ removed: * ``message`` parameter of ``pytest.raises``. +* ``pytest.raises``, ``pytest.warns`` and ``ParameterSet.param`` now use native keyword-only + syntax. This might change the exception message from previous versions, but they still raise + ``TypeError`` on unknown keyword arguments as before. + For more information consult `Deprecations and Removals `__ in the docs. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index bbb073520..dd2d65d15 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -10,7 +10,6 @@ in case of warnings which need to format their messages. """ from _pytest.warning_types import PytestDeprecationWarning from _pytest.warning_types import RemovedInPytest4Warning -from _pytest.warning_types import UnformattedWarning YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored" @@ -73,15 +72,3 @@ PYTEST_LOGWARNING = PytestDeprecationWarning( "pytest_logwarning is deprecated, no longer being called, and will be removed soon\n" "please use pytest_warning_captured instead" ) - -PYTEST_WARNS_UNKNOWN_KWARGS = UnformattedWarning( - PytestDeprecationWarning, - "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/mark/structures.py b/src/_pytest/mark/structures.py index 1af7a9b42..aa4c8f828 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -9,7 +9,6 @@ import attr from ..compat import ascii_escaped from ..compat import getfslineno from ..compat import NOTSET -from _pytest.deprecated import PYTEST_PARAM_UNKNOWN_KWARGS from _pytest.outcomes import fail from _pytest.warning_types import PytestUnknownMarkWarning @@ -61,26 +60,19 @@ def get_empty_parameterset_mark(config, argnames, func): class ParameterSet(namedtuple("ParameterSet", "values, marks, id")): @classmethod - def param(cls, *values, **kwargs): - marks = kwargs.pop("marks", ()) + def param(cls, *values, marks=(), id=None): if isinstance(marks, MarkDecorator): marks = (marks,) else: assert isinstance(marks, (tuple, list, set)) - id_ = kwargs.pop("id", None) - if id_ is not None: - if not isinstance(id_, str): + if id is not None: + if not isinstance(id, str): raise TypeError( - "Expected id to be a string, got {}: {!r}".format(type(id_), id_) + "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_) + id = ascii_escaped(id) + return cls(values, marks, id) @classmethod def extract_from(cls, parameterset, force_tuple=False): diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 9f5220766..3266188a1 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -523,7 +523,7 @@ def _is_numpy_array(obj): # builtin pytest.raises helper -def raises(expected_exception, *args, **kwargs): +def raises(expected_exception, *args, match=None, **kwargs): r""" Assert that a code block/function call raises ``expected_exception`` or raise a failure exception otherwise. @@ -649,16 +649,14 @@ def raises(expected_exception, *args, **kwargs): raise TypeError(msg % type(exc)) message = "DID NOT RAISE {}".format(expected_exception) - match_expr = None if not args: - if "match" in kwargs: - match_expr = kwargs.pop("match") if kwargs: msg = "Unexpected keyword arguments passed to pytest.raises: " msg += ", ".join(sorted(kwargs)) + msg += "\nUse context-manager form instead?" raise TypeError(msg) - return RaisesContext(expected_exception, message, match_expr) + return RaisesContext(expected_exception, message, match) else: func = args[0] if not callable(func): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 8733a893a..8a1cad4a1 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -3,7 +3,6 @@ import inspect import re import warnings -from _pytest.deprecated import PYTEST_WARNS_UNKNOWN_KWARGS from _pytest.fixtures import yield_fixture from _pytest.outcomes import fail @@ -43,7 +42,7 @@ def deprecated_call(func=None, *args, **kwargs): return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) -def warns(expected_warning, *args, **kwargs): +def warns(expected_warning, *args, match=None, **kwargs): r"""Assert that code raises a particular class of warning. Specifically, the parameter ``expected_warning`` can be a warning class or @@ -77,12 +76,12 @@ def warns(expected_warning, *args, **kwargs): """ __tracebackhide__ = True if not args: - match_expr = kwargs.pop("match", None) if kwargs: - warnings.warn( - PYTEST_WARNS_UNKNOWN_KWARGS.format(args=sorted(kwargs)), stacklevel=2 - ) - return WarningsChecker(expected_warning, match_expr=match_expr) + msg = "Unexpected keyword arguments passed to pytest.warns: " + msg += ", ".join(sorted(kwargs)) + msg += "\nUse context-manager form instead?" + raise TypeError(msg) + return WarningsChecker(expected_warning, match_expr=match) else: func = args[0] if not callable(func): diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 4db4a9c98..b21a08703 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -2,7 +2,6 @@ import os import pytest from _pytest import deprecated -from _pytest.warning_types import PytestDeprecationWarning from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG pytestmark = pytest.mark.pytester_example_path("deprecated") @@ -199,11 +198,3 @@ def test_fixture_named_request(testdir): "*'request' is a reserved name for fixtures and will raise an error in future versions" ] ) - - -def test_pytest_warns_unknown_kwargs(): - with pytest.warns( - PytestDeprecationWarning, - match=r"pytest.warns\(\) got unexpected keyword arguments: \['foo'\]", - ): - pytest.warns(UserWarning, foo="hello") diff --git a/testing/test_mark.py b/testing/test_mark.py index 1544ffe56..ce5991563 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -8,7 +8,6 @@ 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 ignore_markinfo = pytest.mark.filterwarnings( @@ -1004,15 +1003,3 @@ 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." - ) From 647d89c44495a8105c2eec947a0c11fb55bc9e3c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 12:15:29 -0300 Subject: [PATCH 05/70] Move code about 'pytest_plugins' error to a more appropriate place It is no longer deprecated, but part of the normal code for 'config' --- src/_pytest/config/__init__.py | 24 ++++--- src/_pytest/deprecated.py | 10 --- testing/deprecated_test.py | 118 --------------------------------- testing/test_config.py | 114 +++++++++++++++++++++++++++++++ 4 files changed, 128 insertions(+), 138 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c1bd2e7eb..cf6ecd385 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -204,6 +204,19 @@ def _prepareconfig(args=None, plugins=None): raise +def _fail_on_non_top_pytest_plugins(conftestpath, confcutdir): + msg = ( + "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported:\n" + "It affects the entire test suite instead of just below the conftest as expected.\n" + " {}\n" + "Please move it to a top level conftest file at the rootdir:\n" + " {}\n" + "For more information, visit:\n" + " https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" + ) + fail(msg.format(conftestpath, confcutdir), pytrace=False) + + class PytestPluginManager(PluginManager): """ Overwrites :py:class:`pluggy.PluginManager ` to add pytest-specific @@ -424,16 +437,7 @@ class PytestPluginManager(PluginManager): and self._configured and not self._using_pyargs ): - from _pytest.deprecated import ( - PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST, - ) - - fail( - PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST.format( - conftestpath, self._confcutdir - ), - pytrace=False, - ) + _fail_on_non_top_pytest_plugins(conftestpath, self._confcutdir) except Exception: raise ConftestImportFailure(conftestpath, sys.exc_info()) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index dd2d65d15..2598b2a9c 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -48,16 +48,6 @@ RESULT_LOG = PytestDeprecationWarning( ) -PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST = ( - "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported " - "because it affects the entire directory tree in a non-explicit way.\n" - " {}\n" - "Please move it to a top level conftest file at the rootdir:\n" - " {}\n" - "For more information, visit:\n" - " https://docs.pytest.org/en/latest/deprecations.html#pytest-plugins-in-non-top-level-conftest-files" -) - PYTEST_CONFIG_GLOBAL = PytestDeprecationWarning( "the `pytest.config` global is deprecated. Please use `request.config` " "or `pytest_configure` (if you're a pytest plugin) instead." diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index b21a08703..1730e740a 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,8 +1,5 @@ -import os - import pytest from _pytest import deprecated -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG pytestmark = pytest.mark.pytester_example_path("deprecated") @@ -75,121 +72,6 @@ def test_external_plugins_integrated(testdir, plugin): testdir.parseconfig("-p", plugin) -def test_pytest_plugins_in_non_top_level_conftest_deprecated(testdir): - from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST - - testdir.makepyfile( - **{ - "subdirectory/conftest.py": """ - pytest_plugins=['capture'] - """ - } - ) - testdir.makepyfile( - """ - def test_func(): - pass - """ - ) - res = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) - assert res.ret == 2 - msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] - res.stdout.fnmatch_lines( - ["*{msg}*".format(msg=msg), "*subdirectory{sep}conftest.py*".format(sep=os.sep)] - ) - - -@pytest.mark.parametrize("use_pyargs", [True, False]) -def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( - testdir, use_pyargs -): - """When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)""" - from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST - - files = { - "src/pkg/__init__.py": "", - "src/pkg/conftest.py": "", - "src/pkg/test_root.py": "def test(): pass", - "src/pkg/sub/__init__.py": "", - "src/pkg/sub/conftest.py": "pytest_plugins=['capture']", - "src/pkg/sub/test_bar.py": "def test(): pass", - } - testdir.makepyfile(**files) - testdir.syspathinsert(testdir.tmpdir.join("src")) - - args = ("--pyargs", "pkg") if use_pyargs else () - args += (SHOW_PYTEST_WARNINGS_ARG,) - res = testdir.runpytest(*args) - assert res.ret == (0 if use_pyargs else 2) - msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] - if use_pyargs: - assert msg not in res.stdout.str() - else: - res.stdout.fnmatch_lines(["*{msg}*".format(msg=msg)]) - - -def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( - testdir -): - from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST - - subdirectory = testdir.tmpdir.join("subdirectory") - subdirectory.mkdir() - testdir.makeconftest( - """ - pytest_plugins=['capture'] - """ - ) - testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) - - testdir.makepyfile( - """ - def test_func(): - pass - """ - ) - - res = testdir.runpytest_subprocess() - assert res.ret == 2 - msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] - res.stdout.fnmatch_lines( - ["*{msg}*".format(msg=msg), "*subdirectory{sep}conftest.py*".format(sep=os.sep)] - ) - - -def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( - testdir -): - from _pytest.deprecated import PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST - - subdirectory = testdir.tmpdir.join("subdirectory") - subdirectory.mkdir() - testdir.makeconftest( - """ - pass - """ - ) - testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) - - testdir.makeconftest( - """ - import warnings - warnings.filterwarnings('always', category=DeprecationWarning) - pytest_plugins=['capture'] - """ - ) - testdir.makepyfile( - """ - def test_func(): - pass - """ - ) - res = testdir.runpytest_subprocess() - assert res.ret == 0 - msg = str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0] - assert msg not in res.stdout.str() - - def test_fixture_named_request(testdir): testdir.copy_example() result = testdir.runpytest() diff --git a/testing/test_config.py b/testing/test_config.py index ff993e401..76202ca33 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,3 +1,4 @@ +import os import sys import textwrap @@ -1242,3 +1243,116 @@ def test_config_blocked_default_plugins(testdir, plugin): result.stdout.fnmatch_lines(["* 1 failed in *"]) else: assert result.stdout.lines == [""] + + +class TestPytestPluginsVariable: + def test_pytest_plugins_in_non_top_level_conftest_unsupported(self, testdir): + testdir.makepyfile( + **{ + "subdirectory/conftest.py": """ + pytest_plugins=['capture'] + """ + } + ) + testdir.makepyfile( + """ + def test_func(): + pass + """ + ) + res = testdir.runpytest() + assert res.ret == 2 + msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" + res.stdout.fnmatch_lines( + [ + "*{msg}*".format(msg=msg), + "*subdirectory{sep}conftest.py*".format(sep=os.sep), + ] + ) + + @pytest.mark.parametrize("use_pyargs", [True, False]) + def test_pytest_plugins_in_non_top_level_conftest_unsupported_pyargs( + self, testdir, use_pyargs + ): + """When using --pyargs, do not emit the warning about non-top-level conftest warnings (#4039, #4044)""" + + files = { + "src/pkg/__init__.py": "", + "src/pkg/conftest.py": "", + "src/pkg/test_root.py": "def test(): pass", + "src/pkg/sub/__init__.py": "", + "src/pkg/sub/conftest.py": "pytest_plugins=['capture']", + "src/pkg/sub/test_bar.py": "def test(): pass", + } + testdir.makepyfile(**files) + testdir.syspathinsert(testdir.tmpdir.join("src")) + + args = ("--pyargs", "pkg") if use_pyargs else () + res = testdir.runpytest(*args) + assert res.ret == (0 if use_pyargs else 2) + msg = ( + msg + ) = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" + if use_pyargs: + assert msg not in res.stdout.str() + else: + res.stdout.fnmatch_lines(["*{msg}*".format(msg=msg)]) + + def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_top_level_conftest( + self, testdir + ): + subdirectory = testdir.tmpdir.join("subdirectory") + subdirectory.mkdir() + testdir.makeconftest( + """ + pytest_plugins=['capture'] + """ + ) + testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) + + testdir.makepyfile( + """ + def test_func(): + pass + """ + ) + + res = testdir.runpytest_subprocess() + assert res.ret == 2 + msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" + res.stdout.fnmatch_lines( + [ + "*{msg}*".format(msg=msg), + "*subdirectory{sep}conftest.py*".format(sep=os.sep), + ] + ) + + def test_pytest_plugins_in_non_top_level_conftest_unsupported_no_false_positives( + self, testdir + ): + subdirectory = testdir.tmpdir.join("subdirectory") + subdirectory.mkdir() + testdir.makeconftest( + """ + pass + """ + ) + testdir.tmpdir.join("conftest.py").move(subdirectory.join("conftest.py")) + + testdir.makeconftest( + """ + import warnings + warnings.filterwarnings('always', category=DeprecationWarning) + pytest_plugins=['capture'] + """ + ) + testdir.makepyfile( + """ + def test_func(): + pass + """ + ) + res = testdir.runpytest_subprocess() + assert res.ret == 0 + msg = "Defining 'pytest_plugins' in a non-top-level conftest is no longer supported" + assert msg not in res.stdout.str() From f2b7809d5d2c04afc3f04f6989e0b327ca753b88 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 12:18:14 -0300 Subject: [PATCH 06/70] Move setup.cfg error message and tests to an appropriate location Those are not really deprecated anymore and are part of the normal code for config --- src/_pytest/config/findpaths.py | 7 +++---- src/_pytest/deprecated.py | 2 -- testing/deprecated_test.py | 24 ------------------------ testing/test_config.py | 24 ++++++++++++++++++++++++ 4 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/_pytest/config/findpaths.py b/src/_pytest/config/findpaths.py index fa2024470..abb248b1d 100644 --- a/src/_pytest/config/findpaths.py +++ b/src/_pytest/config/findpaths.py @@ -20,8 +20,6 @@ def getcfg(args, config=None): note: config is optional and used only to issue warnings explicitly (#2891). """ - from _pytest.deprecated import CFG_PYTEST_SECTION - inibasenames = ["pytest.ini", "tox.ini", "setup.cfg"] args = [x for x in args if not str(x).startswith("-")] if not args: @@ -97,6 +95,9 @@ def get_dirs_from_args(args): return [get_dir_from_path(path) for path in possible_paths if path.exists()] +CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." + + def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): dirs = get_dirs_from_args(args) if inifile: @@ -107,8 +108,6 @@ def determine_setup(inifile, args, rootdir_cmd_arg=None, config=None): try: inicfg = iniconfig[section] if is_cfg_file and section == "pytest" and config is not None: - from _pytest.deprecated import CFG_PYTEST_SECTION - fail( CFG_PYTEST_SECTION.format(filename=str(inifile)), pytrace=False ) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 2598b2a9c..0cce0efb2 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -33,8 +33,6 @@ FIXTURE_NAMED_REQUEST = PytestDeprecationWarning( "'request' is a reserved name for fixtures and will raise an error in future versions" ) -CFG_PYTEST_SECTION = "[pytest] section in {filename} files is no longer supported, change to [tool:pytest] instead." - FUNCARGNAMES = PytestDeprecationWarning( "The `funcargnames` attribute was an alias for `fixturenames`, " diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 1730e740a..e4a8f72bc 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -4,30 +4,6 @@ from _pytest import deprecated pytestmark = pytest.mark.pytester_example_path("deprecated") -def test_pytest_setup_cfg_unsupported(testdir): - testdir.makefile( - ".cfg", - setup=""" - [pytest] - addopts = --verbose - """, - ) - with pytest.raises(pytest.fail.Exception): - testdir.runpytest() - - -def test_pytest_custom_cfg_unsupported(testdir): - testdir.makefile( - ".cfg", - custom=""" - [pytest] - addopts = --verbose - """, - ) - with pytest.raises(pytest.fail.Exception): - testdir.runpytest("-c", "custom.cfg") - - @pytest.mark.filterwarnings("default") def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--help") diff --git a/testing/test_config.py b/testing/test_config.py index 76202ca33..9533144e7 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1245,6 +1245,30 @@ def test_config_blocked_default_plugins(testdir, plugin): assert result.stdout.lines == [""] +class TestSetupCfg: + def test_pytest_setup_cfg_unsupported(self, testdir): + testdir.makefile( + ".cfg", + setup=""" + [pytest] + addopts = --verbose + """, + ) + with pytest.raises(pytest.fail.Exception): + testdir.runpytest() + + def test_pytest_custom_cfg_unsupported(self, testdir): + testdir.makefile( + ".cfg", + custom=""" + [pytest] + addopts = --verbose + """, + ) + with pytest.raises(pytest.fail.Exception): + testdir.runpytest("-c", "custom.cfg") + + class TestPytestPluginsVariable: def test_pytest_plugins_in_non_top_level_conftest_unsupported(self, testdir): testdir.makepyfile( From 7e58defc15e92d6f5af2c3bd5367b4ec877c8086 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 12:24:51 -0300 Subject: [PATCH 07/70] Remove 'pytest.config' --- changelog/5180.removal.rst | 2 ++ doc/en/deprecations.rst | 25 ++++++++++++------------- src/_pytest/deprecated.py | 5 ----- src/_pytest/main.py | 22 ---------------------- testing/test_pytester.py | 3 +-- 5 files changed, 15 insertions(+), 42 deletions(-) diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst index e72b76d5a..49f896f94 100644 --- a/changelog/5180.removal.rst +++ b/changelog/5180.removal.rst @@ -11,6 +11,8 @@ removed: syntax. This might change the exception message from previous versions, but they still raise ``TypeError`` on unknown keyword arguments as before. +* ``pytest.config`` global variable. + For more information consult `Deprecations and Removals `__ in the docs. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index c97061f09..052ab836a 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -34,19 +34,6 @@ in places where we or plugin authors must distinguish between fixture names and names supplied by non-fixture things such as ``pytest.mark.parametrize``. - - -``pytest.config`` global -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 4.1 - -The ``pytest.config`` global object is deprecated. Instead use -``request.config`` (via the ``request`` fixture) or if you are a plugin author -use the ``pytest_configure(config)`` hook. Note that many hooks can also access -the ``config`` object indirectly, through ``session.config`` or ``item.config`` for example. - - Result log (``--result-log``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -63,6 +50,7 @@ stable. The actual alternative is still being discussed in issue `#4488 `__. + Removed Features ---------------- @@ -70,6 +58,17 @@ As stated in our :ref:`backwards-compatibility` policy, deprecated features are an appropriate period of deprecation has passed. +``pytest.config`` global +~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 5.0 + +The ``pytest.config`` global object is deprecated. Instead use +``request.config`` (via the ``request`` fixture) or if you are a plugin author +use the ``pytest_configure(config)`` hook. Note that many hooks can also access +the ``config`` object indirectly, through ``session.config`` or ``item.config`` for example. + + .. _`raises message deprecated`: ``"message"`` parameter of ``pytest.raises`` diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 0cce0efb2..2c853d48b 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -46,11 +46,6 @@ RESULT_LOG = PytestDeprecationWarning( ) -PYTEST_CONFIG_GLOBAL = PytestDeprecationWarning( - "the `pytest.config` global is deprecated. Please use `request.config` " - "or `pytest_configure` (if you're a pytest plugin) instead." -) - PYTEST_ENSURETEMP = RemovedInPytest4Warning( "pytest/tmpdir_factory.ensuretemp is deprecated, \n" "please use the tmp_path fixture or tmp_path_factory.mktemp" diff --git a/src/_pytest/main.py b/src/_pytest/main.py index f28bc68db..a5283d737 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -5,7 +5,6 @@ import functools import importlib import os import sys -import warnings import attr import py @@ -15,7 +14,6 @@ from _pytest import nodes from _pytest.config import directory_arg from _pytest.config import hookimpl from _pytest.config import UsageError -from _pytest.deprecated import PYTEST_CONFIG_GLOBAL from _pytest.outcomes import exit from _pytest.runner import collect_one_node @@ -179,26 +177,6 @@ def pytest_addoption(parser): ) -class _ConfigDeprecated: - def __init__(self, config): - self.__dict__["_config"] = config - - def __getattr__(self, attr): - warnings.warn(PYTEST_CONFIG_GLOBAL, stacklevel=2) - return getattr(self._config, attr) - - def __setattr__(self, attr, val): - warnings.warn(PYTEST_CONFIG_GLOBAL, stacklevel=2) - return setattr(self._config, attr, val) - - def __repr__(self): - return "{}({!r})".format(type(self).__name__, self._config) - - -def pytest_configure(config): - __import__("pytest").config = _ConfigDeprecated(config) # compatibility - - def wrap_session(config, doit): """Skeleton command line program""" session = Session(config) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 37b63f31a..f115ad3d0 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -72,8 +72,7 @@ def test_make_hook_recorder(testdir): def test_parseconfig(testdir): config1 = testdir.parseconfig() config2 = testdir.parseconfig() - assert config2 != config1 - assert config1 != pytest.config + assert config2 is not config1 def test_testdir_runs_with_plugin(testdir): From aa1955de72dbca1a907d4692487f707da14b5d96 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 12:27:29 -0300 Subject: [PATCH 08/70] Remove 'tmpdir_factory.ensuretemp' --- changelog/5180.removal.rst | 2 ++ src/_pytest/deprecated.py | 6 ------ src/_pytest/tmpdir.py | 15 --------------- testing/test_tmpdir.py | 16 ++++++---------- 4 files changed, 8 insertions(+), 31 deletions(-) diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst index 49f896f94..dfe9c5f8d 100644 --- a/changelog/5180.removal.rst +++ b/changelog/5180.removal.rst @@ -13,6 +13,8 @@ removed: * ``pytest.config`` global variable. +* ``tmpdir_factory.ensuretemp`` method. + For more information consult `Deprecations and Removals `__ in the docs. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 2c853d48b..58ffee1ae 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -9,7 +9,6 @@ All constants defined in this module should be either PytestWarning instances or in case of warnings which need to format their messages. """ from _pytest.warning_types import PytestDeprecationWarning -from _pytest.warning_types import RemovedInPytest4Warning YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored" @@ -46,11 +45,6 @@ RESULT_LOG = PytestDeprecationWarning( ) -PYTEST_ENSURETEMP = RemovedInPytest4Warning( - "pytest/tmpdir_factory.ensuretemp is deprecated, \n" - "please use the tmp_path fixture or tmp_path_factory.mktemp" -) - PYTEST_LOGWARNING = PytestDeprecationWarning( "pytest_logwarning is deprecated, no longer being called, and will be removed soon\n" "please use pytest_warning_captured instead" diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index f2c4d905c..c7a61b0d4 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -2,7 +2,6 @@ import os import re import tempfile -import warnings import attr import py @@ -85,19 +84,6 @@ class TempdirFactory: _tmppath_factory = attr.ib() - def ensuretemp(self, string, dir=1): - """ (deprecated) return temporary directory path with - the given string as the trailing part. It is usually - better to use the 'tmpdir' function argument which - provides an empty unique-per-test-invocation directory - and is guaranteed to be empty. - """ - # py.log._apiwarn(">1.1", "use tmpdir function argument") - from .deprecated import PYTEST_ENSURETEMP - - warnings.warn(PYTEST_ENSURETEMP, stacklevel=2) - return self.getbasetemp().ensure(string, dir=dir) - def mktemp(self, basename, numbered=True): """Create a subdirectory of the base temporary directory and return it. If ``numbered``, ensure the directory is unique by adding a number @@ -135,7 +121,6 @@ def pytest_configure(config): config._cleanup.append(mp.undo) mp.setattr(config, "_tmp_path_factory", tmppath_handler, raising=False) mp.setattr(config, "_tmpdirhandler", t, raising=False) - mp.setattr(pytest, "ensuretemp", t.ensuretemp, raising=False) @pytest.fixture(scope="session") diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index c4c7ebe25..f11ddbe68 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -14,13 +14,6 @@ def test_tmpdir_fixture(testdir): results.stdout.fnmatch_lines(["*1 passed*"]) -def test_ensuretemp(recwarn): - d1 = pytest.ensuretemp("hello") - d2 = pytest.ensuretemp("hello") - assert d1 == d2 - assert d1.check(dir=1) - - @attr.s class FakeConfig: basetemp = attr.ib() @@ -85,12 +78,15 @@ def test_basetemp(testdir): p = testdir.makepyfile( """ import pytest - def test_1(): - pytest.ensuretemp("hello") + def test_1(tmpdir_factory): + tmpdir_factory.mktemp('hello', numbered=False) """ ) - result = testdir.runpytest(p, "--basetemp=%s" % mytemp, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest( + p, "--basetemp=%s" % mytemp, SHOW_PYTEST_WARNINGS_ARG, "-s" + ) assert result.ret == 0 + print(mytemp) assert mytemp.join("hello").check() From 7b354050335caaf8316cf5cf17d45af64649dbb1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 12:28:40 -0300 Subject: [PATCH 09/70] Remove YIELD_TESTS from deprecated module as it is now an error --- src/_pytest/deprecated.py | 2 -- src/_pytest/python.py | 5 +++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 58ffee1ae..fff7af2f9 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -10,8 +10,6 @@ in case of warnings which need to format their messages. """ from _pytest.warning_types import PytestDeprecationWarning -YIELD_TESTS = "yield tests were removed in pytest 4.0 - {name} will be ignored" - # set of plugins which have been integrated into the core; we use this list to ignore # them during registration to avoid conflicts DEPRECATED_EXTERNAL_PLUGINS = { diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 66d853060..801a16a5c 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -12,7 +12,6 @@ from textwrap import dedent import py import _pytest -from _pytest import deprecated from _pytest import fixtures from _pytest import nodes from _pytest._code import filter_traceback @@ -218,7 +217,9 @@ def pytest_pycollect_makeitem(collector, name, obj): elif getattr(obj, "__test__", True): if is_generator(obj): res = Function(name, parent=collector) - reason = deprecated.YIELD_TESTS.format(name=name) + reason = "yield tests were removed in pytest 4.0 - {name} will be ignored".format( + name=name + ) res.add_marker(MARK_GEN.xfail(run=False, reason=reason)) res.warn(PytestCollectionWarning(reason)) else: From 3a17c1b30bc59320f1601c0a54a039098a021c88 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 12:30:08 -0300 Subject: [PATCH 10/70] Remove 'pytest_logwarning' hook --- changelog/5180.removal.rst | 2 ++ src/_pytest/deprecated.py | 6 ------ src/_pytest/hookspec.py | 22 ---------------------- 3 files changed, 2 insertions(+), 28 deletions(-) diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst index dfe9c5f8d..c78bbc876 100644 --- a/changelog/5180.removal.rst +++ b/changelog/5180.removal.rst @@ -15,6 +15,8 @@ removed: * ``tmpdir_factory.ensuretemp`` method. +* ``pytest_logwarning`` hook. + For more information consult `Deprecations and Removals `__ in the docs. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index fff7af2f9..3d3f73a60 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -41,9 +41,3 @@ RESULT_LOG = PytestDeprecationWarning( "--result-log is deprecated and scheduled for removal in pytest 6.0.\n" "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." ) - - -PYTEST_LOGWARNING = PytestDeprecationWarning( - "pytest_logwarning is deprecated, no longer being called, and will be removed soon\n" - "please use pytest_warning_captured instead" -) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 9e6d13fab..59fc569f4 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -1,7 +1,6 @@ """ hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ from pluggy import HookspecMarker -from _pytest.deprecated import PYTEST_LOGWARNING hookspec = HookspecMarker("pytest") @@ -575,27 +574,6 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config): """ -@hookspec(historic=True, warn_on_impl=PYTEST_LOGWARNING) -def pytest_logwarning(message, code, nodeid, fslocation): - """ - .. deprecated:: 3.8 - - This hook is will stop working in a future release. - - pytest no longer triggers this hook, but the - terminal writer still implements it to display warnings issued by - :meth:`_pytest.config.Config.warn` and :meth:`_pytest.nodes.Node.warn`. Calling those functions will be - an error in future releases. - - process a warning specified by a message, a code string, - a nodeid and fslocation (both of which may be None - if the warning is not tied to a particular node/location). - - .. note:: - This hook is incompatible with ``hookwrapper=True``. - """ - - @hookspec(historic=True) def pytest_warning_captured(warning_message, when, item): """ From c470ade0a5a6ac8bfa23ec66a0f72c2e171b3db5 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 12:56:27 -0300 Subject: [PATCH 11/70] Remove 'RemovedInPytest4Warning' --- changelog/5180.removal.rst | 2 ++ doc/en/warnings.rst | 2 -- src/_pytest/warning_types.py | 10 --------- src/_pytest/warnings.py | 5 ----- src/pytest.py | 3 +-- testing/acceptance_test.py | 7 ++----- testing/python/fixtures.py | 3 +-- testing/python/metafunc.py | 27 ++++++++++++------------- testing/test_mark.py | 12 +++-------- testing/test_tmpdir.py | 5 +---- testing/test_warnings.py | 39 ++++++++---------------------------- tox.ini | 3 --- 12 files changed, 31 insertions(+), 87 deletions(-) diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst index c78bbc876..112c0150f 100644 --- a/changelog/5180.removal.rst +++ b/changelog/5180.removal.rst @@ -17,6 +17,8 @@ removed: * ``pytest_logwarning`` hook. +* ``RemovedInPytest4Warning`` warning type. + For more information consult `Deprecations and Removals `__ in the docs. diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 8f8d6f3e1..c16aefc21 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -430,5 +430,3 @@ The following warning types ares used by pytest and are part of the public API: .. autoclass:: pytest.PytestUnhandledCoroutineWarning .. autoclass:: pytest.PytestUnknownMarkWarning - -.. autoclass:: pytest.RemovedInPytest4Warning diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index ac7e5ca48..80353ccbc 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -103,16 +103,6 @@ class PytestUnknownMarkWarning(PytestWarning): __module__ = "pytest" -class RemovedInPytest4Warning(PytestDeprecationWarning): - """ - Bases: :class:`pytest.PytestDeprecationWarning`. - - Warning class for features scheduled to be removed in pytest 4.0. - """ - - __module__ = "pytest" - - @attr.s class UnformattedWarning: """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index f47eee0d4..63d22477c 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -4,8 +4,6 @@ from contextlib import contextmanager import pytest -SHOW_PYTEST_WARNINGS_ARG = "-Walways::pytest.RemovedInPytest4Warning" - def _setoption(wmod, arg): """ @@ -74,9 +72,6 @@ def catch_warnings_for_item(config, ihook, when, item): warnings.filterwarnings("always", category=DeprecationWarning) warnings.filterwarnings("always", category=PendingDeprecationWarning) - warnings.filterwarnings("error", category=pytest.RemovedInPytest4Warning) - warnings.filterwarnings("error", category=pytest.PytestDeprecationWarning) - # filters should have this precedence: mark, cmdline options, ini # filters should be applied in the inverse order of precedence for arg in inifilters: diff --git a/src/pytest.py b/src/pytest.py index b4faf4978..b934e65cb 100644 --- a/src/pytest.py +++ b/src/pytest.py @@ -44,7 +44,7 @@ from _pytest.warning_types import PytestExperimentalApiWarning from _pytest.warning_types import PytestUnhandledCoroutineWarning from _pytest.warning_types import PytestUnknownMarkWarning from _pytest.warning_types import PytestWarning -from _pytest.warning_types import RemovedInPytest4Warning + set_trace = __pytestPDB.set_trace @@ -84,7 +84,6 @@ __all__ = [ "PytestWarning", "raises", "register_assert_rewrite", - "RemovedInPytest4Warning", "Session", "set_trace", "skip", diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index d2a348f40..803856ff8 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -9,7 +9,6 @@ import py import pytest from _pytest.main import ExitCode -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG def prepend_pythonpath(*dirs): @@ -343,7 +342,7 @@ class TestGeneralUsage: """ ) p = testdir.makepyfile("""def test_func(x): pass""") - res = testdir.runpytest(p, SHOW_PYTEST_WARNINGS_ARG) + res = testdir.runpytest(p) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 skipped*"]) @@ -356,9 +355,7 @@ class TestGeneralUsage: pass """ ) - res = testdir.runpytest( - p.basename + "::" + "test_func[1]", SHOW_PYTEST_WARNINGS_ARG - ) + res = testdir.runpytest(p.basename + "::" + "test_func[1]") assert res.ret == 0 res.stdout.fnmatch_lines(["*1 passed*"]) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 63fb6294d..c2484a8a2 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -7,7 +7,6 @@ from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureRequest from _pytest.pathlib import Path from _pytest.pytester import get_public_names -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG def test_getfuncargnames(): @@ -2186,7 +2185,7 @@ class TestFixtureMarker: pass """ ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines( ["*ScopeMismatch*You tried*function*session*request*"] diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 542557252..9173105df 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -9,7 +9,6 @@ from hypothesis import strategies import pytest from _pytest import fixtures from _pytest import python -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG class TestMetafunc: @@ -915,7 +914,7 @@ class TestMetafuncFunctional: assert metafunc.cls == TestClass """ ) - result = testdir.runpytest(p, "-v", SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest(p, "-v") result.assert_outcomes(passed=2) def test_two_functions(self, testdir): @@ -931,7 +930,7 @@ class TestMetafuncFunctional: assert arg1 in (10, 20) """ ) - result = testdir.runpytest("-v", p, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest("-v", p) result.stdout.fnmatch_lines( [ "*test_func1*0*PASS*", @@ -967,7 +966,7 @@ class TestMetafuncFunctional: assert hello == "world" """ ) - result = testdir.runpytest("-v", p, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest("-v", p) result.stdout.fnmatch_lines(["*test_myfunc*hello*PASS*", "*1 passed*"]) def test_two_functions_not_same_instance(self, testdir): @@ -982,7 +981,7 @@ class TestMetafuncFunctional: self.x = 1 """ ) - result = testdir.runpytest("-v", p, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest("-v", p) result.stdout.fnmatch_lines( ["*test_func*0*PASS*", "*test_func*1*PASS*", "*2 pass*"] ) @@ -1000,7 +999,7 @@ class TestMetafuncFunctional: self.val = 1 """ ) - result = testdir.runpytest(p, SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest(p) result.assert_outcomes(passed=1) def test_parametrize_functional2(self, testdir): @@ -1522,7 +1521,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - rec = testdir.inline_run("-m", "foo", SHOW_PYTEST_WARNINGS_ARG) + rec = testdir.inline_run("-m", "foo") passed, skipped, fail = rec.listoutcomes() assert len(passed) == 1 assert len(skipped) == 0 @@ -1562,7 +1561,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() # xfail is skip?? reprec.assertoutcome(passed=2, skipped=1) @@ -1579,7 +1578,7 @@ class TestMarkersWithParametrization: assert n % 2 == 0 """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) def test_xfail_with_arg(self, testdir): @@ -1595,7 +1594,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) def test_xfail_with_kwarg(self, testdir): @@ -1611,7 +1610,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) def test_xfail_with_arg_and_kwarg(self, testdir): @@ -1627,7 +1626,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) @pytest.mark.parametrize("strict", [True, False]) @@ -1648,7 +1647,7 @@ class TestMarkersWithParametrization: strict=strict ) testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() passed, failed = (2, 1) if strict else (3, 0) reprec.assertoutcome(passed=passed, failed=failed) @@ -1672,7 +1671,7 @@ class TestMarkersWithParametrization: assert n + 1 == expected """ testdir.makepyfile(s) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=2) def test_parametrize_ID_generation_string_int_works(self, testdir): diff --git a/testing/test_mark.py b/testing/test_mark.py index ce5991563..8747d1c6b 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -8,11 +8,6 @@ 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.warnings import SHOW_PYTEST_WARNINGS_ARG - -ignore_markinfo = pytest.mark.filterwarnings( - "ignore:MarkInfo objects:pytest.RemovedInPytest4Warning" -) class TestMark: @@ -625,7 +620,6 @@ class TestFunctional: reprec = testdir.inline_run() reprec.assertoutcome(passed=1) - @ignore_markinfo def test_keyword_added_for_session(self, testdir): testdir.makeconftest( """ @@ -651,7 +645,7 @@ class TestFunctional: assert marker.kwargs == {} """ ) - reprec = testdir.inline_run("-m", "mark1", SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run("-m", "mark1") reprec.assertoutcome(passed=1) def assert_markers(self, items, **expected): @@ -689,7 +683,7 @@ class TestFunctional: assert True """ ) - reprec = testdir.inline_run(SHOW_PYTEST_WARNINGS_ARG) + reprec = testdir.inline_run() reprec.assertoutcome(skipped=1) @@ -989,7 +983,7 @@ def test_markers_from_parametrize(testdir): """ ) - result = testdir.runpytest(SHOW_PYTEST_WARNINGS_ARG) + result = testdir.runpytest() result.assert_outcomes(passed=4) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index f11ddbe68..11556594b 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -5,7 +5,6 @@ import attr import pytest from _pytest import pathlib from _pytest.pathlib import Path -from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG def test_tmpdir_fixture(testdir): @@ -82,9 +81,7 @@ def test_basetemp(testdir): tmpdir_factory.mktemp('hello', numbered=False) """ ) - result = testdir.runpytest( - p, "--basetemp=%s" % mytemp, SHOW_PYTEST_WARNINGS_ARG, "-s" - ) + result = testdir.runpytest(p, "--basetemp=%s" % mytemp) assert result.ret == 0 print(mytemp) assert mytemp.join("hello").check() diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 2ce83ae88..4d58c99e3 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -498,38 +498,15 @@ class TestDeprecationWarningsByDefault: @pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) -def test_removed_in_pytest4_warning_as_error(testdir, change_default): - testdir.makepyfile( - """ - import warnings, pytest - def test(): - warnings.warn(pytest.RemovedInPytest4Warning("some warning")) - """ - ) - if change_default == "ini": - testdir.makeini( - """ - [pytest] - filterwarnings = - ignore::pytest.RemovedInPytest4Warning - """ - ) - - args = ( - ("-Wignore::pytest.RemovedInPytest4Warning",) - if change_default == "cmdline" - else () - ) - result = testdir.runpytest(*args) - if change_default is None: - result.stdout.fnmatch_lines(["* 1 failed in *"]) - else: - assert change_default in ("ini", "cmdline") - result.stdout.fnmatch_lines(["* 1 passed in *"]) - - -@pytest.mark.parametrize("change_default", [None, "ini", "cmdline"]) +@pytest.mark.skip( + reason="This test should be enabled again before pytest 6.0 is released" +) def test_deprecation_warning_as_error(testdir, change_default): + """This ensures that PytestDeprecationWarnings raised by pytest are turned into errors. + + This test should be enabled as part of each major release, and skipped again afterwards + to ensure our deprecations are turning into warnings as expected. + """ testdir.makepyfile( """ import warnings, pytest diff --git a/tox.ini b/tox.ini index 9757b0983..832b97298 100644 --- a/tox.ini +++ b/tox.ini @@ -128,9 +128,6 @@ norecursedirs = testing/example_scripts xfail_strict=true filterwarnings = error - ignore:yield tests are deprecated, and scheduled to be removed in pytest 4.0:pytest.RemovedInPytest4Warning - ignore:Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0:pytest.RemovedInPytest4Warning - ignore::pytest.RemovedInPytest4Warning ignore:Module already imported so cannot be rewritten:pytest.PytestWarning # produced by path.local ignore:bad escape.*:DeprecationWarning:re From 85cc12e3284e2ad6126fd9c7adb69fcaf1b9be3b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 12:58:50 -0300 Subject: [PATCH 12/70] Move FIXTURE_FUNCTION_CALL constant to the point of error This is no longer a deprecation so it makes sense to move it to the place where it is needed instead of leaving it in deprecated.py --- src/_pytest/deprecated.py | 7 ------- src/_pytest/fixtures.py | 10 ++++++---- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 3d3f73a60..39c8c23cd 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -19,13 +19,6 @@ DEPRECATED_EXTERNAL_PLUGINS = { } -FIXTURE_FUNCTION_CALL = ( - 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' - "but are created automatically when test functions request them as parameters.\n" - "See https://docs.pytest.org/en/latest/fixture.html for more information about fixtures, and\n" - "https://docs.pytest.org/en/latest/deprecations.html#calling-fixtures-directly about how to update your code." -) - FIXTURE_NAMED_REQUEST = PytestDeprecationWarning( "'request' is a reserved name for fixtures and will raise an error in future versions" ) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 2cde5725c..9a385fc42 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -26,7 +26,6 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import safe_getattr -from _pytest.deprecated import FIXTURE_FUNCTION_CALL from _pytest.deprecated import FIXTURE_NAMED_REQUEST from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -933,9 +932,12 @@ def wrap_function_to_error_out_if_called_directly(function, fixture_marker): """Wrap the given fixture function so we can raise an error about it being called directly, instead of used as an argument in a test function. """ - message = FIXTURE_FUNCTION_CALL.format( - name=fixture_marker.name or function.__name__ - ) + message = ( + 'Fixture "{name}" called directly. Fixtures are not meant to be called directly,\n' + "but are created automatically when test functions request them as parameters.\n" + "See https://docs.pytest.org/en/latest/fixture.html for more information about fixtures, and\n" + "https://docs.pytest.org/en/latest/deprecations.html#calling-fixtures-directly about how to update your code." + ).format(name=fixture_marker.name or function.__name__) @functools.wraps(function) def result(*args, **kwargs): From 0ed7aa2db678fb8c35a617525efd48c3352fc256 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sun, 30 Jun 2019 13:14:38 -0300 Subject: [PATCH 13/70] Make 'request' a reserved name for fixtures --- changelog/5180.removal.rst | 2 ++ src/_pytest/compat.py | 4 +-- src/_pytest/config/__init__.py | 1 + src/_pytest/deprecated.py | 5 ---- src/_pytest/fixtures.py | 10 ++++--- testing/deprecated_test.py | 12 --------- .../test_fixture_named_request.py | 0 testing/python/fixtures.py | 26 ++++++++----------- 8 files changed, 23 insertions(+), 37 deletions(-) rename testing/example_scripts/{deprecated => fixtures}/test_fixture_named_request.py (100%) diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst index 112c0150f..1174a7cba 100644 --- a/changelog/5180.removal.rst +++ b/changelog/5180.removal.rst @@ -19,6 +19,8 @@ removed: * ``RemovedInPytest4Warning`` warning type. +* ``request`` is now a reserved name for fixtures. + For more information consult `Deprecations and Removals `__ in the docs. diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index d238061b4..6b750e16f 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -50,11 +50,11 @@ def iscoroutinefunction(func): ) -def getlocation(function, curdir): +def getlocation(function, curdir=None): function = get_real_func(function) fn = py.path.local(inspect.getfile(function)) lineno = function.__code__.co_firstlineno - if fn.relto(curdir): + if curdir is not None and fn.relto(curdir): fn = fn.relto(curdir) return "%s:%d" % (fn, lineno + 1) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index cf6ecd385..d7a20ad50 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -18,6 +18,7 @@ from pluggy import PluginManager import _pytest._code import _pytest.assertion +import _pytest.deprecated import _pytest.hookspec # the extension point definitions from .exceptions import PrintHelp from .exceptions import UsageError diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 39c8c23cd..c06908932 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -19,11 +19,6 @@ DEPRECATED_EXTERNAL_PLUGINS = { } -FIXTURE_NAMED_REQUEST = PytestDeprecationWarning( - "'request' is a reserved name for fixtures and will raise an error in future versions" -) - - FUNCARGNAMES = PytestDeprecationWarning( "The `funcargnames` attribute was an alias for `fixturenames`, " "since pytest 2.3 - use the newer attribute instead." diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9a385fc42..46ef8c642 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -2,7 +2,6 @@ import functools import inspect import itertools import sys -import warnings from collections import defaultdict from collections import deque from collections import OrderedDict @@ -26,7 +25,6 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import safe_getattr -from _pytest.deprecated import FIXTURE_NAMED_REQUEST from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -971,7 +969,13 @@ class FixtureFunctionMarker: name = self.name or function.__name__ if name == "request": - warnings.warn(FIXTURE_NAMED_REQUEST) + location = getlocation(function) + fail( + "'request' is a reserved word for fixtures, use another name:\n {}".format( + location + ), + pytrace=False, + ) function._pytestfixturefunction = self return function diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index e4a8f72bc..97b88e939 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,8 +1,6 @@ import pytest from _pytest import deprecated -pytestmark = pytest.mark.pytester_example_path("deprecated") - @pytest.mark.filterwarnings("default") def test_resultlog_is_deprecated(testdir): @@ -46,13 +44,3 @@ def test_external_plugins_integrated(testdir, plugin): with pytest.warns(pytest.PytestConfigWarning): testdir.parseconfig("-p", plugin) - - -def test_fixture_named_request(testdir): - testdir.copy_example() - result = testdir.runpytest() - result.stdout.fnmatch_lines( - [ - "*'request' is a reserved name for fixtures and will raise an error in future versions" - ] - ) diff --git a/testing/example_scripts/deprecated/test_fixture_named_request.py b/testing/example_scripts/fixtures/test_fixture_named_request.py similarity index 100% rename from testing/example_scripts/deprecated/test_fixture_named_request.py rename to testing/example_scripts/fixtures/test_fixture_named_request.py diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index c2484a8a2..e5bc25a1c 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1126,21 +1126,6 @@ class TestFixtureUsages: values = reprec.getfailedcollections() assert len(values) == 1 - def test_request_can_be_overridden(self, testdir): - testdir.makepyfile( - """ - import pytest - @pytest.fixture() - def request(request): - request.a = 1 - return request - def test_request(request): - assert request.a == 1 - """ - ) - reprec = testdir.inline_run("-Wignore::pytest.PytestDeprecationWarning") - reprec.assertoutcome(passed=1) - def test_usefixtures_marker(self, testdir): testdir.makepyfile( """ @@ -3973,3 +3958,14 @@ def test_fixture_param_shadowing(testdir): result.stdout.fnmatch_lines(["*::test_normal_fixture[[]a[]]*"]) result.stdout.fnmatch_lines(["*::test_normal_fixture[[]b[]]*"]) result.stdout.fnmatch_lines(["*::test_indirect[[]1[]]*"]) + + +def test_fixture_named_request(testdir): + testdir.copy_example("fixtures/test_fixture_named_request.py") + result = testdir.runpytest() + result.stdout.fnmatch_lines( + [ + "*'request' is a reserved word for fixtures, use another name:", + " *test_fixture_named_request.py:5", + ] + ) From dfe54cd82f55f17f3c9e6e078325f306a046b93b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 3 Jul 2019 13:57:28 -0300 Subject: [PATCH 14/70] Let context-managers for raises and warns handle unknown keyword arguments As suggested during review --- src/_pytest/python_api.py | 9 +++------ src/_pytest/recwarn.py | 7 +------ 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 3266188a1..3d52bbbcf 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -651,12 +651,9 @@ def raises(expected_exception, *args, match=None, **kwargs): message = "DID NOT RAISE {}".format(expected_exception) if not args: - if kwargs: - msg = "Unexpected keyword arguments passed to pytest.raises: " - msg += ", ".join(sorted(kwargs)) - msg += "\nUse context-manager form instead?" - raise TypeError(msg) - return RaisesContext(expected_exception, message, match) + return RaisesContext( + expected_exception, message=message, match_expr=match, **kwargs + ) else: func = args[0] if not callable(func): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 8a1cad4a1..3ab83d1e3 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -76,12 +76,7 @@ def warns(expected_warning, *args, match=None, **kwargs): """ __tracebackhide__ = True if not args: - if kwargs: - msg = "Unexpected keyword arguments passed to pytest.warns: " - msg += ", ".join(sorted(kwargs)) - msg += "\nUse context-manager form instead?" - raise TypeError(msg) - return WarningsChecker(expected_warning, match_expr=match) + return WarningsChecker(expected_warning, match_expr=match, **kwargs) else: func = args[0] if not callable(func): From 1db132290f25f53482f8d22db0afd00b8817a9d0 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 29 Jun 2019 11:13:24 -0300 Subject: [PATCH 15/70] Apply workaround for multiple short options for Python <= 3.8 Hopefully by Python 3.9 this will be fixed upstream, if not we will need to bump the version again. Fix #5523 --- changelog/5523.bugfix.rst | 1 + src/_pytest/config/argparsing.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog/5523.bugfix.rst diff --git a/changelog/5523.bugfix.rst b/changelog/5523.bugfix.rst new file mode 100644 index 000000000..5155b92b1 --- /dev/null +++ b/changelog/5523.bugfix.rst @@ -0,0 +1 @@ +Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+. diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index d62ed0d03..43cf62ab1 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -358,7 +358,7 @@ class MyOptionParser(argparse.ArgumentParser): getattr(args, FILE_OR_DIR).extend(argv) return args - if sys.version_info[:2] < (3, 8): # pragma: no cover + if sys.version_info[:2] < (3, 9): # pragma: no cover # Backport of https://github.com/python/cpython/pull/14316 so we can # disable long --argument abbreviations without breaking short flags. def _parse_optional(self, arg_string): From c54cbd63c8bc5db6626d26c0f3339a552a6b1a90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Wed, 3 Jul 2019 01:46:44 +0200 Subject: [PATCH 16/70] Replace importlib_metadata with importlib.metadata on Python 3.8+ Fixes https://github.com/pytest-dev/pytest/issues/5537 --- changelog/5537.bugfix.rst | 2 ++ setup.py | 2 +- src/_pytest/compat.py | 6 ++++++ src/_pytest/config/__init__.py | 2 +- testing/acceptance_test.py | 2 +- testing/test_assertion.py | 3 ++- testing/test_config.py | 3 +-- testing/test_entry_points.py | 2 +- 8 files changed, 15 insertions(+), 7 deletions(-) create mode 100644 changelog/5537.bugfix.rst diff --git a/changelog/5537.bugfix.rst b/changelog/5537.bugfix.rst new file mode 100644 index 000000000..0263e8cdf --- /dev/null +++ b/changelog/5537.bugfix.rst @@ -0,0 +1,2 @@ +Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the +standard library on Python 3.8+. diff --git a/setup.py b/setup.py index 4c87c6429..e8bc8e895 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ INSTALL_REQUIRES = [ 'pathlib2>=2.2.0;python_version<"3.6"', 'colorama;sys_platform=="win32"', "pluggy>=0.12,<1.0", - "importlib-metadata>=0.12", + 'importlib-metadata>=0.12;python_version<"3.8"', "wcwidth", ] diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index d238061b4..a83932c88 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -26,6 +26,12 @@ MODULE_NOT_FOUND_ERROR = ( ) +if sys.version_info >= (3, 8): + from importlib import metadata as importlib_metadata # noqa +else: + import importlib_metadata # noqa + + def _format_args(func): return str(signature(func)) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c1bd2e7eb..c9310bcb5 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -9,7 +9,6 @@ import types import warnings from functools import lru_cache -import importlib_metadata import py from packaging.version import Version from pluggy import HookimplMarker @@ -25,6 +24,7 @@ from .findpaths import determine_setup from .findpaths import exists from _pytest._code import ExceptionInfo from _pytest._code import filter_traceback +from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped from _pytest.warning_types import PytestConfigWarning diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index d2a348f40..41de690bc 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -4,10 +4,10 @@ import textwrap import types import attr -import importlib_metadata import py import pytest +from _pytest.compat import importlib_metadata from _pytest.main import ExitCode from _pytest.warnings import SHOW_PYTEST_WARNINGS_ARG diff --git a/testing/test_assertion.py b/testing/test_assertion.py index f58d240a5..8079c45a0 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -172,7 +172,8 @@ class TestImportHookInstallation: return check """, "mainwrapper.py": """\ - import pytest, importlib_metadata + import pytest + from _pytest.compat import importlib_metadata class DummyEntryPoint(object): name = 'spam' diff --git a/testing/test_config.py b/testing/test_config.py index ff993e401..eb7f95271 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,10 +1,9 @@ import sys import textwrap -import importlib_metadata - import _pytest._code import pytest +from _pytest.compat import importlib_metadata from _pytest.config import _iter_rewritable_modules from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import determine_setup diff --git a/testing/test_entry_points.py b/testing/test_entry_points.py index 9812ce998..5d0031273 100644 --- a/testing/test_entry_points.py +++ b/testing/test_entry_points.py @@ -1,4 +1,4 @@ -import importlib_metadata +from _pytest.compat import importlib_metadata def test_pytest_entry_points_are_identical(): From a43ba78d3bde1630a42aaa95776687d3886891e6 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 3 Jul 2019 20:34:12 -0300 Subject: [PATCH 17/70] Include root tag in generated XML Fix #5477 --- changelog/5477.bugfix.rst | 1 + src/_pytest/junitxml.py | 21 ++++++++++----------- testing/test_junitxml.py | 30 +++++++++++++++++++++++++++--- 3 files changed, 38 insertions(+), 14 deletions(-) create mode 100644 changelog/5477.bugfix.rst diff --git a/changelog/5477.bugfix.rst b/changelog/5477.bugfix.rst new file mode 100644 index 000000000..c9c9386e9 --- /dev/null +++ b/changelog/5477.bugfix.rst @@ -0,0 +1 @@ +The XML file produced by ``--junitxml`` now correctly contain a ```` root element. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index ea33e606c..15c630b1d 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -657,18 +657,17 @@ class LogXML: ) logfile.write('') - logfile.write( - Junit.testsuite( - self._get_global_properties_node(), - [x.to_xml() for x in self.node_reporters_ordered], - name=self.suite_name, - errors=self.stats["error"], - failures=self.stats["failure"], - skipped=self.stats["skipped"], - tests=numtests, - time="%.3f" % suite_time_delta, - ).unicode(indent=0) + suite_node = Junit.testsuite( + self._get_global_properties_node(), + [x.to_xml() for x in self.node_reporters_ordered], + name=self.suite_name, + errors=self.stats["error"], + failures=self.stats["failure"], + skipped=self.stats["skipped"], + tests=numtests, + time="%.3f" % suite_time_delta, ) + logfile.write(Junit.testsuites([suite_node]).unicode(indent=0)) logfile.close() def pytest_terminal_summary(self, terminalreporter): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index bcf83b352..15643b081 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -41,6 +41,16 @@ class DomNode: def _by_tag(self, tag): return self.__node.getElementsByTagName(tag) + @property + def children(self): + return [type(self)(x) for x in self.__node.childNodes] + + @property + def get_unique_child(self): + children = self.children + assert len(children) == 1 + return children[0] + def find_nth_by_tag(self, tag, n): items = self._by_tag(tag) try: @@ -75,7 +85,7 @@ class DomNode: return self.__node.tagName @property - def next_siebling(self): + def next_sibling(self): return type(self)(self.__node.nextSibling) @@ -384,11 +394,11 @@ class TestPython: fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="ValueError: 42") assert "ValueError" in fnode.toxml() - systemout = fnode.next_siebling + systemout = fnode.next_sibling assert systemout.tag == "system-out" assert "hello-stdout" in systemout.toxml() assert "info msg" not in systemout.toxml() - systemerr = systemout.next_siebling + systemerr = systemout.next_sibling assert systemerr.tag == "system-err" assert "hello-stderr" in systemerr.toxml() assert "info msg" not in systemerr.toxml() @@ -1094,6 +1104,20 @@ def test_random_report_log_xdist(testdir, monkeypatch): assert failed == ["test_x[22]"] +def test_root_testsuites_tag(testdir): + testdir.makepyfile( + """ + def test_x(): + pass + """ + ) + _, dom = runandparse(testdir) + root = dom.get_unique_child + assert root.tag == "testsuites" + suite_node = root.get_unique_child + assert suite_node.tag == "testsuite" + + def test_runs_twice(testdir): f = testdir.makepyfile( """ From 3e669a262a880acdc8a69cad0761e4f1925abe21 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 5 Jul 2019 19:38:16 -0300 Subject: [PATCH 18/70] Introduce Config.invocation_args and Config.invocation_plugins These attributes can be used to access the unchanged arguments passed to pytest.main(). The intention is to use these attributes to initialize workers in the same manner as the master node is initialized in pytest-xdist. --- changelog/5564.feature.rst | 3 +++ src/_pytest/config/__init__.py | 26 ++++++++++++++++++++------ testing/test_config.py | 22 ++++++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 changelog/5564.feature.rst diff --git a/changelog/5564.feature.rst b/changelog/5564.feature.rst new file mode 100644 index 000000000..afc9f3323 --- /dev/null +++ b/changelog/5564.feature.rst @@ -0,0 +1,3 @@ +New ``Config.invocation_args`` and ``Config.invocation_plugins`` attributes. + +These attributes can be used by plugins to access the unchanged arguments passed to ``pytest.main()``. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index c9310bcb5..688a126d4 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -70,6 +70,8 @@ def main(args=None, plugins=None): tw.line(line.rstrip(), red=True) return 4 else: + config.invocation_args = args + config.invocation_plugins = plugins try: return config.hook.pytest_cmdline_main(config=config) finally: @@ -608,20 +610,33 @@ def _iter_rewritable_modules(package_files): class Config: - """ access to configuration values, pluginmanager and plugin hooks. """ + """ + Access to configuration values, pluginmanager and plugin hooks. + + :ivar PytestPluginManager pluginmanager: the plugin manager handles plugin registration and hook invocation. + + :ivar argparse.Namespace option: access to command line option as attributes. + + :ivar invocation_args: list of command-line arguments as passed to pytest.main() + + :ivar invocation_plugins: list of extra plugins passed to pytest.main(), might be None + + :ivar py.path.local invocation_dir: directory where pytest.main() was invoked from + """ def __init__(self, pluginmanager): - #: access to command line option as attributes. - #: (deprecated), use :py:func:`getoption() <_pytest.config.Config.getoption>` instead - self.option = argparse.Namespace() from .argparsing import Parser, FILE_OR_DIR + self.option = argparse.Namespace() + self.invocation_args = None + self.invocation_plugins = None + self.invocation_dir = py.path.local() + _a = FILE_OR_DIR self._parser = Parser( usage="%(prog)s [options] [{}] [{}] [...]".format(_a, _a), processopt=self._processopt, ) - #: a pluginmanager instance self.pluginmanager = pluginmanager self.trace = self.pluginmanager.trace.root.get("config") self.hook = self.pluginmanager.hook @@ -631,7 +646,6 @@ class Config: self._cleanup = [] self.pluginmanager.register(self, "pytestconfig") self._configured = False - self.invocation_dir = py.path.local() self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) def add_cleanup(self, func): diff --git a/testing/test_config.py b/testing/test_config.py index eb7f95271..7ec3c9087 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1198,6 +1198,28 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): assert result.ret == ExitCode.USAGE_ERROR +def test_invocation_arguments(testdir): + """Ensure that Config.invocation_* arguments are correctly defined""" + + class DummyPlugin: + pass + + p = testdir.makepyfile("def test(): pass") + plugin = DummyPlugin() + rec = testdir.inline_run(p, "-v", plugins=[plugin]) + calls = rec.getcalls("pytest_runtest_protocol") + assert len(calls) == 1 + call = calls[0] + config = call.item.config + + assert config.invocation_args == [p, "-v"] + + plugins = config.invocation_plugins + assert len(plugins) == 2 + assert plugins[0] is plugin + assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run() + + @pytest.mark.parametrize( "plugin", [ From 6a9bf2852a8314773c1c04221bb3a0446d35b5d1 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jul 2019 12:22:19 -0300 Subject: [PATCH 19/70] Apply review suggestions: use a simple struct for invocation params --- src/_pytest/config/__init__.py | 50 ++++++++++++++++++++++++++-------- testing/test_config.py | 6 ++-- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 688a126d4..389b0bc81 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -8,7 +8,9 @@ import sys import types import warnings from functools import lru_cache +from pathlib import Path +import attr import py from packaging.version import Version from pluggy import HookimplMarker @@ -70,8 +72,6 @@ def main(args=None, plugins=None): tw.line(line.rstrip(), red=True) return 4 else: - config.invocation_args = args - config.invocation_plugins = plugins try: return config.hook.pytest_cmdline_main(config=config) finally: @@ -149,10 +149,15 @@ builtin_plugins = set(default_plugins) builtin_plugins.add("pytester") -def get_config(args=None): +def get_config(args=None, plugins=None): # subsequent calls to main will create a fresh instance pluginmanager = PytestPluginManager() - config = Config(pluginmanager) + config = Config( + pluginmanager, + invocation_params=Config.InvocationParams( + args=args, plugins=plugins, dir=Path().resolve() + ), + ) if args is not None: # Handle any "-p no:plugin" args. @@ -185,7 +190,7 @@ def _prepareconfig(args=None, plugins=None): msg = "`args` parameter expected to be a list or tuple of strings, got: {!r} (type: {})" raise TypeError(msg.format(args, type(args))) - config = get_config(args) + config = get_config(args, plugins) pluginmanager = config.pluginmanager try: if plugins: @@ -617,20 +622,36 @@ class Config: :ivar argparse.Namespace option: access to command line option as attributes. - :ivar invocation_args: list of command-line arguments as passed to pytest.main() + :ivar InvocationParams invocation_params: - :ivar invocation_plugins: list of extra plugins passed to pytest.main(), might be None + Object containing the parameters regarding the ``pytest.main`` + invocation. - :ivar py.path.local invocation_dir: directory where pytest.main() was invoked from + Contains the followinig read-only attributes: + + * ``args``: list of command-line arguments as passed to ``pytest.main()``. + * ``plugins``: list of extra plugins, might be None + * ``dir``: directory where ``pytest.main()`` was invoked from. """ - def __init__(self, pluginmanager): + @attr.s(frozen=True) + class InvocationParams: + """Holds parameters passed during ``pytest.main()``""" + + args = attr.ib() + plugins = attr.ib() + dir = attr.ib() + + def __init__(self, pluginmanager, *, invocation_params=None): from .argparsing import Parser, FILE_OR_DIR + if invocation_params is None: + invocation_params = self.InvocationParams( + args=(), plugins=None, dir=Path().resolve() + ) + self.option = argparse.Namespace() - self.invocation_args = None - self.invocation_plugins = None - self.invocation_dir = py.path.local() + self.invocation_params = invocation_params _a = FILE_OR_DIR self._parser = Parser( @@ -648,6 +669,11 @@ class Config: self._configured = False self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) + @property + def invocation_dir(self): + """Backward compatibility""" + return py.path.local(str(self.invocation_params.dir)) + def add_cleanup(self, func): """ Add a function to be called when the config object gets out of use (usually coninciding with pytest_unconfigure).""" diff --git a/testing/test_config.py b/testing/test_config.py index 7ec3c9087..8ff14cc20 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,5 +1,6 @@ import sys import textwrap +from pathlib import Path import _pytest._code import pytest @@ -1212,9 +1213,10 @@ def test_invocation_arguments(testdir): call = calls[0] config = call.item.config - assert config.invocation_args == [p, "-v"] + assert config.invocation_params.args == [p, "-v"] + assert config.invocation_params.dir == Path(testdir.tmpdir) - plugins = config.invocation_plugins + plugins = config.invocation_params.plugins assert len(plugins) == 2 assert plugins[0] is plugin assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run() From 4cda7093f6362f1d524dfd7d6f4874a4bcdafd7e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jul 2019 17:27:54 -0300 Subject: [PATCH 20/70] Add note about PYTEST_ADDOPTS --- src/_pytest/config/__init__.py | 10 +++++++++- testing/test_config.py | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 389b0bc81..b5ede4abb 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -636,7 +636,15 @@ class Config: @attr.s(frozen=True) class InvocationParams: - """Holds parameters passed during ``pytest.main()``""" + """Holds parameters passed during ``pytest.main()`` + + .. note:: + + Currently the environment variable PYTEST_ADDOPTS is also handled by + pytest implicitly, not being part of the invocation. + + Plugins accessing ``InvocationParams`` must be aware of that. + """ args = attr.ib() plugins = attr.ib() diff --git a/testing/test_config.py b/testing/test_config.py index 8ff14cc20..a00645a4e 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1199,7 +1199,7 @@ def test_config_does_not_load_blocked_plugin_from_args(testdir): assert result.ret == ExitCode.USAGE_ERROR -def test_invocation_arguments(testdir): +def test_invocation_args(testdir): """Ensure that Config.invocation_* arguments are correctly defined""" class DummyPlugin: @@ -1214,7 +1214,7 @@ def test_invocation_arguments(testdir): config = call.item.config assert config.invocation_params.args == [p, "-v"] - assert config.invocation_params.dir == Path(testdir.tmpdir) + assert config.invocation_params.dir == Path(str(testdir.tmpdir)) plugins = config.invocation_params.plugins assert len(plugins) == 2 From 7a82285b0330c3ccf3681d51927cced9d9d7778c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 6 Jul 2019 17:29:35 -0300 Subject: [PATCH 21/70] Update CHANGELOG --- changelog/5564.feature.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/changelog/5564.feature.rst b/changelog/5564.feature.rst index afc9f3323..e2f365a33 100644 --- a/changelog/5564.feature.rst +++ b/changelog/5564.feature.rst @@ -1,3 +1 @@ -New ``Config.invocation_args`` and ``Config.invocation_plugins`` attributes. - -These attributes can be used by plugins to access the unchanged arguments passed to ``pytest.main()``. +New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. From aaa7e837cce7439fdd74746f5adfc5bfe02edb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6thlisberger?= Date: Mon, 8 Jul 2019 15:41:33 +0100 Subject: [PATCH 22/70] doctest: Add +NUMBER option to ignore irrelevant floating-point differences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When enabled, floating-point numbers only need to match as far as the precision you have written in the expected doctest output. This avoids false positives caused by limited floating-point precision, like this: Expected: 0.233 Got: 0.23300000000000001 This is inspired by Sébastien Boisgérault's [numtest] but the implementation is a bit different: * This implementation edits the literals that are in the "got" string (the actual output from the expression being tested), and then proceeds to compare the strings literally. This is similar to pytest's existing ALLOW_UNICODE and ALLOW_BYTES implementation. * This implementation only compares floats against floats, not ints against floats. That is, the following doctest will fail with pytest whereas it would pass with numtest: >>> math.py # doctest: +NUMBER 3 This behaviour should be less surprising (less false negatives) when you enable NUMBER globally in pytest.ini. Advantages of this implementation compared to numtest: * Doesn't require `import numtest` at the top level of the file. * Works with pytest (if you try to use pytest & numtest together, pytest raises "TypeError: unbound method check_output() must be called with NumTestOutputChecker instance as first argument (got LiteralsOutputChecker instance instead)"). * Works with Python 3. [numtest]: https://github.com/boisgera/numtest --- doc/en/doctest.rst | 29 ++++++-- src/_pytest/doctest.py | 111 +++++++++++++++++++++++++------ testing/test_doctest.py | 144 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 258 insertions(+), 26 deletions(-) diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 2cb70af72..8eca000fd 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -115,15 +115,36 @@ lengthy exception stack traces you can just write: [pytest] doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL -pytest also introduces new options to allow doctests to run in Python 2 and -Python 3 unchanged: +pytest also introduces new options: * ``ALLOW_UNICODE``: when enabled, the ``u`` prefix is stripped from unicode - strings in expected doctest output. + strings in expected doctest output. This allows doctests to run in Python 2 + and Python 3 unchanged. -* ``ALLOW_BYTES``: when enabled, the ``b`` prefix is stripped from byte strings +* ``ALLOW_BYTES``: similarly, the ``b`` prefix is stripped from byte strings in expected doctest output. +* ``NUMBER``: when enabled, floating-point numbers only need to match as far as + the precision you have written in the expected doctest output. For example, + the following output would only need to match to 2 decimal places:: + + >>> math.pi + 3.14 + + If you wrote ``3.1416`` then the actual output would need to match to 4 + decimal places; and so on. + + This avoids false positives caused by limited floating-point precision, like + this:: + + Expected: + 0.233 + Got: + 0.23300000000000001 + + ``NUMBER`` also supports lists of floating-point numbers -- in fact, it + supports floating-point numbers appearing anywhere in the output. + Alternatively, options can be enabled by an inline comment in the doc test itself: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index ca6e4675f..cf886f906 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -13,6 +13,7 @@ from _pytest._code.code import TerminalRepr from _pytest.compat import safe_getattr from _pytest.fixtures import FixtureRequest from _pytest.outcomes import Skipped +from _pytest.python_api import approx from _pytest.warning_types import PytestWarning DOCTEST_REPORT_CHOICE_NONE = "none" @@ -286,6 +287,7 @@ def _get_flag_lookup(): COMPARISON_FLAGS=doctest.COMPARISON_FLAGS, ALLOW_UNICODE=_get_allow_unicode_flag(), ALLOW_BYTES=_get_allow_bytes_flag(), + NUMBER=_get_number_flag(), ) @@ -453,10 +455,15 @@ def _setup_fixtures(doctest_item): def _get_checker(): """ - Returns a doctest.OutputChecker subclass that takes in account the - ALLOW_UNICODE option to ignore u'' prefixes in strings and ALLOW_BYTES - to strip b'' prefixes. - Useful when the same doctest should run in Python 2 and Python 3. + Returns a doctest.OutputChecker subclass that supports some + additional options: + + * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b'' + prefixes (respectively) in string literals. Useful when the same + doctest should run in Python 2 and Python 3. + + * NUMBER to ignore floating-point differences smaller than the + precision of the literal number in the doctest. An inner class is used to avoid importing "doctest" at the module level. @@ -469,38 +476,89 @@ def _get_checker(): class LiteralsOutputChecker(doctest.OutputChecker): """ - Copied from doctest_nose_plugin.py from the nltk project: - https://github.com/nltk/nltk - - Further extended to also support byte literals. + Based on doctest_nose_plugin.py from the nltk project + (https://github.com/nltk/nltk) and on the "numtest" doctest extension + by Sebastien Boisgerault (https://github.com/boisgera/numtest). """ _unicode_literal_re = re.compile(r"(\W|^)[uU]([rR]?[\'\"])", re.UNICODE) _bytes_literal_re = re.compile(r"(\W|^)[bB]([rR]?[\'\"])", re.UNICODE) + _number_re = re.compile( + r""" + (?P + (?P + (?P [+-]?\d*)\.(?P\d+) + | + (?P [+-]?\d+)\. + ) + (?: + [Ee] + (?P [+-]?\d+) + )? + | + (?P [+-]?\d+) + (?: + [Ee] + (?P [+-]?\d+) + ) + ) + """, + re.VERBOSE, + ) def check_output(self, want, got, optionflags): - res = doctest.OutputChecker.check_output(self, want, got, optionflags) - if res: + if doctest.OutputChecker.check_output(self, want, got, optionflags): return True allow_unicode = optionflags & _get_allow_unicode_flag() allow_bytes = optionflags & _get_allow_bytes_flag() - if not allow_unicode and not allow_bytes: + allow_number = optionflags & _get_number_flag() + + if not allow_unicode and not allow_bytes and not allow_number: return False - else: # pragma: no cover + def remove_prefixes(regex, txt): + return re.sub(regex, r"\1\2", txt) - def remove_prefixes(regex, txt): - return re.sub(regex, r"\1\2", txt) + if allow_unicode: + want = remove_prefixes(self._unicode_literal_re, want) + got = remove_prefixes(self._unicode_literal_re, got) - if allow_unicode: - want = remove_prefixes(self._unicode_literal_re, want) - got = remove_prefixes(self._unicode_literal_re, got) - if allow_bytes: - want = remove_prefixes(self._bytes_literal_re, want) - got = remove_prefixes(self._bytes_literal_re, got) - res = doctest.OutputChecker.check_output(self, want, got, optionflags) - return res + if allow_bytes: + want = remove_prefixes(self._bytes_literal_re, want) + got = remove_prefixes(self._bytes_literal_re, got) + + if allow_number: + got = self._remove_unwanted_precision(want, got) + + return doctest.OutputChecker.check_output(self, want, got, optionflags) + + def _remove_unwanted_precision(self, want, got): + wants = list(self._number_re.finditer(want)) + gots = list(self._number_re.finditer(got)) + if len(wants) != len(gots): + return got + offset = 0 + for w, g in zip(wants, gots): + fraction = w.group("fraction") + exponent = w.group("exponent1") + if exponent is None: + exponent = w.group("exponent2") + if fraction is None: + precision = 0 + else: + precision = len(fraction) + if exponent is not None: + precision -= int(exponent) + if float(w.group()) == approx(float(g.group()), abs=10 ** -precision): + # They're close enough. Replace the text we actually + # got with the text we want, so that it will match when we + # check the string literally. + got = ( + got[: g.start() + offset] + w.group() + got[g.end() + offset :] + ) + offset += w.end() - w.start() - (g.end() - g.start()) + return got _get_checker.LiteralsOutputChecker = LiteralsOutputChecker return _get_checker.LiteralsOutputChecker() @@ -524,6 +582,15 @@ def _get_allow_bytes_flag(): return doctest.register_optionflag("ALLOW_BYTES") +def _get_number_flag(): + """ + Registers and returns the NUMBER flag. + """ + import doctest + + return doctest.register_optionflag("NUMBER") + + def _get_report_choice(key): """ This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 236066673..678c61af1 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -3,6 +3,7 @@ import textwrap import pytest from _pytest.compat import MODULE_NOT_FOUND_ERROR +from _pytest.doctest import _get_checker from _pytest.doctest import _is_mocked from _pytest.doctest import _patch_unwrap_mock_aware from _pytest.doctest import DoctestItem @@ -838,6 +839,149 @@ class TestLiterals: reprec = testdir.inline_run() reprec.assertoutcome(failed=1) + def test_number_re(self): + for s in [ + "1.", + "+1.", + "-1.", + ".1", + "+.1", + "-.1", + "0.1", + "+0.1", + "-0.1", + "1e5", + "+1e5", + "1e+5", + "+1e+5", + "1e-5", + "+1e-5", + "-1e-5", + "1.2e3", + "-1.2e-3", + ]: + print(s) + m = _get_checker()._number_re.match(s) + assert m is not None + assert float(m.group()) == pytest.approx(float(s)) + for s in ["1", "abc"]: + print(s) + assert _get_checker()._number_re.match(s) is None + + @pytest.mark.parametrize("config_mode", ["ini", "comment"]) + def test_number_precision(self, testdir, config_mode): + """Test the NUMBER option.""" + if config_mode == "ini": + testdir.makeini( + """ + [pytest] + doctest_optionflags = NUMBER + """ + ) + comment = "" + else: + comment = "#doctest: +NUMBER" + + testdir.maketxtfile( + test_doc=""" + + Scalars: + + >>> import math + >>> math.pi {comment} + 3.141592653589793 + >>> math.pi {comment} + 3.1416 + >>> math.pi {comment} + 3.14 + >>> -math.pi {comment} + -3.14 + >>> math.pi {comment} + 3. + >>> 3. {comment} + 3.0 + >>> 3. {comment} + 3. + >>> 3. {comment} + 3.01 + >>> 3. {comment} + 2.99 + >>> .299 {comment} + .3 + >>> .301 {comment} + .3 + >>> 951. {comment} + 1e3 + >>> 1049. {comment} + 1e3 + >>> -1049. {comment} + -1e3 + >>> 1e3 {comment} + 1e3 + >>> 1e3 {comment} + 1000. + + Lists: + + >>> [3.1415, 0.097, 13.1, 7, 8.22222e5, 0.598e-2] {comment} + [3.14, 0.1, 13., 7, 8.22e5, 6.0e-3] + >>> [[0.333, 0.667], [0.999, 1.333]] {comment} + [[0.33, 0.667], [0.999, 1.333]] + >>> [[[0.101]]] {comment} + [[[0.1]]] + + Doesn't barf on non-numbers: + + >>> 'abc' {comment} + 'abc' + >>> None {comment} + """.format( + comment=comment + ) + ) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + + @pytest.mark.parametrize( + "expression,output", + [ + # ints shouldn't match floats: + ("3.0", "3"), + ("3e0", "3"), + ("1e3", "1000"), + ("3", "3.0"), + # Rounding: + ("3.1", "3.0"), + ("3.1", "3.2"), + ("3.1", "4.0"), + ("8.22e5", "810000.0"), + # Only the actual output is rounded up, not the expected output: + ("3.0", "2.99"), + ("1e3", "999"), + ], + ) + def test_number_non_matches(self, testdir, expression, output): + testdir.maketxtfile( + test_doc=""" + >>> {expression} #doctest: +NUMBER + {output} + """ + ) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=0, failed=1) + + def test_number_and_allow_unicode(self, testdir): + testdir.maketxtfile( + test_doc=""" + >>> from collections import namedtuple + >>> T = namedtuple('T', 'a b c') + >>> T(a=0.2330000001, b=u'str', c=b'bytes') # doctest: +ALLOW_UNICODE, +ALLOW_BYTES, +NUMBER + T(a=0.233, b=u'str', c='bytes') + """ + ) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + class TestDoctestSkips: """ From 2a23fdab9f066e3ebf7f3bcffe6e40c136772a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6thlisberger?= Date: Mon, 8 Jul 2019 17:00:32 +0100 Subject: [PATCH 23/70] docs: Tidy up doctest options section * Move the parts about "how to configure it" (pytest.ini vs. inline comment) together. * Move `--doctest-continue-on-failure` into its own sub-heading, as it isn't related to the doctest optionflags. --- doc/en/doctest.rst | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 8eca000fd..9839c7ad4 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -103,7 +103,7 @@ that will be used for those doctest files using the Using 'doctest' options ----------------------- -The standard ``doctest`` module provides some `options `__ +Python's standard ``doctest`` module provides some `options `__ to configure the strictness of doctest tests. In pytest, you can enable those flags using the configuration file. @@ -115,6 +115,15 @@ lengthy exception stack traces you can just write: [pytest] doctest_optionflags= NORMALIZE_WHITESPACE IGNORE_EXCEPTION_DETAIL +Alternatively, options can be enabled by an inline comment in the doc test +itself: + +.. code-block:: rst + + >>> something_that_raises() # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ValueError: ... + pytest also introduces new options: * ``ALLOW_UNICODE``: when enabled, the ``u`` prefix is stripped from unicode @@ -145,14 +154,9 @@ pytest also introduces new options: ``NUMBER`` also supports lists of floating-point numbers -- in fact, it supports floating-point numbers appearing anywhere in the output. -Alternatively, options can be enabled by an inline comment in the doc test -itself: -.. code-block:: rst - - # content of example.rst - >>> get_unicode_greeting() # doctest: +ALLOW_UNICODE - 'Hello' +Continue on failure +------------------- By default, pytest would report only the first failure for a given doctest. If you want to continue the test even when you have failures, do: From d5cc0f2a622ad7ce182ed1f4dad0ac1641a68d2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6thlisberger?= Date: Mon, 8 Jul 2019 17:16:35 +0100 Subject: [PATCH 24/70] changelog for new NUMBERS doctest option --- AUTHORS | 1 + changelog/5576.feature.rst | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 changelog/5576.feature.rst diff --git a/AUTHORS b/AUTHORS index 087fce8d0..aacf16a36 100644 --- a/AUTHORS +++ b/AUTHORS @@ -70,6 +70,7 @@ Danielle Jenkins Dave Hunt David Díaz-Barquero David Mohr +David Paul Röthlisberger David Szotten David Vierra Daw-Ran Liou diff --git a/changelog/5576.feature.rst b/changelog/5576.feature.rst new file mode 100644 index 000000000..267a28292 --- /dev/null +++ b/changelog/5576.feature.rst @@ -0,0 +1,4 @@ +New `NUMBER `__ +option for doctests to ignore irrelevant differences in floating-point numbers. +Inspired by Sébastien Boisgérault's `numtest `__ +extension for doctest. From c1167ac5522b0e4d16c22f135351f530d3f3e214 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 8 Jul 2019 10:04:19 +0300 Subject: [PATCH 25/70] Add rudimentary mypy type checking Add a very lax mypy configuration, add it to tox -e linting, and fix/ignore the few errors that come up. The idea is to get it running before diving in too much. This enables: - Progressively adding type annotations and enabling more strict options, which will improve the codebase (IMO). - Annotating the public API in-line, and eventually exposing it to library users who use type checkers (with a py.typed file). Though, none of this is done yet. Refs https://github.com/pytest-dev/pytest/issues/3342. --- .gitignore | 1 + .pre-commit-config.yaml | 12 +++++++++ bench/bench.py | 2 +- setup.cfg | 8 ++++++ src/_pytest/_argcomplete.py | 3 ++- src/_pytest/_code/code.py | 13 +++++----- src/_pytest/_code/source.py | 3 ++- src/_pytest/assertion/rewrite.py | 25 +++++++++++-------- src/_pytest/capture.py | 12 ++++++--- src/_pytest/debugging.py | 2 +- src/_pytest/fixtures.py | 12 ++++++--- src/_pytest/mark/__init__.py | 3 ++- src/_pytest/mark/structures.py | 3 ++- src/_pytest/nodes.py | 3 ++- src/_pytest/outcomes.py | 12 ++++++--- src/_pytest/python_api.py | 15 +++++++---- src/_pytest/reports.py | 3 ++- src/_pytest/tmpdir.py | 5 +++- testing/code/test_excinfo.py | 3 ++- .../package_infinite_recursion/__init__.pyi | 0 .../config/collect_pytest_prefix/__init__.pyi | 0 .../conftest_usageerror/__init__.pyi | 0 .../fixtures/custom_item/__init__.pyi | 0 .../__init__.pyi | 0 .../__init__.pyi | 0 .../__init__.pyi | 0 .../marks_considered_keywords/__init__.pyi | 0 testing/python/raises.py | 3 ++- testing/test_compat.py | 3 ++- testing/test_pdb.py | 3 ++- 30 files changed, 104 insertions(+), 45 deletions(-) create mode 100644 testing/example_scripts/collect/package_infinite_recursion/__init__.pyi create mode 100644 testing/example_scripts/config/collect_pytest_prefix/__init__.pyi create mode 100644 testing/example_scripts/conftest_usageerror/__init__.pyi create mode 100644 testing/example_scripts/fixtures/custom_item/__init__.pyi create mode 100644 testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/__init__.pyi create mode 100644 testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/__init__.pyi create mode 100644 testing/example_scripts/issue88_initial_file_multinodes/__init__.pyi create mode 100644 testing/example_scripts/marks/marks_considered_keywords/__init__.pyi diff --git a/.gitignore b/.gitignore index a008b4363..27bd93c7b 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ env/ .tox .cache .pytest_cache +.mypy_cache .coverage .coverage.* coverage.xml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 12fa0d343..fce7978c4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,6 +28,7 @@ repos: hooks: - id: flake8 language_version: python3 + additional_dependencies: [flake8-typing-imports] - repo: https://github.com/asottile/reorder_python_imports rev: v1.4.0 hooks: @@ -42,6 +43,17 @@ repos: rev: v1.4.0 hooks: - id: rst-backticks +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.711 + hooks: + - id: mypy + name: mypy (src) + files: ^src/ + args: [] + - id: mypy + name: mypy (testing) + files: ^testing/ + args: [] - repo: local hooks: - id: rst diff --git a/bench/bench.py b/bench/bench.py index 31cc7ac13..c40fc8636 100644 --- a/bench/bench.py +++ b/bench/bench.py @@ -6,7 +6,7 @@ if __name__ == "__main__": import pstats script = sys.argv[1:] if len(sys.argv) > 1 else ["empty.py"] - stats = cProfile.run("pytest.cmdline.main(%r)" % script, "prof") + cProfile.run("pytest.cmdline.main(%r)" % script, "prof") p = pstats.Stats("prof") p.strip_dirs() p.sort_stats("cumulative") diff --git a/setup.cfg b/setup.cfg index 2d6e5bee1..60e866562 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,3 +61,11 @@ ignore = [devpi:upload] formats = sdist.tgz,bdist_wheel + +[mypy] +ignore_missing_imports = True +no_implicit_optional = True +strict_equality = True +warn_redundant_casts = True +warn_return_any = True +warn_unused_configs = True diff --git a/src/_pytest/_argcomplete.py b/src/_pytest/_argcomplete.py index 1ebf7432c..688c9077d 100644 --- a/src/_pytest/_argcomplete.py +++ b/src/_pytest/_argcomplete.py @@ -56,6 +56,7 @@ If things do not work right away: import os import sys from glob import glob +from typing import Optional class FastFilesCompleter: @@ -91,7 +92,7 @@ if os.environ.get("_ARGCOMPLETE"): import argcomplete.completers except ImportError: sys.exit(-1) - filescompleter = FastFilesCompleter() + filescompleter = FastFilesCompleter() # type: Optional[FastFilesCompleter] def try_argcomplete(parser): argcomplete.autocomplete(parser, always_complete_options=False) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index b0b4d6531..6f520ebcf 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -33,7 +33,8 @@ class Code: def __eq__(self, other): return self.raw == other.raw - __hash__ = None + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore def __ne__(self, other): return not self == other @@ -188,11 +189,11 @@ class TracebackEntry: """ path to the source code """ return self.frame.code.path - def getlocals(self): + @property + def locals(self): + """ locals of underlaying frame """ return self.frame.f_locals - locals = property(getlocals, None, None, "locals of underlaying frame") - def getfirstlinesource(self): return self.frame.code.firstlineno @@ -255,11 +256,11 @@ class TracebackEntry: line = "???" return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line) + @property def name(self): + """ co_name of underlaying code """ return self.frame.code.raw.co_name - name = property(name, None, None, "co_name of underlaying code") - class Traceback(list): """ Traceback objects encapsulate and offer higher level diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 70d5f8fcd..ea2fc5e3f 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -44,7 +44,8 @@ class Source: return str(self) == other return False - __hash__ = None + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore def __getitem__(self, key): if isinstance(key, int): diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 8b2c1e146..7bd46eeb6 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -12,6 +12,10 @@ import struct import sys import tokenize import types +from typing import Dict +from typing import List +from typing import Optional +from typing import Set import atomicwrites @@ -459,17 +463,18 @@ def set_location(node, lineno, col_offset): return node -def _get_assertion_exprs(src: bytes): # -> Dict[int, str] +def _get_assertion_exprs(src: bytes) -> Dict[int, str]: """Returns a mapping from {lineno: "assertion test expression"}""" - ret = {} + ret = {} # type: Dict[int, str] depth = 0 - lines = [] - assert_lineno = None - seen_lines = set() + lines = [] # type: List[str] + assert_lineno = None # type: Optional[int] + seen_lines = set() # type: Set[int] def _write_and_reset() -> None: nonlocal depth, lines, assert_lineno, seen_lines + assert assert_lineno is not None ret[assert_lineno] = "".join(lines).rstrip().rstrip("\\") depth = 0 lines = [] @@ -477,21 +482,21 @@ def _get_assertion_exprs(src: bytes): # -> Dict[int, str] seen_lines = set() tokens = tokenize.tokenize(io.BytesIO(src).readline) - for tp, src, (lineno, offset), _, line in tokens: - if tp == tokenize.NAME and src == "assert": + for tp, source, (lineno, offset), _, line in tokens: + if tp == tokenize.NAME and source == "assert": assert_lineno = lineno elif assert_lineno is not None: # keep track of depth for the assert-message `,` lookup - if tp == tokenize.OP and src in "([{": + if tp == tokenize.OP and source in "([{": depth += 1 - elif tp == tokenize.OP and src in ")]}": + elif tp == tokenize.OP and source in ")]}": depth -= 1 if not lines: lines.append(line[offset:]) seen_lines.add(lineno) # a non-nested comma separates the expression from the message - elif depth == 0 and tp == tokenize.OP and src == ",": + elif depth == 0 and tp == tokenize.OP and source == ",": # one line assert with message if lineno in seen_lines and len(lines) == 1: offset_in_trimmed = offset + len(lines[-1]) - len(line) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index 302979ef4..f89aaefba 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -547,6 +547,8 @@ class FDCaptureBinary: self.start = lambda: None self.done = lambda: None else: + self.start = self._start + self.done = self._done if targetfd == 0: assert not tmpfile, "cannot set tmpfile with stdin" tmpfile = open(os.devnull, "r") @@ -568,7 +570,7 @@ class FDCaptureBinary: self.targetfd, getattr(self, "targetfd_save", None), self._state ) - def start(self): + def _start(self): """ Start capturing on targetfd using memorized tmpfile. """ try: os.fstat(self.targetfd_save) @@ -585,7 +587,7 @@ class FDCaptureBinary: self.tmpfile.truncate() return res - def done(self): + def _done(self): """ stop capturing, restore streams, return original capture file, seeked to position zero. """ targetfd_save = self.__dict__.pop("targetfd_save") @@ -618,7 +620,8 @@ class FDCapture(FDCaptureBinary): snap() produces text """ - EMPTY_BUFFER = str() + # Ignore type because it doesn't match the type in the superclass (bytes). + EMPTY_BUFFER = str() # type: ignore def snap(self): res = super().snap() @@ -679,7 +682,8 @@ class SysCapture: class SysCaptureBinary(SysCapture): - EMPTY_BUFFER = b"" + # Ignore type because it doesn't match the type in the superclass (str). + EMPTY_BUFFER = b"" # type: ignore def snap(self): res = self.tmpfile.buffer.getvalue() diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 891630b43..2e3d49c37 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -74,7 +74,7 @@ class pytestPDB: _pluginmanager = None _config = None - _saved = [] + _saved = [] # type: list _recursive_debug = 0 _wrapped_pdb_cls = None diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 3262b65bb..965a2e6e9 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -6,6 +6,8 @@ import warnings from collections import defaultdict from collections import deque from collections import OrderedDict +from typing import Dict +from typing import Tuple import attr import py @@ -31,6 +33,9 @@ from _pytest.deprecated import FIXTURE_NAMED_REQUEST from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME +if False: # TYPE_CHECKING + from typing import Type + @attr.s(frozen=True) class PseudoFixtureDef: @@ -54,10 +59,10 @@ def pytest_sessionstart(session): session._fixturemanager = FixtureManager(session) -scopename2class = {} +scopename2class = {} # type: Dict[str, Type[nodes.Node]] -scope2props = dict(session=()) +scope2props = dict(session=()) # type: Dict[str, Tuple[str, ...]] scope2props["package"] = ("fspath",) scope2props["module"] = ("fspath", "module") scope2props["class"] = scope2props["module"] + ("cls",) @@ -960,7 +965,8 @@ class FixtureFunctionMarker: scope = attr.ib() params = attr.ib(converter=attr.converters.optional(tuple)) autouse = attr.ib(default=False) - ids = attr.ib(default=None, converter=_ensure_immutable_ids) + # Ignore type because of https://github.com/python/mypy/issues/6172. + ids = attr.ib(default=None, converter=_ensure_immutable_ids) # type: ignore name = attr.ib(default=None) def __call__(self, function): diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index 30c6e0048..e76bb7857 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -91,7 +91,8 @@ def pytest_cmdline_main(config): return 0 -pytest_cmdline_main.tryfirst = True +# Ignore type because of https://github.com/python/mypy/issues/2087. +pytest_cmdline_main.tryfirst = True # type: ignore def deselect_by_keyword(items, config): diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 1af7a9b42..0887d6b9c 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -3,6 +3,7 @@ import warnings from collections import namedtuple from collections.abc import MutableMapping from operator import attrgetter +from typing import Set import attr @@ -298,7 +299,7 @@ class MarkGenerator: on the ``test_function`` object. """ _config = None - _markers = set() + _markers = set() # type: Set[str] def __getattr__(self, name): if name[0] == "_": diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 491cf9d2c..7e1c40bcb 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -280,7 +280,8 @@ class Node: truncate_locals=truncate_locals, ) - repr_failure = _repr_failure_py + def repr_failure(self, excinfo, style=None): + return self._repr_failure_py(excinfo, style) def get_fslocation_from_item(item): diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index fb4d471b5..c7e26f5cc 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -70,7 +70,8 @@ def exit(msg, returncode=None): raise Exit(msg, returncode) -exit.Exception = Exit +# Ignore type because of https://github.com/python/mypy/issues/2087. +exit.Exception = Exit # type: ignore def skip(msg="", *, allow_module_level=False): @@ -96,7 +97,8 @@ def skip(msg="", *, allow_module_level=False): raise Skipped(msg=msg, allow_module_level=allow_module_level) -skip.Exception = Skipped +# Ignore type because of https://github.com/python/mypy/issues/2087. +skip.Exception = Skipped # type: ignore def fail(msg="", pytrace=True): @@ -111,7 +113,8 @@ def fail(msg="", pytrace=True): raise Failed(msg=msg, pytrace=pytrace) -fail.Exception = Failed +# Ignore type because of https://github.com/python/mypy/issues/2087. +fail.Exception = Failed # type: ignore class XFailed(Failed): @@ -132,7 +135,8 @@ def xfail(reason=""): raise XFailed(reason) -xfail.Exception = XFailed +# Ignore type because of https://github.com/python/mypy/issues/2087. +xfail.Exception = XFailed # type: ignore def importorskip(modname, minversion=None, reason=None): diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 374fa598f..cbd833946 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -9,6 +9,7 @@ from collections.abc import Sized from decimal import Decimal from itertools import filterfalse from numbers import Number +from typing import Union from more_itertools.more import always_iterable @@ -58,7 +59,8 @@ class ApproxBase: a == self._approx_scalar(x) for a, x in self._yield_comparisons(actual) ) - __hash__ = None + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore def __ne__(self, actual): return not (actual == self) @@ -202,8 +204,10 @@ class ApproxScalar(ApproxBase): Perform approximate comparisons where the expected value is a single number. """ - DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 - DEFAULT_RELATIVE_TOLERANCE = 1e-6 + # Using Real should be better than this Union, but not possible yet: + # https://github.com/python/typeshed/pull/3108 + DEFAULT_ABSOLUTE_TOLERANCE = 1e-12 # type: Union[float, Decimal] + DEFAULT_RELATIVE_TOLERANCE = 1e-6 # type: Union[float, Decimal] def __repr__(self): """ @@ -261,7 +265,8 @@ class ApproxScalar(ApproxBase): # Return true if the two numbers are within the tolerance. return abs(self.expected - actual) <= self.tolerance - __hash__ = None + # Ignore type because of https://github.com/python/mypy/issues/4266. + __hash__ = None # type: ignore @property def tolerance(self): @@ -691,7 +696,7 @@ def raises(expected_exception, *args, **kwargs): fail(message) -raises.Exception = fail.Exception +raises.Exception = fail.Exception # type: ignore class RaisesContext: diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index d2f1f33e2..4682d5b6e 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,4 +1,5 @@ from pprint import pprint +from typing import Optional import py @@ -28,7 +29,7 @@ def getslaveinfoline(node): class BaseReport: - when = None + when = None # type: Optional[str] location = None def __init__(self, **kw): diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index f2c4d905c..48680c07e 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -26,7 +26,10 @@ class TempPathFactory: # using os.path.abspath() to get absolute path instead of resolve() as it # does not work the same in all platforms (see #4427) # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012) - converter=attr.converters.optional(lambda p: Path(os.path.abspath(str(p)))) + # Ignore type because of https://github.com/python/mypy/issues/6172. + converter=attr.converters.optional( + lambda p: Path(os.path.abspath(str(p))) # type: ignore + ) ) _trace = attr.ib() _basetemp = attr.ib(default=None) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index f7787c282..d7771833a 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -589,7 +589,8 @@ raise ValueError() def test_repr_local_with_exception_in_class_property(self): class ExceptionWithBrokenClass(Exception): - @property + # Type ignored because it's bypassed intentionally. + @property # type: ignore def __class__(self): raise TypeError("boom!") diff --git a/testing/example_scripts/collect/package_infinite_recursion/__init__.pyi b/testing/example_scripts/collect/package_infinite_recursion/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/testing/example_scripts/config/collect_pytest_prefix/__init__.pyi b/testing/example_scripts/config/collect_pytest_prefix/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/testing/example_scripts/conftest_usageerror/__init__.pyi b/testing/example_scripts/conftest_usageerror/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/testing/example_scripts/fixtures/custom_item/__init__.pyi b/testing/example_scripts/fixtures/custom_item/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/__init__.pyi b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_conftest/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/__init__.pyi b/testing/example_scripts/fixtures/fill_fixtures/test_extend_fixture_conftest_module/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/testing/example_scripts/issue88_initial_file_multinodes/__init__.pyi b/testing/example_scripts/issue88_initial_file_multinodes/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/testing/example_scripts/marks/marks_considered_keywords/__init__.pyi b/testing/example_scripts/marks/marks_considered_keywords/__init__.pyi new file mode 100644 index 000000000..e69de29bb diff --git a/testing/python/raises.py b/testing/python/raises.py index c9ede412a..6d9604b1d 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -258,7 +258,8 @@ class TestRaises: """Test current behavior with regard to exceptions via __class__ (#4284).""" class CrappyClass(Exception): - @property + # Type ignored because it's bypassed intentionally. + @property # type: ignore def __class__(self): assert False, "via __class__" diff --git a/testing/test_compat.py b/testing/test_compat.py index 028d48bed..9e7d05c5d 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -141,7 +141,8 @@ def test_safe_isclass(): assert safe_isclass(type) is True class CrappyClass(Exception): - @property + # Type ignored because it's bypassed intentionally. + @property # type: ignore def __class__(self): assert False, "Should be ignored" diff --git a/testing/test_pdb.py b/testing/test_pdb.py index f3f7ca702..8d327cbb3 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -6,7 +6,8 @@ import pytest from _pytest.debugging import _validate_usepdb_cls try: - breakpoint + # Type ignored for Python <= 3.6. + breakpoint # type: ignore except NameError: SUPPORTS_BREAKPOINT_BUILTIN = False else: From f7747f5dd652694ce1357ea633787cb772627bef Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 10 Jul 2019 13:27:28 +0300 Subject: [PATCH 26/70] Remove references to old-style classes in a couple error messages These don't exist in Python 3. --- src/_pytest/python_api.py | 5 +---- src/_pytest/recwarn.py | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 82305bb1c..7c63a3588 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -647,10 +647,7 @@ def raises(expected_exception, *args, match=None, **kwargs): for exc in filterfalse( inspect.isclass, always_iterable(expected_exception, BASE_TYPE) ): - msg = ( - "exceptions must be old-style classes or" - " derived from BaseException, not %s" - ) + msg = "exceptions must be derived from BaseException, not %s" raise TypeError(msg % type(exc)) message = "DID NOT RAISE {}".format(expected_exception) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 3ab83d1e3..b124c69d5 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -151,7 +151,7 @@ class WarningsChecker(WarningsRecorder): def __init__(self, expected_warning=None, match_expr=None): super().__init__() - msg = "exceptions must be old-style classes or derived from Warning, not %s" + msg = "exceptions must be derived from Warning, not %s" if isinstance(expected_warning, tuple): for exc in expected_warning: if not inspect.isclass(exc): From 4c590e002fc220ae27e9451a39990fdc452a93a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6thlisberger?= Date: Thu, 11 Jul 2019 09:57:44 +0100 Subject: [PATCH 27/70] Fix test_doctest.test_number_non_matches These doctests were expected to fail, but they were failing because of a silly bug (I forgot to replace "{expression}" with the actual expression to be tested), not because of the thing they were meant to be testing. Then I had to fix one of the testcases because it was actually matching: >>> 3.0 #doctest: +NUMBER 2.99 The doctest is saying that the actual output should match to 2 decimal places, i.e. within 0.01 -- which it is, so it passes. I changed the expected output to 2.98 and now it doesn't match (as we expect). --- testing/test_doctest.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 678c61af1..40b6d7ebb 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -956,7 +956,7 @@ class TestLiterals: ("3.1", "4.0"), ("8.22e5", "810000.0"), # Only the actual output is rounded up, not the expected output: - ("3.0", "2.99"), + ("3.0", "2.98"), ("1e3", "999"), ], ) @@ -965,7 +965,9 @@ class TestLiterals: test_doc=""" >>> {expression} #doctest: +NUMBER {output} - """ + """.format( + expression=expression, output=output + ) ) reprec = testdir.inline_run() reprec.assertoutcome(passed=0, failed=1) From a740ef20367bad2d401ab6ac0092d26bd2b62379 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20R=C3=B6thlisberger?= Date: Thu, 11 Jul 2019 10:04:43 +0100 Subject: [PATCH 28/70] docs: Document doctest +NUMBER limitation with strings Also added an "xfail" testcase for the same. --- doc/en/doctest.rst | 4 +++- testing/test_doctest.py | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 9839c7ad4..b17327f62 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -152,7 +152,9 @@ pytest also introduces new options: 0.23300000000000001 ``NUMBER`` also supports lists of floating-point numbers -- in fact, it - supports floating-point numbers appearing anywhere in the output. + matches floating-point numbers appearing anywhere in the output, even inside + a string! This means that it may not be appropriate to enable globally in + ``doctest_optionflags`` in your configuration file. Continue on failure diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 40b6d7ebb..4aac5432d 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -958,6 +958,9 @@ class TestLiterals: # Only the actual output is rounded up, not the expected output: ("3.0", "2.98"), ("1e3", "999"), + # The current implementation doesn't understand that numbers inside + # strings shouldn't be treated as numbers: + pytest.param("'3.1416'", "'3.14'", marks=pytest.mark.xfail), ], ) def test_number_non_matches(self, testdir, expression, output): From 24a66db8d39f593306d02263f9c5166c5f703f41 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 9 Jul 2019 19:49:35 -0300 Subject: [PATCH 29/70] Remove support code for unittest 2 Also moved a pytest_runtest_makereport hook implemented in nose.py, but nowadays makes more sense to be implemented in unittest.py --- changelog/5565.removal.rst | 13 +++++++++++++ src/_pytest/nose.py | 25 ------------------------- src/_pytest/runner.py | 9 +++++---- src/_pytest/unittest.py | 9 +++++++++ testing/test_unittest.py | 4 +--- tox.ini | 1 - 6 files changed, 28 insertions(+), 33 deletions(-) create mode 100644 changelog/5565.removal.rst diff --git a/changelog/5565.removal.rst b/changelog/5565.removal.rst new file mode 100644 index 000000000..33de2b4e7 --- /dev/null +++ b/changelog/5565.removal.rst @@ -0,0 +1,13 @@ +Removed unused support code for `unittest2 `__. + +The ``unittest2`` backport module is no longer +necessary since Python 3.3+, and the small amount of code in pytest to support it also doesn't seem +to be used: after removed, all tests still pass unchanged. + +Although our policy is to introduce a deprecation period before removing any features or support +for third party libraries, because this code is apparently not used +at all (even if ``unittest2`` is used by a test suite executed by pytest), it was decided to +remove it in this release. + +If you experience a regression because of this, please +`file an issue `__. diff --git a/src/_pytest/nose.py b/src/_pytest/nose.py index bb5ca198c..d6f3c2b22 100644 --- a/src/_pytest/nose.py +++ b/src/_pytest/nose.py @@ -1,31 +1,9 @@ """ run test suites written for nose. """ -import sys - -import pytest from _pytest import python -from _pytest import runner from _pytest import unittest from _pytest.config import hookimpl -def get_skip_exceptions(): - skip_classes = set() - for module_name in ("unittest", "unittest2", "nose"): - mod = sys.modules.get(module_name) - if hasattr(mod, "SkipTest"): - skip_classes.add(mod.SkipTest) - return tuple(skip_classes) - - -def pytest_runtest_makereport(item, call): - if call.excinfo and call.excinfo.errisinstance(get_skip_exceptions()): - # let's substitute the excinfo with a pytest.skip one - call2 = runner.CallInfo.from_call( - lambda: pytest.skip(str(call.excinfo.value)), call.when - ) - call.excinfo = call2.excinfo - - @hookimpl(trylast=True) def pytest_runtest_setup(item): if is_potential_nosetest(item): @@ -40,9 +18,6 @@ def teardown_nose(item): if is_potential_nosetest(item): if not call_optional(item.obj, "teardown"): call_optional(item.parent.obj, "teardown") - # if hasattr(item.parent, '_nosegensetup'): - # #call_optional(item._nosegensetup, 'teardown') - # del item.parent._nosegensetup def is_potential_nosetest(item): diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 9c91a49a5..edaee9725 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -249,10 +249,11 @@ def pytest_make_collect_report(collector): if not call.excinfo: outcome = "passed" else: - from _pytest import nose - - skip_exceptions = (Skipped,) + nose.get_skip_exceptions() - if call.excinfo.errisinstance(skip_exceptions): + skip_exceptions = [Skipped] + unittest = sys.modules.get("unittest") + if unittest is not None: + skip_exceptions.append(unittest.SkipTest) + if call.excinfo.errisinstance(tuple(skip_exceptions)): outcome = "skipped" r = collector._repr_failure_py(call.excinfo, "line").reprcrash longrepr = (str(r.path), r.lineno, r.message) diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index 216266979..c9bdf79c1 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -11,6 +11,7 @@ from _pytest.outcomes import skip from _pytest.outcomes import xfail from _pytest.python import Class from _pytest.python import Function +from _pytest.runner import CallInfo def pytest_pycollect_makeitem(collector, name, obj): @@ -223,6 +224,14 @@ def pytest_runtest_makereport(item, call): except AttributeError: pass + unittest = sys.modules.get("unittest") + if unittest and call.excinfo and call.excinfo.errisinstance(unittest.SkipTest): + # let's substitute the excinfo with a pytest.skip one + call2 = CallInfo.from_call( + lambda: pytest.skip(str(call.excinfo.value)), call.when + ) + call.excinfo = call2.excinfo + # twisted trial support diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 2467ddd39..039068269 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -939,9 +939,7 @@ def test_class_method_containing_test_issue1558(testdir): reprec.assertoutcome(passed=1) -@pytest.mark.parametrize( - "base", ["builtins.object", "unittest.TestCase", "unittest2.TestCase"] -) +@pytest.mark.parametrize("base", ["builtins.object", "unittest.TestCase"]) def test_usefixtures_marker_on_unittest(base, testdir): """#3498""" module = base.rsplit(".", 1)[0] diff --git a/tox.ini b/tox.ini index 832b97298..b666b44bd 100644 --- a/tox.ini +++ b/tox.ini @@ -45,7 +45,6 @@ deps = pexpect: pexpect pluggymaster: git+https://github.com/pytest-dev/pluggy.git@master twisted: twisted - twisted: unittest2 xdist: pytest-xdist>=1.13 {env:_PYTEST_TOX_EXTRA_DEP:} platform = {env:_PYTEST_TOX_PLATFORM:.*} From e98627223f7884505327baf80a14872ead7fc051 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 12 Jul 2019 06:14:36 +0200 Subject: [PATCH 30/70] remove the noop init of PyobjMixin --- src/_pytest/python.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 801a16a5c..1bbe7ff25 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -240,9 +240,6 @@ class PyobjContext: class PyobjMixin(PyobjContext): _ALLOW_MARKERS = True - def __init__(self, *k, **kw): - super().__init__(*k, **kw) - @property def obj(self): """Underlying Python object.""" From 13d750db2022ef2a9860a1a5a22c4b0d9a379fe4 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 12 Jul 2019 06:16:06 +0200 Subject: [PATCH 31/70] simplify the expression which transfers mark names to keywords --- src/_pytest/python.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 1bbe7ff25..8ca15fb0f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1399,14 +1399,11 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): # https://github.com/pytest-dev/pytest/issues/4569 self.keywords.update( - dict.fromkeys( - [ - mark.name - for mark in self.iter_markers() - if mark.name not in self.keywords - ], - True, - ) + { + mark.name: True + for mark in self.iter_markers() + if mark.name not in self.keywords + } ) if fixtureinfo is None: From 4480d3e51805e60f4645017242495d9fc3b21331 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 12 Jul 2019 17:44:17 +0200 Subject: [PATCH 32/70] generate_tests: always use call_extra - its a noop addition in the methods is empty list --- src/_pytest/python.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 8ca15fb0f..293194c68 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -391,12 +391,8 @@ class PyCollector(PyobjMixin, nodes.Collector): methods.append(module.pytest_generate_tests) if hasattr(cls, "pytest_generate_tests"): methods.append(cls().pytest_generate_tests) - if methods: - self.ihook.pytest_generate_tests.call_extra( - methods, dict(metafunc=metafunc) - ) - else: - self.ihook.pytest_generate_tests(metafunc=metafunc) + + self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) if not metafunc._calls: yield Function(name, parent=self, fixtureinfo=fixtureinfo) From 898028cb2282d6ff62cd99a95c6ab9d71fedd178 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 12 Jul 2019 17:45:23 +0200 Subject: [PATCH 33/70] remove unused _get_xunit_setup_teardown --- src/_pytest/python.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 293194c68..c671d9584 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -649,27 +649,6 @@ class Package(Module): pkg_prefixes.add(path) -def _get_xunit_setup_teardown(holder, attr_name, param_obj=None): - """ - Return a callable to perform xunit-style setup or teardown if - the function exists in the ``holder`` object. - The ``param_obj`` parameter is the parameter which will be passed to the function - when the callable is called without arguments, defaults to the ``holder`` object. - Return ``None`` if a suitable callable is not found. - """ - # TODO: only needed because of Package! - param_obj = param_obj if param_obj is not None else holder - result = _get_non_fixture_func(holder, attr_name) - if result is not None: - arg_count = result.__code__.co_argcount - if inspect.ismethod(result): - arg_count -= 1 - if arg_count: - return lambda: result(param_obj) - else: - return result - - def _call_with_optional_argument(func, arg): """Call the given function with the given argument if func accepts one argument, otherwise calls func without arguments""" From 2c071a060e607db21717f604527be7cd675bfeb8 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 12 Jul 2019 17:47:52 +0200 Subject: [PATCH 34/70] refactor resolve_arg_value_types * more explicit type checks * expand from list+tuple to sequence --- src/_pytest/python.py | 21 +++++++++++++-------- testing/python/metafunc.py | 11 +++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c671d9584..ce92e76ad 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1,11 +1,12 @@ """ Python test discovery, setup and run of test functions. """ -import collections import enum import fnmatch import inspect import os import sys import warnings +from collections import Counter +from collections.abc import Sequence from functools import partial from textwrap import dedent @@ -1042,12 +1043,9 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): * "params" if the argname should be the parameter of a fixture of the same name. * "funcargs" if the argname should be a parameter to the parametrized test function. """ - valtypes = {} - if indirect is True: - valtypes = dict.fromkeys(argnames, "params") - elif indirect is False: - valtypes = dict.fromkeys(argnames, "funcargs") - elif isinstance(indirect, (tuple, list)): + if isinstance(indirect, bool): + valtypes = dict.fromkeys(argnames, "params" if indirect else "funcargs") + elif isinstance(indirect, Sequence): valtypes = dict.fromkeys(argnames, "funcargs") for arg in indirect: if arg not in argnames: @@ -1058,6 +1056,13 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): pytrace=False, ) valtypes[arg] = "params" + else: + fail( + "In {func}: expected Sequence or boolean for indirect, got {type}".format( + type=type(indirect).__name__, func=self.function.__name__ + ), + pytrace=False, + ) return valtypes def _validate_if_using_arg_names(self, argnames, indirect): @@ -1185,7 +1190,7 @@ def idmaker(argnames, parametersets, idfn=None, ids=None, config=None, item=None if len(set(ids)) != len(ids): # The ids are not unique duplicates = [testid for testid in ids if ids.count(testid) > 1] - counters = collections.defaultdict(lambda: 0) + counters = Counter() for index, testid in enumerate(ids): if testid in duplicates: ids[index] = testid + str(counters[testid]) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 9173105df..4273ab796 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -599,6 +599,17 @@ class TestMetafunc: assert metafunc._calls[0].funcargs == dict(x="a", y="b") assert metafunc._calls[0].params == {} + def test_parametrize_indirect_wrong_type(self): + def func(x, y): + pass + + metafunc = self.Metafunc(func) + with pytest.raises( + pytest.fail.Exception, + match="In func: expected Sequence or boolean for indirect, got dict", + ): + metafunc.parametrize("x, y", [("a", "b")], indirect={}) + def test_parametrize_indirect_list_functional(self, testdir): """ #714 From 374c4325a8fb3e2645508b23824485c996322371 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Fri, 12 Jul 2019 17:55:58 +0200 Subject: [PATCH 35/70] refactor finding xunit setup/teardown functions s/_get_non_fixture_func(obj, name: str)/_get_first_non_fixture_func(obj, names: List[str])/ --- src/_pytest/python.py | 44 ++++++++++++++++++++++--------------------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ce92e76ad..8e5a02292 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -438,13 +438,12 @@ class Module(nodes.File, PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_module = _get_non_fixture_func(self.obj, "setUpModule") - if setup_module is None: - setup_module = _get_non_fixture_func(self.obj, "setup_module") - - teardown_module = _get_non_fixture_func(self.obj, "tearDownModule") - if teardown_module is None: - teardown_module = _get_non_fixture_func(self.obj, "teardown_module") + setup_module = _get_first_non_fixture_func( + self.obj, ("setUpModule", "setup_module") + ) + teardown_module = _get_first_non_fixture_func( + self.obj, ("tearDownModule", "teardown_module") + ) if setup_module is None and teardown_module is None: return @@ -466,8 +465,10 @@ class Module(nodes.File, PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_function = _get_non_fixture_func(self.obj, "setup_function") - teardown_function = _get_non_fixture_func(self.obj, "teardown_function") + setup_function = _get_first_non_fixture_func(self.obj, ("setup_function",)) + teardown_function = _get_first_non_fixture_func( + self.obj, ("teardown_function",) + ) if setup_function is None and teardown_function is None: return @@ -551,15 +552,15 @@ class Package(Module): def setup(self): # not using fixtures to call setup_module here because autouse fixtures # from packages are not called automatically (#4085) - setup_module = _get_non_fixture_func(self.obj, "setUpModule") - if setup_module is None: - setup_module = _get_non_fixture_func(self.obj, "setup_module") + setup_module = _get_first_non_fixture_func( + self.obj, ("setUpModule", "setup_module") + ) if setup_module is not None: _call_with_optional_argument(setup_module, self.obj) - teardown_module = _get_non_fixture_func(self.obj, "tearDownModule") - if teardown_module is None: - teardown_module = _get_non_fixture_func(self.obj, "teardown_module") + teardown_module = _get_first_non_fixture_func( + self.obj, ("tearDownModule", "teardown_module") + ) if teardown_module is not None: func = partial(_call_with_optional_argument, teardown_module, self.obj) self.addfinalizer(func) @@ -662,14 +663,15 @@ def _call_with_optional_argument(func, arg): func() -def _get_non_fixture_func(obj, name): +def _get_first_non_fixture_func(obj, names): """Return the attribute from the given object to be used as a setup/teardown xunit-style function, but only if not marked as a fixture to avoid calling it twice. """ - meth = getattr(obj, name, None) - if fixtures.getfixturemarker(meth) is None: - return meth + for name in names: + meth = getattr(obj, name, None) + if meth is not None and fixtures.getfixturemarker(meth) is None: + return meth class Class(PyCollector): @@ -709,7 +711,7 @@ class Class(PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_class = _get_non_fixture_func(self.obj, "setup_class") + setup_class = _get_first_non_fixture_func(self.obj, ("setup_class",)) teardown_class = getattr(self.obj, "teardown_class", None) if setup_class is None and teardown_class is None: return @@ -733,7 +735,7 @@ class Class(PyCollector): Using a fixture to invoke this methods ensures we play nicely and unsurprisingly with other fixtures (#517). """ - setup_method = _get_non_fixture_func(self.obj, "setup_method") + setup_method = _get_first_non_fixture_func(self.obj, ("setup_method",)) teardown_method = getattr(self.obj, "teardown_method", None) if setup_method is None and teardown_method is None: return From 35a57a0dfbe2dd25f82c66e6c3388fecbac4eb72 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 11:12:47 +0300 Subject: [PATCH 36/70] Use flake8's extend-ignore instead of ignore extend-ignore adds ignores in addition to flake8's existing ignores. The default ignores currently are: E121,E123,E126,E226,E24,E704,W503,W504 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 832b97298..87d8141a3 100644 --- a/tox.ini +++ b/tox.ini @@ -156,7 +156,7 @@ markers = [flake8] max-line-length = 120 -ignore = E203,W503 +extend-ignore = E203 [isort] ; This config mimics what reorder-python-imports does. From 866904ab80605351c97c922db76c0586924403dd Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 11:39:30 +0300 Subject: [PATCH 37/70] Revert "Let context-managers for raises and warns handle unknown keyword arguments" This reverts commit dfe54cd82f55f17f3c9e6e078325f306a046b93b. The idea in the commit was to simplify the code by removing the check and instead letting it TypeError which has the same effect. However this type error is caught by mypy, and rather than ignoring the error we think it's better and clearer to go back to the previous explicit check. --- src/_pytest/python_api.py | 9 ++++++--- src/_pytest/recwarn.py | 7 ++++++- testing/python/raises.py | 6 ++++++ testing/test_recwarn.py | 6 ++++++ 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 7c63a3588..aae5ced33 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -653,9 +653,12 @@ def raises(expected_exception, *args, match=None, **kwargs): message = "DID NOT RAISE {}".format(expected_exception) if not args: - return RaisesContext( - expected_exception, message=message, match_expr=match, **kwargs - ) + if kwargs: + msg = "Unexpected keyword arguments passed to pytest.raises: " + msg += ", ".join(sorted(kwargs)) + msg += "\nUse context-manager form instead?" + raise TypeError(msg) + return RaisesContext(expected_exception, message, match) else: func = args[0] if not callable(func): diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index b124c69d5..7e772aa35 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -76,7 +76,12 @@ def warns(expected_warning, *args, match=None, **kwargs): """ __tracebackhide__ = True if not args: - return WarningsChecker(expected_warning, match_expr=match, **kwargs) + if kwargs: + msg = "Unexpected keyword arguments passed to pytest.warns: " + msg += ", ".join(sorted(kwargs)) + msg += "\nUse context-manager form instead?" + raise TypeError(msg) + return WarningsChecker(expected_warning, match_expr=match) else: func = args[0] if not callable(func): diff --git a/testing/python/raises.py b/testing/python/raises.py index 1f5594c8a..668be57fc 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -248,3 +248,9 @@ class TestRaises: with pytest.raises(CrappyClass()): pass assert "via __class__" in excinfo.value.args[0] + + def test_raises_context_manager_with_kwargs(self): + with pytest.raises(TypeError) as excinfo: + with pytest.raises(Exception, foo="bar"): + pass + assert "Unexpected keyword arguments" in str(excinfo.value) diff --git a/testing/test_recwarn.py b/testing/test_recwarn.py index 65fdd1682..208dc5b44 100644 --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -374,3 +374,9 @@ class TestWarns: assert f() == 10 assert pytest.warns(UserWarning, f) == 10 assert pytest.warns(UserWarning, f) == 10 + + def test_warns_context_manager_with_kwargs(self): + with pytest.raises(TypeError) as excinfo: + with pytest.warns(UserWarning, foo="bar"): + pass + assert "Unexpected keyword arguments" in str(excinfo.value) From d7ee3dac2ce7ac8c335e95fd643b05ade9bec897 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 10 Jul 2019 11:23:02 +0300 Subject: [PATCH 38/70] Type-annotate pytest.{exit,skip,fail,xfail,importorskip} --- src/_pytest/outcomes.py | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index c7e26f5cc..aaf0b35fb 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -3,21 +3,26 @@ exception classes and constants handling test outcomes as well as functions creating them """ import sys +from typing import Any +from typing import Optional from packaging.version import Version +if False: # TYPE_CHECKING + from typing import NoReturn + class OutcomeException(BaseException): """ OutcomeException and its subclass instances indicate and contain info about test and collection outcomes. """ - def __init__(self, msg=None, pytrace=True): + def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: BaseException.__init__(self, msg) self.msg = msg self.pytrace = pytrace - def __repr__(self): + def __repr__(self) -> str: if self.msg: val = self.msg if isinstance(val, bytes): @@ -36,7 +41,12 @@ class Skipped(OutcomeException): # in order to have Skipped exception printing shorter/nicer __module__ = "builtins" - def __init__(self, msg=None, pytrace=True, allow_module_level=False): + def __init__( + self, + msg: Optional[str] = None, + pytrace: bool = True, + allow_module_level: bool = False, + ) -> None: OutcomeException.__init__(self, msg=msg, pytrace=pytrace) self.allow_module_level = allow_module_level @@ -50,7 +60,9 @@ class Failed(OutcomeException): class Exit(Exception): """ raised for immediate program exits (no tracebacks/summaries)""" - def __init__(self, msg="unknown reason", returncode=None): + def __init__( + self, msg: str = "unknown reason", returncode: Optional[int] = None + ) -> None: self.msg = msg self.returncode = returncode super().__init__(msg) @@ -59,7 +71,7 @@ class Exit(Exception): # exposed helper methods -def exit(msg, returncode=None): +def exit(msg: str, returncode: Optional[int] = None) -> "NoReturn": """ Exit testing process. @@ -74,7 +86,7 @@ def exit(msg, returncode=None): exit.Exception = Exit # type: ignore -def skip(msg="", *, allow_module_level=False): +def skip(msg: str = "", *, allow_module_level: bool = False) -> "NoReturn": """ Skip an executing test with the given message. @@ -101,7 +113,7 @@ def skip(msg="", *, allow_module_level=False): skip.Exception = Skipped # type: ignore -def fail(msg="", pytrace=True): +def fail(msg: str = "", pytrace: bool = True) -> "NoReturn": """ Explicitly fail an executing test with the given message. @@ -121,7 +133,7 @@ class XFailed(Failed): """ raised from an explicit call to pytest.xfail() """ -def xfail(reason=""): +def xfail(reason: str = "") -> "NoReturn": """ Imperatively xfail an executing test or setup functions with the given reason. @@ -139,7 +151,9 @@ def xfail(reason=""): xfail.Exception = XFailed # type: ignore -def importorskip(modname, minversion=None, reason=None): +def importorskip( + modname: str, minversion: Optional[str] = None, reason: Optional[str] = None +) -> Any: """Imports and returns the requested module ``modname``, or skip the current test if the module cannot be imported. From 2dca68b863441ebe7e2ce16dcad9aaf6201a8fe7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 10 Jul 2019 14:36:07 +0300 Subject: [PATCH 39/70] Type-annotate pytest.warns --- src/_pytest/recwarn.py | 110 ++++++++++++++++++++++++++++++++--------- 1 file changed, 87 insertions(+), 23 deletions(-) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 7e772aa35..19e3938c3 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -1,11 +1,23 @@ """ recording warnings during test function execution. """ -import inspect import re import warnings +from types import TracebackType +from typing import Any +from typing import Callable +from typing import Iterator +from typing import List +from typing import Optional +from typing import overload +from typing import Pattern +from typing import Tuple +from typing import Union from _pytest.fixtures import yield_fixture from _pytest.outcomes import fail +if False: # TYPE_CHECKING + from typing import Type + @yield_fixture def recwarn(): @@ -42,7 +54,32 @@ def deprecated_call(func=None, *args, **kwargs): return warns((DeprecationWarning, PendingDeprecationWarning), *args, **kwargs) -def warns(expected_warning, *args, match=None, **kwargs): +@overload +def warns( + expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], + *, + match: Optional[Union[str, Pattern]] = ... +) -> "WarningsChecker": + ... # pragma: no cover + + +@overload +def warns( + expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], + func: Callable, + *args: Any, + match: Optional[Union[str, Pattern]] = ..., + **kwargs: Any +) -> Union[Any]: + ... # pragma: no cover + + +def warns( + expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], + *args: Any, + match: Optional[Union[str, Pattern]] = None, + **kwargs: Any +) -> Union["WarningsChecker", Any]: r"""Assert that code raises a particular class of warning. Specifically, the parameter ``expected_warning`` can be a warning class or @@ -101,26 +138,26 @@ class WarningsRecorder(warnings.catch_warnings): def __init__(self): super().__init__(record=True) self._entered = False - self._list = [] + self._list = [] # type: List[warnings._Record] @property - def list(self): + def list(self) -> List["warnings._Record"]: """The list of recorded warnings.""" return self._list - def __getitem__(self, i): + def __getitem__(self, i: int) -> "warnings._Record": """Get a recorded warning by index.""" return self._list[i] - def __iter__(self): + def __iter__(self) -> Iterator["warnings._Record"]: """Iterate through the recorded warnings.""" return iter(self._list) - def __len__(self): + def __len__(self) -> int: """The number of recorded warnings.""" return len(self._list) - def pop(self, cls=Warning): + def pop(self, cls: "Type[Warning]" = Warning) -> "warnings._Record": """Pop the first recorded warning, raise exception if not exists.""" for i, w in enumerate(self._list): if issubclass(w.category, cls): @@ -128,54 +165,80 @@ class WarningsRecorder(warnings.catch_warnings): __tracebackhide__ = True raise AssertionError("%r not found in warning list" % cls) - def clear(self): + def clear(self) -> None: """Clear the list of recorded warnings.""" self._list[:] = [] - def __enter__(self): + # Type ignored because it doesn't exactly warnings.catch_warnings.__enter__ + # -- it returns a List but we only emulate one. + def __enter__(self) -> "WarningsRecorder": # type: ignore if self._entered: __tracebackhide__ = True raise RuntimeError("Cannot enter %r twice" % self) - self._list = super().__enter__() + _list = super().__enter__() + # record=True means it's None. + assert _list is not None + self._list = _list warnings.simplefilter("always") return self - def __exit__(self, *exc_info): + def __exit__( + self, + exc_type: Optional["Type[BaseException]"], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: if not self._entered: __tracebackhide__ = True raise RuntimeError("Cannot exit %r without entering first" % self) - super().__exit__(*exc_info) + super().__exit__(exc_type, exc_val, exc_tb) # Built-in catch_warnings does not reset entered state so we do it # manually here for this context manager to become reusable. self._entered = False + return False + class WarningsChecker(WarningsRecorder): - def __init__(self, expected_warning=None, match_expr=None): + def __init__( + self, + expected_warning: Optional[ + Union["Type[Warning]", Tuple["Type[Warning]", ...]] + ] = None, + match_expr: Optional[Union[str, Pattern]] = None, + ) -> None: super().__init__() msg = "exceptions must be derived from Warning, not %s" - if isinstance(expected_warning, tuple): + if expected_warning is None: + expected_warning_tup = None + elif isinstance(expected_warning, tuple): for exc in expected_warning: - if not inspect.isclass(exc): + if not issubclass(exc, Warning): raise TypeError(msg % type(exc)) - elif inspect.isclass(expected_warning): - expected_warning = (expected_warning,) - elif expected_warning is not None: + expected_warning_tup = expected_warning + elif issubclass(expected_warning, Warning): + expected_warning_tup = (expected_warning,) + else: raise TypeError(msg % type(expected_warning)) - self.expected_warning = expected_warning + self.expected_warning = expected_warning_tup self.match_expr = match_expr - def __exit__(self, *exc_info): - super().__exit__(*exc_info) + def __exit__( + self, + exc_type: Optional["Type[BaseException]"], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: + super().__exit__(exc_type, exc_val, exc_tb) __tracebackhide__ = True # only check if we're not currently handling an exception - if all(a is None for a in exc_info): + if exc_type is None and exc_val is None and exc_tb is None: if self.expected_warning is not None: if not any(issubclass(r.category, self.expected_warning) for r in self): __tracebackhide__ = True @@ -200,3 +263,4 @@ class WarningsChecker(WarningsRecorder): [each.message for each in self], ) ) + return False From 55a570e5135cc8e08f242794b2b7a38677d81838 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 10 Jul 2019 12:30:29 +0300 Subject: [PATCH 40/70] Type-annotate ExceptionInfo --- src/_pytest/_code/code.py | 75 ++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 25 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index d63c010e4..d9b06ffd9 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -5,6 +5,11 @@ import traceback from inspect import CO_VARARGS from inspect import CO_VARKEYWORDS from traceback import format_exception_only +from types import TracebackType +from typing import Optional +from typing import Pattern +from typing import Tuple +from typing import Union from weakref import ref import attr @@ -15,6 +20,9 @@ import _pytest from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr +if False: # TYPE_CHECKING + from typing import Type + class Code: """ wrapper around Python code objects """ @@ -379,12 +387,14 @@ class ExceptionInfo: _assert_start_repr = "AssertionError('assert " - _excinfo = attr.ib() - _striptext = attr.ib(default="") - _traceback = attr.ib(default=None) + _excinfo = attr.ib( + type=Optional[Tuple["Type[BaseException]", BaseException, TracebackType]] + ) + _striptext = attr.ib(type=str, default="") + _traceback = attr.ib(type=Optional[Traceback], default=None) @classmethod - def from_current(cls, exprinfo=None): + def from_current(cls, exprinfo: Optional[str] = None) -> "ExceptionInfo": """returns an ExceptionInfo matching the current traceback .. warning:: @@ -396,8 +406,11 @@ class ExceptionInfo: strip ``AssertionError`` from the output, defaults to the exception message/``__str__()`` """ - tup = sys.exc_info() - assert tup[0] is not None, "no current exception" + tup_ = sys.exc_info() + assert tup_[0] is not None, "no current exception" + assert tup_[1] is not None, "no current exception" + assert tup_[2] is not None, "no current exception" + tup = (tup_[0], tup_[1], tup_[2]) _striptext = "" if exprinfo is None and isinstance(tup[1], AssertionError): exprinfo = getattr(tup[1], "msg", None) @@ -409,48 +422,60 @@ class ExceptionInfo: return cls(tup, _striptext) @classmethod - def for_later(cls): + def for_later(cls) -> "ExceptionInfo": """return an unfilled ExceptionInfo """ return cls(None) @property - def type(self): + def type(self) -> "Type[BaseException]": """the exception class""" + assert ( + self._excinfo is not None + ), ".type can only be used after the context manager exits" return self._excinfo[0] @property - def value(self): + def value(self) -> BaseException: """the exception value""" + assert ( + self._excinfo is not None + ), ".value can only be used after the context manager exits" return self._excinfo[1] @property - def tb(self): + def tb(self) -> TracebackType: """the exception raw traceback""" + assert ( + self._excinfo is not None + ), ".tb can only be used after the context manager exits" return self._excinfo[2] @property - def typename(self): + def typename(self) -> str: """the type name of the exception""" + assert ( + self._excinfo is not None + ), ".typename can only be used after the context manager exits" return self.type.__name__ @property - def traceback(self): + def traceback(self) -> Traceback: """the traceback""" if self._traceback is None: self._traceback = Traceback(self.tb, excinfo=ref(self)) return self._traceback @traceback.setter - def traceback(self, value): + def traceback(self, value: Traceback) -> None: self._traceback = value - def __repr__(self): + def __repr__(self) -> str: if self._excinfo is None: return "" return "" % (self.typename, len(self.traceback)) - def exconly(self, tryshort=False): + def exconly(self, tryshort: bool = False) -> str: """ return the exception as a string when 'tryshort' resolves to True, and the exception is a @@ -466,11 +491,11 @@ class ExceptionInfo: text = text[len(self._striptext) :] return text - def errisinstance(self, exc): + def errisinstance(self, exc: "Type[BaseException]") -> bool: """ return True if the exception is an instance of exc """ return isinstance(self.value, exc) - def _getreprcrash(self): + def _getreprcrash(self) -> "ReprFileLocation": exconly = self.exconly(tryshort=True) entry = self.traceback.getcrashentry() path, lineno = entry.frame.code.raw.co_filename, entry.lineno @@ -478,13 +503,13 @@ class ExceptionInfo: def getrepr( self, - showlocals=False, - style="long", - abspath=False, - tbfilter=True, - funcargs=False, - truncate_locals=True, - chain=True, + showlocals: bool = False, + style: str = "long", + abspath: bool = False, + tbfilter: bool = True, + funcargs: bool = False, + truncate_locals: bool = True, + chain: bool = True, ): """ Return str()able representation of this exception info. @@ -535,7 +560,7 @@ class ExceptionInfo: ) return fmt.repr_excinfo(self) - def match(self, regexp): + def match(self, regexp: Union[str, Pattern]) -> bool: """ Check whether the regular expression 'regexp' is found in the string representation of the exception using ``re.search``. If it matches From 56dcc9e1f884dc9f5f699c975a303cb0a97ccfa9 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 10 Jul 2019 11:28:43 +0300 Subject: [PATCH 41/70] Type-annotate pytest.raises --- src/_pytest/python_api.py | 63 ++++++++++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 7 deletions(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index aae5ced33..9ede24df6 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -7,6 +7,13 @@ from collections.abc import Sized from decimal import Decimal from itertools import filterfalse from numbers import Number +from types import TracebackType +from typing import Any +from typing import Callable +from typing import Optional +from typing import overload +from typing import Pattern +from typing import Tuple from typing import Union from more_itertools.more import always_iterable @@ -15,6 +22,9 @@ import _pytest._code from _pytest.compat import STRING_TYPES from _pytest.outcomes import fail +if False: # TYPE_CHECKING + from typing import Type # noqa: F401 (used in type string) + BASE_TYPE = (type, STRING_TYPES) @@ -528,7 +538,32 @@ def _is_numpy_array(obj): # builtin pytest.raises helper -def raises(expected_exception, *args, match=None, **kwargs): +@overload +def raises( + expected_exception: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]], + *, + match: Optional[Union[str, Pattern]] = ... +) -> "RaisesContext": + ... # pragma: no cover + + +@overload +def raises( + expected_exception: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]], + func: Callable, + *args: Any, + match: Optional[str] = ..., + **kwargs: Any +) -> Optional[_pytest._code.ExceptionInfo]: + ... # pragma: no cover + + +def raises( + expected_exception: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]], + *args: Any, + match: Optional[Union[str, Pattern]] = None, + **kwargs: Any +) -> Union["RaisesContext", Optional[_pytest._code.ExceptionInfo]]: r""" Assert that a code block/function call raises ``expected_exception`` or raise a failure exception otherwise. @@ -676,21 +711,35 @@ raises.Exception = fail.Exception # type: ignore class RaisesContext: - def __init__(self, expected_exception, message, match_expr): + def __init__( + self, + expected_exception: Union[ + "Type[BaseException]", Tuple["Type[BaseException]", ...] + ], + message: str, + match_expr: Optional[Union[str, Pattern]] = None, + ) -> None: self.expected_exception = expected_exception self.message = message self.match_expr = match_expr - self.excinfo = None + self.excinfo = None # type: Optional[_pytest._code.ExceptionInfo] - def __enter__(self): + def __enter__(self) -> _pytest._code.ExceptionInfo: self.excinfo = _pytest._code.ExceptionInfo.for_later() return self.excinfo - def __exit__(self, *tp): + def __exit__( + self, + exc_type: Optional["Type[BaseException]"], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> bool: __tracebackhide__ = True - if tp[0] is None: + if exc_type is None: fail(self.message) - self.excinfo.__init__(tp) + assert self.excinfo is not None + # Type ignored because mypy doesn't like calling __init__ directly like this. + self.excinfo.__init__((exc_type, exc_val, exc_tb)) # type: ignore suppress_exception = issubclass(self.excinfo.type, self.expected_exception) if self.match_expr is not None and suppress_exception: self.excinfo.match(self.match_expr) From 14bf4cdf44be4d8e2482b1f2b9cafeba06c03550 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 10 Jul 2019 20:12:41 +0300 Subject: [PATCH 42/70] Make ExceptionInfo generic in the exception type This way, in with pytest.raises(ValueError) as cm: ... cm.value is a ValueError and not a BaseException. --- src/_pytest/_code/code.py | 21 +++++++++++++-------- src/_pytest/python_api.py | 33 ++++++++++++++++++++------------- 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index d9b06ffd9..203e90287 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -6,9 +6,11 @@ from inspect import CO_VARARGS from inspect import CO_VARKEYWORDS from traceback import format_exception_only from types import TracebackType +from typing import Generic from typing import Optional from typing import Pattern from typing import Tuple +from typing import TypeVar from typing import Union from weakref import ref @@ -379,22 +381,25 @@ co_equal = compile( ) +_E = TypeVar("_E", bound=BaseException) + + @attr.s(repr=False) -class ExceptionInfo: +class ExceptionInfo(Generic[_E]): """ wraps sys.exc_info() objects and offers help for navigating the traceback. """ _assert_start_repr = "AssertionError('assert " - _excinfo = attr.ib( - type=Optional[Tuple["Type[BaseException]", BaseException, TracebackType]] - ) + _excinfo = attr.ib(type=Optional[Tuple["Type[_E]", "_E", TracebackType]]) _striptext = attr.ib(type=str, default="") _traceback = attr.ib(type=Optional[Traceback], default=None) @classmethod - def from_current(cls, exprinfo: Optional[str] = None) -> "ExceptionInfo": + def from_current( + cls, exprinfo: Optional[str] = None + ) -> "ExceptionInfo[BaseException]": """returns an ExceptionInfo matching the current traceback .. warning:: @@ -422,13 +427,13 @@ class ExceptionInfo: return cls(tup, _striptext) @classmethod - def for_later(cls) -> "ExceptionInfo": + def for_later(cls) -> "ExceptionInfo[_E]": """return an unfilled ExceptionInfo """ return cls(None) @property - def type(self) -> "Type[BaseException]": + def type(self) -> "Type[_E]": """the exception class""" assert ( self._excinfo is not None @@ -436,7 +441,7 @@ class ExceptionInfo: return self._excinfo[0] @property - def value(self) -> BaseException: + def value(self) -> _E: """the exception value""" assert ( self._excinfo is not None diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 9ede24df6..7ca545878 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -10,10 +10,13 @@ from numbers import Number from types import TracebackType from typing import Any from typing import Callable +from typing import cast +from typing import Generic from typing import Optional from typing import overload from typing import Pattern from typing import Tuple +from typing import TypeVar from typing import Union from more_itertools.more import always_iterable @@ -537,33 +540,35 @@ def _is_numpy_array(obj): # builtin pytest.raises helper +_E = TypeVar("_E", bound=BaseException) + @overload def raises( - expected_exception: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]], + expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], *, match: Optional[Union[str, Pattern]] = ... -) -> "RaisesContext": +) -> "RaisesContext[_E]": ... # pragma: no cover @overload def raises( - expected_exception: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]], + expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], func: Callable, *args: Any, match: Optional[str] = ..., **kwargs: Any -) -> Optional[_pytest._code.ExceptionInfo]: +) -> Optional[_pytest._code.ExceptionInfo[_E]]: ... # pragma: no cover def raises( - expected_exception: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]], + expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], *args: Any, match: Optional[Union[str, Pattern]] = None, **kwargs: Any -) -> Union["RaisesContext", Optional[_pytest._code.ExceptionInfo]]: +) -> Union["RaisesContext[_E]", Optional[_pytest._code.ExceptionInfo[_E]]]: r""" Assert that a code block/function call raises ``expected_exception`` or raise a failure exception otherwise. @@ -703,28 +708,30 @@ def raises( try: func(*args[1:], **kwargs) except expected_exception: - return _pytest._code.ExceptionInfo.from_current() + # Cast to narrow the type to expected_exception (_E). + return cast( + _pytest._code.ExceptionInfo[_E], + _pytest._code.ExceptionInfo.from_current(), + ) fail(message) raises.Exception = fail.Exception # type: ignore -class RaisesContext: +class RaisesContext(Generic[_E]): def __init__( self, - expected_exception: Union[ - "Type[BaseException]", Tuple["Type[BaseException]", ...] - ], + expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], message: str, match_expr: Optional[Union[str, Pattern]] = None, ) -> None: self.expected_exception = expected_exception self.message = message self.match_expr = match_expr - self.excinfo = None # type: Optional[_pytest._code.ExceptionInfo] + self.excinfo = None # type: Optional[_pytest._code.ExceptionInfo[_E]] - def __enter__(self) -> _pytest._code.ExceptionInfo: + def __enter__(self) -> _pytest._code.ExceptionInfo[_E]: self.excinfo = _pytest._code.ExceptionInfo.for_later() return self.excinfo From 3f1fb625844a38871d7bc357d290f39ce87039ca Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Thu, 11 Jul 2019 14:36:13 +0300 Subject: [PATCH 43/70] Rework ExceptionInfo to not require manual __init__ call Mypy doesn't like calling __init__() in this way. --- src/_pytest/_code/code.py | 5 +++++ src/_pytest/python_api.py | 14 +++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 203e90287..07ed8066c 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -432,6 +432,11 @@ class ExceptionInfo(Generic[_E]): """ return cls(None) + def fill_unfilled(self, exc_info: Tuple["Type[_E]", _E, TracebackType]) -> None: + """fill an unfilled ExceptionInfo created with for_later()""" + assert self._excinfo is None, "ExceptionInfo was already filled" + self._excinfo = exc_info + @property def type(self) -> "Type[_E]": """the exception class""" diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 7ca545878..e3cb8a970 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -745,9 +745,13 @@ class RaisesContext(Generic[_E]): if exc_type is None: fail(self.message) assert self.excinfo is not None - # Type ignored because mypy doesn't like calling __init__ directly like this. - self.excinfo.__init__((exc_type, exc_val, exc_tb)) # type: ignore - suppress_exception = issubclass(self.excinfo.type, self.expected_exception) - if self.match_expr is not None and suppress_exception: + if not issubclass(exc_type, self.expected_exception): + return False + # Cast to narrow the exception type now that it's verified. + exc_info = cast( + Tuple["Type[_E]", _E, TracebackType], (exc_type, exc_val, exc_tb) + ) + self.excinfo.fill_unfilled(exc_info) + if self.match_expr is not None: self.excinfo.match(self.match_expr) - return suppress_exception + return True From 11f1f792226622fc69a99b2a1b567630130b09f8 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 11:36:33 +0300 Subject: [PATCH 44/70] Allow creating ExceptionInfo from existing exc_info for better typing This way the ExceptionInfo generic parameter can be inferred from the passed-in exc_info. See for example the replaced cast(). --- src/_pytest/_code/code.py | 47 +++++++++++++++++++++++++----------- src/_pytest/python_api.py | 10 ++++---- testing/code/test_excinfo.py | 10 +++++++- 3 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 07ed8066c..30ab01235 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -396,6 +396,33 @@ class ExceptionInfo(Generic[_E]): _striptext = attr.ib(type=str, default="") _traceback = attr.ib(type=Optional[Traceback], default=None) + @classmethod + def from_exc_info( + cls, + exc_info: Tuple["Type[_E]", "_E", TracebackType], + exprinfo: Optional[str] = None, + ) -> "ExceptionInfo[_E]": + """returns an ExceptionInfo for an existing exc_info tuple. + + .. warning:: + + Experimental API + + + :param exprinfo: a text string helping to determine if we should + strip ``AssertionError`` from the output, defaults + to the exception message/``__str__()`` + """ + _striptext = "" + if exprinfo is None and isinstance(exc_info[1], AssertionError): + exprinfo = getattr(exc_info[1], "msg", None) + if exprinfo is None: + exprinfo = saferepr(exc_info[1]) + if exprinfo and exprinfo.startswith(cls._assert_start_repr): + _striptext = "AssertionError: " + + return cls(exc_info, _striptext) + @classmethod def from_current( cls, exprinfo: Optional[str] = None @@ -411,20 +438,12 @@ class ExceptionInfo(Generic[_E]): strip ``AssertionError`` from the output, defaults to the exception message/``__str__()`` """ - tup_ = sys.exc_info() - assert tup_[0] is not None, "no current exception" - assert tup_[1] is not None, "no current exception" - assert tup_[2] is not None, "no current exception" - tup = (tup_[0], tup_[1], tup_[2]) - _striptext = "" - if exprinfo is None and isinstance(tup[1], AssertionError): - exprinfo = getattr(tup[1], "msg", None) - if exprinfo is None: - exprinfo = saferepr(tup[1]) - if exprinfo and exprinfo.startswith(cls._assert_start_repr): - _striptext = "AssertionError: " - - return cls(tup, _striptext) + tup = sys.exc_info() + assert tup[0] is not None, "no current exception" + assert tup[1] is not None, "no current exception" + assert tup[2] is not None, "no current exception" + exc_info = (tup[0], tup[1], tup[2]) + return cls.from_exc_info(exc_info) @classmethod def for_later(cls) -> "ExceptionInfo[_E]": diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index e3cb8a970..08426d69c 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -707,11 +707,11 @@ def raises( ) try: func(*args[1:], **kwargs) - except expected_exception: - # Cast to narrow the type to expected_exception (_E). - return cast( - _pytest._code.ExceptionInfo[_E], - _pytest._code.ExceptionInfo.from_current(), + except expected_exception as e: + # We just caught the exception - there is a traceback. + assert e.__traceback__ is not None + return _pytest._code.ExceptionInfo.from_exc_info( + (type(e), e, e.__traceback__) ) fail(message) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index d7771833a..76f974957 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -58,7 +58,7 @@ class TWMock: fullwidth = 80 -def test_excinfo_simple(): +def test_excinfo_simple() -> None: try: raise ValueError except ValueError: @@ -66,6 +66,14 @@ def test_excinfo_simple(): assert info.type == ValueError +def test_excinfo_from_exc_info_simple(): + try: + raise ValueError + except ValueError as e: + info = _pytest._code.ExceptionInfo.from_exc_info((type(e), e, e.__traceback__)) + assert info.type == ValueError + + def test_excinfo_getstatement(): def g(): raise ValueError From 0225be53a2e9f8f69536ff91241b30a8b1d313d7 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 21:42:53 +0300 Subject: [PATCH 45/70] saferepr: Remove dead SafeRepr.repr_unicode This function is not called anywhere directly, and cannot be called by the dynamic `repr_()` dispatch mechanism because unicode is no longer a type in Python 3. --- src/_pytest/_io/saferepr.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 74f75124f..b5387d205 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -25,24 +25,6 @@ class SafeRepr(reprlib.Repr): def repr(self, x): return self._callhelper(reprlib.Repr.repr, self, x) - def repr_unicode(self, x, level): - # Strictly speaking wrong on narrow builds - def repr(u): - if "'" not in u: - return "'%s'" % u - elif '"' not in u: - return '"%s"' % u - else: - return "'%s'" % u.replace("'", r"\'") - - s = repr(x[: self.maxstring]) - if len(s) > self.maxstring: - i = max(0, (self.maxstring - 3) // 2) - j = max(0, self.maxstring - 3 - i) - s = repr(x[:i] + x[len(x) - j :]) - s = s[:i] + "..." + s[len(s) - j :] - return s - def repr_instance(self, x, level): return self._callhelper(repr, x) From 0394ebffee0b711c78fb81e4e057f6190a111250 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 21:50:59 +0300 Subject: [PATCH 46/70] saferepr: Use an __init__ instead of setting attributes after construction This will be easier to type-check, and also somewhat clearer. --- src/_pytest/_io/saferepr.py | 13 +++++++------ testing/io/test_saferepr.py | 2 +- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index b5387d205..5a4c76b4b 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -22,6 +22,12 @@ class SafeRepr(reprlib.Repr): and includes information on exceptions raised during the call. """ + def __init__(self, maxsize): + super().__init__() + self.maxstring = maxsize + self.maxsize = maxsize + self.maxother = 160 + def repr(self, x): return self._callhelper(reprlib.Repr.repr, self, x) @@ -52,9 +58,4 @@ def saferepr(obj, maxsize=240): care to never raise exceptions itself. This function is a wrapper around the Repr/reprlib functionality of the standard 2.6 lib. """ - # review exception handling - srepr = SafeRepr() - srepr.maxstring = maxsize - srepr.maxsize = maxsize - srepr.maxother = 160 - return srepr.repr(obj) + return SafeRepr(maxsize).repr(obj) diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index f6abfe322..f005f0cc4 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -48,7 +48,7 @@ def test_exceptions(): def test_big_repr(): from _pytest._io.saferepr import SafeRepr - assert len(saferepr(range(1000))) <= len("[" + SafeRepr().maxlist * "1000" + "]") + assert len(saferepr(range(1000))) <= len("[" + SafeRepr(0).maxlist * "1000" + "]") def test_repr_on_newstyle(): From c7aacc96bbeae1590342f6b10f2364751f187c0c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 22:21:15 +0300 Subject: [PATCH 47/70] saferepr: Remove unused setting of max_other max_other is used by the superclass repr_instance, but we override it and use maxsize instead. --- src/_pytest/_io/saferepr.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 5a4c76b4b..a97411876 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -26,7 +26,6 @@ class SafeRepr(reprlib.Repr): super().__init__() self.maxstring = maxsize self.maxsize = maxsize - self.maxother = 160 def repr(self, x): return self._callhelper(reprlib.Repr.repr, self, x) From 129600d698622633a9e8036f89e88b50aa101abd Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 22:24:12 +0300 Subject: [PATCH 48/70] saferepr: Avoid indirect function calls The DRY savings they provide are rather small, while they make it harder to type-check, and IMO harder to understand. --- changelog/5603.trivial.rst | 1 + src/_pytest/_io/saferepr.py | 53 +++++++++++++++++++++---------------- testing/io/test_saferepr.py | 11 ++++++++ 3 files changed, 42 insertions(+), 23 deletions(-) create mode 100644 changelog/5603.trivial.rst diff --git a/changelog/5603.trivial.rst b/changelog/5603.trivial.rst new file mode 100644 index 000000000..310e88562 --- /dev/null +++ b/changelog/5603.trivial.rst @@ -0,0 +1 @@ +Simplified internal ``SafeRepr`` class and removed some dead code. diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index a97411876..7704421a2 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -2,19 +2,23 @@ import pprint import reprlib -def _call_and_format_exception(call, x, *args): +def _format_repr_exception(exc, obj): + exc_name = type(exc).__name__ try: - # Try the vanilla repr and make sure that the result is a string - return call(x, *args) - except Exception as exc: - exc_name = type(exc).__name__ - try: - exc_info = str(exc) - except Exception: - exc_info = "unknown" - return '<[{}("{}") raised in repr()] {} object at 0x{:x}>'.format( - exc_name, exc_info, x.__class__.__name__, id(x) - ) + exc_info = str(exc) + except Exception: + exc_info = "unknown" + return '<[{}("{}") raised in repr()] {} object at 0x{:x}>'.format( + exc_name, exc_info, obj.__class__.__name__, id(obj) + ) + + +def _ellipsize(s, maxsize): + if len(s) > maxsize: + i = max(0, (maxsize - 3) // 2) + j = max(0, maxsize - 3 - i) + return s[:i] + "..." + s[len(s) - j :] + return s class SafeRepr(reprlib.Repr): @@ -28,18 +32,18 @@ class SafeRepr(reprlib.Repr): self.maxsize = maxsize def repr(self, x): - return self._callhelper(reprlib.Repr.repr, self, x) + try: + s = super().repr(x) + except Exception as exc: + s = _format_repr_exception(exc, x) + return _ellipsize(s, self.maxsize) def repr_instance(self, x, level): - return self._callhelper(repr, x) - - def _callhelper(self, call, x, *args): - s = _call_and_format_exception(call, x, *args) - if len(s) > self.maxsize: - i = max(0, (self.maxsize - 3) // 2) - j = max(0, self.maxsize - 3 - i) - s = s[:i] + "..." + s[len(s) - j :] - return s + try: + s = repr(x) + except Exception as exc: + s = _format_repr_exception(exc, x) + return _ellipsize(s, self.maxsize) def safeformat(obj): @@ -47,7 +51,10 @@ def safeformat(obj): Failing __repr__ functions of user instances will be represented with a short exception info. """ - return _call_and_format_exception(pprint.pformat, obj) + try: + return pprint.pformat(obj) + except Exception as exc: + return _format_repr_exception(exc, obj) def saferepr(obj, maxsize=240): diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index f005f0cc4..86897b57c 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -45,6 +45,17 @@ def test_exceptions(): assert "unknown" in s2 +def test_buggy_builtin_repr(): + # Simulate a case where a repr for a builtin raises. + # reprlib dispatches by type name, so use "int". + + class int: + def __repr__(self): + raise ValueError("Buggy repr!") + + assert "Buggy" in saferepr(int()) + + def test_big_repr(): from _pytest._io.saferepr import SafeRepr From fd8f92d0e706f4ec1d2a9815859e5e5189aaae18 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 18:07:04 +0300 Subject: [PATCH 49/70] Run mypy on src/ and testing/ together This makes testing/ actually pick up the pytest imports -- otherwise they are opaque and we don't actually test the types. A single run is also a bit faster and simpler. The original reason why we split it is no longer relevant (we fixed the problems). --- .pre-commit-config.yaml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fce7978c4..2b0d6e49f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,12 +47,7 @@ repos: rev: v0.711 hooks: - id: mypy - name: mypy (src) - files: ^src/ - args: [] - - id: mypy - name: mypy (testing) - files: ^testing/ + files: ^(src/|testing/) args: [] - repo: local hooks: From 104f8fc8364c66425191e3937708153f1ee154aa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 18:14:34 +0300 Subject: [PATCH 50/70] Update mypy from 0.711 to 0.720 Release notes: http://mypy-lang.blogspot.com/2019/07/mypy-0720-released.html --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2b0d6e49f..59128a4fb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: hooks: - id: rst-backticks - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.711 + rev: v0.720 hooks: - id: mypy files: ^(src/|testing/) From 8d413c1926e489e49f0a1b4df92b071288ec5d2b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Tue, 16 Jul 2019 23:25:13 +0300 Subject: [PATCH 51/70] Allow tuple of exceptions in ExceptionInfo.errisinstance isinstance() accepts it and some code does pass a tuple. Fixup for commit 55a570e5135cc8e08f242794b2b7a38677d81838. --- src/_pytest/_code/code.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 30ab01235..7d72234e7 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -520,7 +520,9 @@ class ExceptionInfo(Generic[_E]): text = text[len(self._striptext) :] return text - def errisinstance(self, exc: "Type[BaseException]") -> bool: + def errisinstance( + self, exc: Union["Type[BaseException]", Tuple["Type[BaseException]", ...]] + ) -> bool: """ return True if the exception is an instance of exc """ return isinstance(self.value, exc) From 65aee1e0c877def80e32b95be60f0edcb2cb7d5c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 18:27:09 +0300 Subject: [PATCH 52/70] Allow bytes for OutcomeException(msg=...) It's __repr__ explicitly handles it so allow it. --- src/_pytest/outcomes.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index aaf0b35fb..df37312ba 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -5,6 +5,7 @@ as well as functions creating them import sys from typing import Any from typing import Optional +from typing import Union from packaging.version import Version @@ -17,7 +18,9 @@ class OutcomeException(BaseException): contain info about test and collection outcomes. """ - def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: + def __init__( + self, msg: Optional[Union[str, bytes]] = None, pytrace: bool = True + ) -> None: BaseException.__init__(self, msg) self.msg = msg self.pytrace = pytrace From 7d1c697c306929e5a02dd34268dba865dfdd33b6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 14 Jul 2019 18:26:05 +0300 Subject: [PATCH 53/70] Remove a no-longer-needed check if enum is available Not needed since 4d49ba65297102110ae8aeecdb3b82b23a231fba. --- src/_pytest/python.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 8e5a02292..82ed8b9cf 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1164,7 +1164,7 @@ def _idval(val, argname, idx, idfn, item, config): return str(val) elif isinstance(val, REGEX_TYPE): return ascii_escaped(val.pattern) - elif enum is not None and isinstance(val, enum.Enum): + elif isinstance(val, enum.Enum): return str(val) elif (inspect.isclass(val) or inspect.isfunction(val)) and hasattr(val, "__name__"): return val.__name__ From 0b532fda76813ae4793669be5c439f8075bfd9ff Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Mon, 15 Jul 2019 22:09:01 +0300 Subject: [PATCH 54/70] Remove unnecessary checks from SetupState Since 4622c28ffdd10ff3b3e7c9d4c59ab7c0d1284cc3, _finalizers cannot contain Nones or tuples, so these checks are not longer needed. --- src/_pytest/runner.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index edaee9725..8aae163c3 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -278,10 +278,7 @@ class SetupState: self._finalizers = {} def addfinalizer(self, finalizer, colitem): - """ attach a finalizer to the given colitem. - if colitem is None, this will add a finalizer that - is called at the end of teardown_all(). - """ + """ attach a finalizer to the given colitem. """ assert colitem and not isinstance(colitem, tuple) assert callable(finalizer) # assert colitem in self.stack # some unit tests don't setup stack :/ @@ -309,12 +306,9 @@ class SetupState: def _teardown_with_finalization(self, colitem): self._callfinalizers(colitem) - if hasattr(colitem, "teardown"): - colitem.teardown() + colitem.teardown() for colitem in self._finalizers: - assert ( - colitem is None or colitem in self.stack or isinstance(colitem, tuple) - ) + assert colitem in self.stack def teardown_all(self): while self.stack: From 675e9507d80fc477d416d38781e6bccc8bb5c0c2 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 17 Jul 2019 10:33:10 +0300 Subject: [PATCH 55/70] Don't accept bytes message in pytest.{fail,xfail,skip} It seems to have been added in #1439 to fix #1178. This was only relevant for Python 2 where it was tempting to use str (== bytes) literals instead of unicode literals. In Python 3, it is unlikely that anyone passes bytes to these functions. --- changelog/5615.removal.rst | 7 +++++++ src/_pytest/outcomes.py | 10 ++-------- 2 files changed, 9 insertions(+), 8 deletions(-) create mode 100644 changelog/5615.removal.rst diff --git a/changelog/5615.removal.rst b/changelog/5615.removal.rst new file mode 100644 index 000000000..6dd9aec1d --- /dev/null +++ b/changelog/5615.removal.rst @@ -0,0 +1,7 @@ +``pytest.fail``, ``pytest.xfail`` and ``pytest.skip`` no longer support bytes for the message argument. + +This was supported for Python 2 where it was tempting to use ``"message"`` +instead of ``u"message"``. + +Python 3 code is unlikely to pass ``bytes`` to these functions. If you do, +please decode it to an ``str`` beforehand. diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index df37312ba..a5a4e655b 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -5,7 +5,6 @@ as well as functions creating them import sys from typing import Any from typing import Optional -from typing import Union from packaging.version import Version @@ -18,19 +17,14 @@ class OutcomeException(BaseException): contain info about test and collection outcomes. """ - def __init__( - self, msg: Optional[Union[str, bytes]] = None, pytrace: bool = True - ) -> None: + def __init__(self, msg: Optional[str] = None, pytrace: bool = True) -> None: BaseException.__init__(self, msg) self.msg = msg self.pytrace = pytrace def __repr__(self) -> str: if self.msg: - val = self.msg - if isinstance(val, bytes): - val = val.decode("UTF-8", errors="replace") - return val + return self.msg return "<{} instance>".format(self.__class__.__name__) __str__ = __repr__ From 3c7438969aa81300b264bbe6ffce6be271e0ecce Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 23 Jul 2019 06:46:56 +0200 Subject: [PATCH 56/70] Replace internal config._origargs with invocation_params.args Added in https://github.com/pytest-dev/pytest/pull/5564. --- src/_pytest/config/__init__.py | 6 +++--- src/_pytest/helpconfig.py | 2 +- testing/test_config.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index cb9a72d0e..dbcc4cc67 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -632,10 +632,10 @@ class Config: Object containing the parameters regarding the ``pytest.main`` invocation. - Contains the followinig read-only attributes: + Contains the following read-only attributes: * ``args``: list of command-line arguments as passed to ``pytest.main()``. - * ``plugins``: list of extra plugins, might be None + * ``plugins``: list of extra plugins, might be None. * ``dir``: directory where ``pytest.main()`` was invoked from. """ @@ -915,7 +915,7 @@ class Config: assert not hasattr( self, "args" ), "can only parse cmdline args at most once per Config object" - self._origargs = args + assert self.invocation_params.args == args self.hook.pytest_addhooks.call_historic( kwargs=dict(pluginmanager=self.pluginmanager) ) diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index b379fae01..50acc2d7d 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -98,7 +98,7 @@ def pytest_cmdline_parse(): py.__version__, ".".join(map(str, sys.version_info)), os.getcwd(), - config._origargs, + config.invocation_params.args, ) ) config.trace.root.setwriter(debugfile.write) diff --git a/testing/test_config.py b/testing/test_config.py index bda89834b..143cb90d1 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -441,7 +441,7 @@ class TestConfigFromdictargs: assert config.option.capture == "no" assert config.args == args - def test_origargs(self, _sys_snapshot): + def test_invocation_params_args(self, _sys_snapshot): """Show that fromdictargs can handle args in their "orig" format""" from _pytest.config import Config @@ -450,7 +450,7 @@ class TestConfigFromdictargs: config = Config.fromdictargs(option_dict, args) assert config.args == ["a", "b"] - assert config._origargs == args + assert config.invocation_params.args == args assert config.option.verbose == 4 assert config.option.capture == "no" From 1ce45a6f6733128aab750674acb82f97a66f29b0 Mon Sep 17 00:00:00 2001 From: Samuel Searles-Bryant Date: Sat, 3 Aug 2019 15:11:36 +0100 Subject: [PATCH 57/70] Add hostname and timestamp to JUnit XML testsuite tag Fix #5471 --- changelog/5471.trivial.rst | 1 + src/_pytest/junitxml.py | 4 ++++ testing/test_junitxml.py | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 changelog/5471.trivial.rst diff --git a/changelog/5471.trivial.rst b/changelog/5471.trivial.rst new file mode 100644 index 000000000..154b64ea7 --- /dev/null +++ b/changelog/5471.trivial.rst @@ -0,0 +1 @@ +JUnit XML now includes a timestamp and hostname in the testsuite tag. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 15c630b1d..ba0f48ed3 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -10,9 +10,11 @@ src/main/resources/org/jenkinsci/plugins/xunit/types/model/xsd/junit-10.xsd """ import functools import os +import platform import re import sys import time +from datetime import datetime import py @@ -666,6 +668,8 @@ class LogXML: skipped=self.stats["skipped"], tests=numtests, time="%.3f" % suite_time_delta, + timestamp=datetime.fromtimestamp(self.suite_start_time).isoformat(), + hostname=platform.node(), ) logfile.write(Junit.testsuites([suite_node]).unicode(indent=0)) logfile.close() diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 3196f0ebd..76420b7d4 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,4 +1,6 @@ import os +import platform +from datetime import datetime from xml.dom import minidom import py @@ -139,6 +141,30 @@ class TestPython: node = dom.find_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5) + def test_hostname_in_xml(self, testdir): + testdir.makepyfile( + """ + def test_pass(): + pass + """ + ) + result, dom = runandparse(testdir) + node = dom.find_first_by_tag("testsuite") + node.assert_attr(hostname=platform.node()) + + def test_timestamp_in_xml(self, testdir): + testdir.makepyfile( + """ + def test_pass(): + pass + """ + ) + start_time = datetime.now() + result, dom = runandparse(testdir) + node = dom.find_first_by_tag("testsuite") + timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f") + assert start_time <= timestamp < datetime.now() + def test_timing_function(self, testdir): testdir.makepyfile( """ From cf6632a57a4f6cbc6e3b3cb80c3e0ecd8bc2460d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 5 Aug 2019 10:19:05 -0300 Subject: [PATCH 58/70] Rename 5471.trivial.rst to 5471.feature.rst --- changelog/{5471.trivial.rst => 5471.feature.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{5471.trivial.rst => 5471.feature.rst} (100%) diff --git a/changelog/5471.trivial.rst b/changelog/5471.feature.rst similarity index 100% rename from changelog/5471.trivial.rst rename to changelog/5471.feature.rst From ba76080b59c902ee3dc8afad84aad877f9b6cf9c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 12 Jul 2019 15:14:04 -0300 Subject: [PATCH 59/70] Validate xunit2 files against the schema Fix #5095 --- changelog/5095.trivial.rst | 2 + setup.py | 5 +- testing/example_scripts/junit-10.xsd | 147 +++++++++++++ testing/test_junitxml.py | 305 +++++++++++++++++---------- 4 files changed, 340 insertions(+), 119 deletions(-) create mode 100644 changelog/5095.trivial.rst create mode 100644 testing/example_scripts/junit-10.xsd diff --git a/changelog/5095.trivial.rst b/changelog/5095.trivial.rst new file mode 100644 index 000000000..2256cf9f4 --- /dev/null +++ b/changelog/5095.trivial.rst @@ -0,0 +1,2 @@ +XML files of the ``xunit2`` family are now validated against the schema by pytest's own test suite +to avoid future regressions. diff --git a/setup.py b/setup.py index e8bc8e895..adbafb557 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,6 @@ def main(): use_scm_version={"write_to": "src/_pytest/_version.py"}, setup_requires=["setuptools-scm", "setuptools>=40.0"], package_dir={"": "src"}, - # fmt: off extras_require={ "testing": [ "argcomplete", @@ -29,9 +28,9 @@ def main(): "mock", "nose", "requests", - ], + "xmlschema", + ] }, - # fmt: on install_requires=INSTALL_REQUIRES, ) diff --git a/testing/example_scripts/junit-10.xsd b/testing/example_scripts/junit-10.xsd new file mode 100644 index 000000000..286fbf7c8 --- /dev/null +++ b/testing/example_scripts/junit-10.xsd @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 76420b7d4..069663693 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,20 +1,47 @@ import os import platform from datetime import datetime +from pathlib import Path from xml.dom import minidom import py +import xmlschema import pytest from _pytest.junitxml import LogXML from _pytest.reports import BaseReport -def runandparse(testdir, *args): - resultpath = testdir.tmpdir.join("junit.xml") - result = testdir.runpytest("--junitxml=%s" % resultpath, *args) - xmldoc = minidom.parse(str(resultpath)) - return result, DomNode(xmldoc) +@pytest.fixture(scope="session") +def schema(): + """Returns a xmlschema.XMLSchema object for the junit-10.xsd file""" + fn = Path(__file__).parent / "example_scripts/junit-10.xsd" + with fn.open() as f: + return xmlschema.XMLSchema(f) + + +@pytest.fixture +def run_and_parse(testdir, schema): + """ + Fixture that returns a function that can be used to execute pytest and return + the parsed ``DomNode`` of the root xml node. + + The ``family`` parameter is used to configure the ``junit_family`` of the written report. + "xunit2" is also automatically validated against the schema. + """ + + def run(*args, family="xunit1"): + if family: + args = ("-o", "junit_family=" + family) + args + xml_path = testdir.tmpdir.join("junit.xml") + result = testdir.runpytest("--junitxml=%s" % xml_path, *args) + if family == "xunit2": + with xml_path.open() as f: + schema.validate(f) + xmldoc = minidom.parse(str(xml_path)) + return result, DomNode(xmldoc) + + return run def assert_attr(node, **kwargs): @@ -91,8 +118,12 @@ class DomNode: return type(self)(self.__node.nextSibling) +parametrize_families = pytest.mark.parametrize("xunit_family", ["xunit1", "xunit2"]) + + class TestPython: - def test_summing_simple(self, testdir): + @parametrize_families + def test_summing_simple(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -110,12 +141,13 @@ class TestPython: assert 1 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=0, failures=1, skipped=2, tests=5) - def test_summing_simple_with_errors(self, testdir): + @parametrize_families + def test_summing_simple_with_errors(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -136,23 +168,25 @@ class TestPython: assert True """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(name="pytest", errors=1, failures=2, skipped=1, tests=5) - def test_hostname_in_xml(self, testdir): + @parametrize_families + def test_hostname_in_xml(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ def test_pass(): pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) node = dom.find_first_by_tag("testsuite") node.assert_attr(hostname=platform.node()) - def test_timestamp_in_xml(self, testdir): + @parametrize_families + def test_timestamp_in_xml(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ def test_pass(): @@ -160,12 +194,12 @@ class TestPython: """ ) start_time = datetime.now() - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) node = dom.find_first_by_tag("testsuite") timestamp = datetime.strptime(node["timestamp"], "%Y-%m-%dT%H:%M:%S.%f") assert start_time <= timestamp < datetime.now() - def test_timing_function(self, testdir): + def test_timing_function(self, testdir, run_and_parse): testdir.makepyfile( """ import time, pytest @@ -177,14 +211,16 @@ class TestPython: time.sleep(0.01) """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") val = tnode["time"] assert round(float(val), 2) >= 0.03 @pytest.mark.parametrize("duration_report", ["call", "total"]) - def test_junit_duration_report(self, testdir, monkeypatch, duration_report): + def test_junit_duration_report( + self, testdir, monkeypatch, duration_report, run_and_parse + ): # mock LogXML.node_reporter so it always sets a known duration to each test report object original_node_reporter = LogXML.node_reporter @@ -202,8 +238,8 @@ class TestPython: pass """ ) - result, dom = runandparse( - testdir, "-o", "junit_duration_report={}".format(duration_report) + result, dom = run_and_parse( + "-o", "junit_duration_report={}".format(duration_report) ) node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") @@ -214,7 +250,8 @@ class TestPython: assert duration_report == "call" assert val == 1.0 - def test_setup_error(self, testdir): + @parametrize_families + def test_setup_error(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -226,7 +263,7 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) @@ -236,7 +273,8 @@ class TestPython: fnode.assert_attr(message="test setup failure") assert "ValueError" in fnode.toxml() - def test_teardown_error(self, testdir): + @parametrize_families + def test_teardown_error(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -249,7 +287,7 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") @@ -258,7 +296,8 @@ class TestPython: fnode.assert_attr(message="test teardown failure") assert "ValueError" in fnode.toxml() - def test_call_failure_teardown_error(self, testdir): + @parametrize_families + def test_call_failure_teardown_error(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -271,7 +310,7 @@ class TestPython: raise Exception("Call Exception") """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, failures=1, tests=1) @@ -283,7 +322,8 @@ class TestPython: snode = second.find_first_by_tag("error") snode.assert_attr(message="test teardown failure") - def test_skip_contains_name_reason(self, testdir): + @parametrize_families + def test_skip_contains_name_reason(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -291,7 +331,7 @@ class TestPython: pytest.skip("hello23") """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1) @@ -300,7 +340,8 @@ class TestPython: snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello23") - def test_mark_skip_contains_name_reason(self, testdir): + @parametrize_families + def test_mark_skip_contains_name_reason(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -309,7 +350,7 @@ class TestPython: assert True """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1) @@ -320,7 +361,10 @@ class TestPython: snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello24") - def test_mark_skipif_contains_name_reason(self, testdir): + @parametrize_families + def test_mark_skipif_contains_name_reason( + self, testdir, run_and_parse, xunit_family + ): testdir.makepyfile( """ import pytest @@ -330,7 +374,7 @@ class TestPython: assert True """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1) @@ -341,7 +385,10 @@ class TestPython: snode = tnode.find_first_by_tag("skipped") snode.assert_attr(type="pytest.skip", message="hello25") - def test_mark_skip_doesnt_capture_output(self, testdir): + @parametrize_families + def test_mark_skip_doesnt_capture_output( + self, testdir, run_and_parse, xunit_family + ): testdir.makepyfile( """ import pytest @@ -350,12 +397,13 @@ class TestPython: print("bar!") """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node_xml = dom.find_first_by_tag("testsuite").toxml() assert "bar!" not in node_xml - def test_classname_instance(self, testdir): + @parametrize_families + def test_classname_instance(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ class TestClass(object): @@ -363,7 +411,7 @@ class TestPython: assert 0 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1) @@ -372,20 +420,22 @@ class TestPython: classname="test_classname_instance.TestClass", name="test_method" ) - def test_classname_nested_dir(self, testdir): + @parametrize_families + def test_classname_nested_dir(self, testdir, run_and_parse, xunit_family): p = testdir.tmpdir.ensure("sub", "test_hello.py") p.write("def test_func(): 0/0") - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr(classname="sub.test_hello", name="test_func") - def test_internal_error(self, testdir): + @parametrize_families + def test_internal_error(self, testdir, run_and_parse, xunit_family): testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") testdir.makepyfile("def test_function(): pass") - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) @@ -396,7 +446,10 @@ class TestPython: assert "Division" in fnode.toxml() @pytest.mark.parametrize("junit_logging", ["no", "system-out", "system-err"]) - def test_failure_function(self, testdir, junit_logging): + @parametrize_families + def test_failure_function( + self, testdir, junit_logging, run_and_parse, xunit_family + ): testdir.makepyfile( """ import logging @@ -411,7 +464,9 @@ class TestPython: """ ) - result, dom = runandparse(testdir, "-o", "junit_logging=%s" % junit_logging) + result, dom = run_and_parse( + "-o", "junit_logging=%s" % junit_logging, family=xunit_family + ) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1, tests=1) @@ -439,7 +494,8 @@ class TestPython: assert "warning msg" not in systemout.toxml() assert "warning msg" not in systemerr.toxml() - def test_failure_verbose_message(self, testdir): + @parametrize_families + def test_failure_verbose_message(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import sys @@ -447,14 +503,14 @@ class TestPython: assert 0, "An error" """ ) - - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="AssertionError: An error assert 0") - def test_failure_escape(self, testdir): + @parametrize_families + def test_failure_escape(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -464,7 +520,7 @@ class TestPython: assert 0 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=3, tests=3) @@ -479,7 +535,8 @@ class TestPython: text = sysout.text assert text == "%s\n" % char - def test_junit_prefixing(self, testdir): + @parametrize_families + def test_junit_prefixing(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ def test_func(): @@ -489,7 +546,7 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir, "--junitprefix=xyz") + result, dom = run_and_parse("--junitprefix=xyz", family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(failures=1, tests=2) @@ -500,7 +557,8 @@ class TestPython: classname="xyz.test_junit_prefixing.TestHello", name="test_hello" ) - def test_xfailure_function(self, testdir): + @parametrize_families + def test_xfailure_function(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -508,7 +566,7 @@ class TestPython: pytest.xfail("42") """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert not result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1, tests=1) @@ -516,9 +574,9 @@ class TestPython: tnode.assert_attr(classname="test_xfailure_function", name="test_xfail") fnode = tnode.find_first_by_tag("skipped") fnode.assert_attr(type="pytest.xfail", message="42") - # assert "ValueError" in fnode.toxml() - def test_xfailure_marker(self, testdir): + @parametrize_families + def test_xfailure_marker(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -527,7 +585,7 @@ class TestPython: assert False """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert not result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=1, tests=1) @@ -536,7 +594,7 @@ class TestPython: fnode = tnode.find_first_by_tag("skipped") fnode.assert_attr(type="pytest.xfail", message="42") - def test_xfail_captures_output_once(self, testdir): + def test_xfail_captures_output_once(self, testdir, run_and_parse): testdir.makepyfile( """ import sys @@ -549,13 +607,14 @@ class TestPython: assert 0 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") assert len(tnode.find_by_tag("system-err")) == 1 assert len(tnode.find_by_tag("system-out")) == 1 - def test_xfailure_xpass(self, testdir): + @parametrize_families + def test_xfailure_xpass(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -564,14 +623,15 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) # assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=0, tests=1) tnode = node.find_first_by_tag("testcase") tnode.assert_attr(classname="test_xfailure_xpass", name="test_xpass") - def test_xfailure_xpass_strict(self, testdir): + @parametrize_families + def test_xfailure_xpass_strict(self, testdir, run_and_parse, xunit_family): testdir.makepyfile( """ import pytest @@ -580,7 +640,7 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) # assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(skipped=0, tests=1) @@ -589,9 +649,10 @@ class TestPython: fnode = tnode.find_first_by_tag("failure") fnode.assert_attr(message="[XPASS(strict)] This needs to fail!") - def test_collect_error(self, testdir): + @parametrize_families + def test_collect_error(self, testdir, run_and_parse, xunit_family): testdir.makepyfile("syntax error") - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=1, tests=1) @@ -600,7 +661,7 @@ class TestPython: fnode.assert_attr(message="collection failure") assert "SyntaxError" in fnode.toxml() - def test_unicode(self, testdir): + def test_unicode(self, testdir, run_and_parse): value = "hx\xc4\x85\xc4\x87\n" testdir.makepyfile( """\ @@ -611,14 +672,14 @@ class TestPython: """ % value ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert result.ret == 1 tnode = dom.find_first_by_tag("testcase") fnode = tnode.find_first_by_tag("failure") assert "hx" in fnode.toxml() - def test_assertion_binchars(self, testdir): - """this test did fail when the escaping wasn't strict""" + def test_assertion_binchars(self, testdir, run_and_parse): + """this test did fail when the escaping wasnt strict""" testdir.makepyfile( """ @@ -629,23 +690,23 @@ class TestPython: assert M1 == M2 """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() print(dom.toxml()) - def test_pass_captures_stdout(self, testdir): + def test_pass_captures_stdout(self, testdir, run_and_parse): testdir.makepyfile( """ def test_pass(): print('hello-stdout') """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-out") assert "hello-stdout" in systemout.toxml() - def test_pass_captures_stderr(self, testdir): + def test_pass_captures_stderr(self, testdir, run_and_parse): testdir.makepyfile( """ import sys @@ -653,13 +714,13 @@ class TestPython: sys.stderr.write('hello-stderr') """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-err") assert "hello-stderr" in systemout.toxml() - def test_setup_error_captures_stdout(self, testdir): + def test_setup_error_captures_stdout(self, testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -672,13 +733,13 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-out") assert "hello-stdout" in systemout.toxml() - def test_setup_error_captures_stderr(self, testdir): + def test_setup_error_captures_stderr(self, testdir, run_and_parse): testdir.makepyfile( """ import sys @@ -692,13 +753,13 @@ class TestPython: pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-err") assert "hello-stderr" in systemout.toxml() - def test_avoid_double_stdout(self, testdir): + def test_avoid_double_stdout(self, testdir, run_and_parse): testdir.makepyfile( """ import sys @@ -713,7 +774,7 @@ class TestPython: sys.stdout.write('hello-stdout call') """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() node = dom.find_first_by_tag("testsuite") pnode = node.find_first_by_tag("testcase") systemout = pnode.find_first_by_tag("system-out") @@ -756,7 +817,8 @@ def test_dont_configure_on_slaves(tmpdir): class TestNonPython: - def test_summing_simple(self, testdir): + @parametrize_families + def test_summing_simple(self, testdir, run_and_parse, xunit_family): testdir.makeconftest( """ import pytest @@ -774,7 +836,7 @@ class TestNonPython: """ ) testdir.tmpdir.join("myfile.xyz").write("hello") - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret node = dom.find_first_by_tag("testsuite") node.assert_attr(errors=0, failures=1, skipped=0, tests=1) @@ -822,8 +884,8 @@ def test_nullbyte_replace(testdir): def test_invalid_xml_escape(): # Test some more invalid xml chars, the full range should be - # tested really but let's just thest the edges of the ranges - # intead. + # tested really but let's just test the edges of the ranges + # instead. # XXX This only tests low unicode character points for now as # there are some issues with the testing infrastructure for # the higher ones. @@ -907,7 +969,7 @@ def test_logxml_check_isdir(testdir): result.stderr.fnmatch_lines(["*--junitxml must be a filename*"]) -def test_escaped_parametrized_names_xml(testdir): +def test_escaped_parametrized_names_xml(testdir, run_and_parse): testdir.makepyfile( """\ import pytest @@ -916,13 +978,13 @@ def test_escaped_parametrized_names_xml(testdir): assert char """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert result.ret == 0 node = dom.find_first_by_tag("testcase") node.assert_attr(name="test_func[\\x00]") -def test_double_colon_split_function_issue469(testdir): +def test_double_colon_split_function_issue469(testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -931,14 +993,14 @@ def test_double_colon_split_function_issue469(testdir): pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert result.ret == 0 node = dom.find_first_by_tag("testcase") node.assert_attr(classname="test_double_colon_split_function_issue469") node.assert_attr(name="test_func[double::colon]") -def test_double_colon_split_method_issue469(testdir): +def test_double_colon_split_method_issue469(testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -948,7 +1010,7 @@ def test_double_colon_split_method_issue469(testdir): pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert result.ret == 0 node = dom.find_first_by_tag("testcase") node.assert_attr(classname="test_double_colon_split_method_issue469.TestClass") @@ -984,7 +1046,7 @@ def test_unicode_issue368(testdir): log.pytest_sessionfinish() -def test_record_property(testdir): +def test_record_property(testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -996,7 +1058,7 @@ def test_record_property(testdir): record_property("foo", "<1"); """ ) - result, dom = runandparse(testdir, "-rwv") + result, dom = run_and_parse("-rwv") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag("properties") @@ -1005,7 +1067,7 @@ def test_record_property(testdir): pnodes[1].assert_attr(name="foo", value="<1") -def test_record_property_same_name(testdir): +def test_record_property_same_name(testdir, run_and_parse): testdir.makepyfile( """ def test_record_with_same_name(record_property): @@ -1013,7 +1075,7 @@ def test_record_property_same_name(testdir): record_property("foo", "baz") """ ) - result, dom = runandparse(testdir, "-rw") + result, dom = run_and_parse("-rw") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") psnode = tnode.find_first_by_tag("properties") @@ -1037,7 +1099,7 @@ def test_record_fixtures_without_junitxml(testdir, fixture_name): @pytest.mark.filterwarnings("default") -def test_record_attribute(testdir): +def test_record_attribute(testdir, run_and_parse): testdir.makeini( """ [pytest] @@ -1055,7 +1117,7 @@ def test_record_attribute(testdir): record_xml_attribute("foo", "<1"); """ ) - result, dom = runandparse(testdir, "-rw") + result, dom = run_and_parse("-rw") node = dom.find_first_by_tag("testsuite") tnode = node.find_first_by_tag("testcase") tnode.assert_attr(bar="1") @@ -1067,7 +1129,7 @@ def test_record_attribute(testdir): @pytest.mark.filterwarnings("default") @pytest.mark.parametrize("fixture_name", ["record_xml_attribute", "record_property"]) -def test_record_fixtures_xunit2(testdir, fixture_name): +def test_record_fixtures_xunit2(testdir, fixture_name, run_and_parse): """Ensure record_xml_attribute and record_property drop values when outside of legacy family """ testdir.makeini( @@ -1090,7 +1152,7 @@ def test_record_fixtures_xunit2(testdir, fixture_name): ) ) - result, dom = runandparse(testdir, "-rw") + result, dom = run_and_parse("-rw", family=None) expected_lines = [] if fixture_name == "record_xml_attribute": expected_lines.append( @@ -1105,7 +1167,7 @@ def test_record_fixtures_xunit2(testdir, fixture_name): result.stdout.fnmatch_lines(expected_lines) -def test_random_report_log_xdist(testdir, monkeypatch): +def test_random_report_log_xdist(testdir, monkeypatch, run_and_parse): """xdist calls pytest_runtest_logreport as they are executed by the slaves, with nodes from several nodes overlapping, so junitxml must cope with that to produce correct reports. #1064 @@ -1120,7 +1182,7 @@ def test_random_report_log_xdist(testdir, monkeypatch): assert i != 22 """ ) - _, dom = runandparse(testdir, "-n2") + _, dom = run_and_parse("-n2") suite_node = dom.find_first_by_tag("testsuite") failed = [] for case_node in suite_node.find_by_tag("testcase"): @@ -1130,21 +1192,22 @@ def test_random_report_log_xdist(testdir, monkeypatch): assert failed == ["test_x[22]"] -def test_root_testsuites_tag(testdir): +@parametrize_families +def test_root_testsuites_tag(testdir, run_and_parse, xunit_family): testdir.makepyfile( """ def test_x(): pass """ ) - _, dom = runandparse(testdir) + _, dom = run_and_parse(family=xunit_family) root = dom.get_unique_child assert root.tag == "testsuites" suite_node = root.get_unique_child assert suite_node.tag == "testsuite" -def test_runs_twice(testdir): +def test_runs_twice(testdir, run_and_parse): f = testdir.makepyfile( """ def test_pass(): @@ -1152,14 +1215,14 @@ def test_runs_twice(testdir): """ ) - result, dom = runandparse(testdir, f, f) + result, dom = run_and_parse(f, f) assert "INTERNALERROR" not in result.stdout.str() first, second = [x["classname"] for x in dom.find_by_tag("testcase")] assert first == second @pytest.mark.xfail(reason="hangs", run=False) -def test_runs_twice_xdist(testdir): +def test_runs_twice_xdist(testdir, run_and_parse): pytest.importorskip("xdist") f = testdir.makepyfile( """ @@ -1168,13 +1231,13 @@ def test_runs_twice_xdist(testdir): """ ) - result, dom = runandparse(testdir, f, "--dist", "each", "--tx", "2*popen") + result, dom = run_and_parse(f, "--dist", "each", "--tx", "2*popen") assert "INTERNALERROR" not in result.stdout.str() first, second = [x["classname"] for x in dom.find_by_tag("testcase")] assert first == second -def test_fancy_items_regression(testdir): +def test_fancy_items_regression(testdir, run_and_parse): # issue 1259 testdir.makeconftest( """ @@ -1207,7 +1270,7 @@ def test_fancy_items_regression(testdir): """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse() assert "INTERNALERROR" not in result.stdout.str() @@ -1226,9 +1289,10 @@ def test_fancy_items_regression(testdir): ] -def test_global_properties(testdir): +@parametrize_families +def test_global_properties(testdir, xunit_family): path = testdir.tmpdir.join("test_global_properties.xml") - log = LogXML(str(path), None) + log = LogXML(str(path), None, family=xunit_family) class Report(BaseReport): sections = [] @@ -1286,7 +1350,8 @@ def test_url_property(testdir): ), "The URL did not get written to the xml" -def test_record_testsuite_property(testdir): +@parametrize_families +def test_record_testsuite_property(testdir, run_and_parse, xunit_family): testdir.makepyfile( """ def test_func1(record_testsuite_property): @@ -1296,7 +1361,7 @@ def test_record_testsuite_property(testdir): record_testsuite_property("stats", 10) """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") properties_node = node.find_first_by_tag("properties") @@ -1334,14 +1399,16 @@ def test_record_testsuite_property_type_checking(testdir, junit): @pytest.mark.parametrize("suite_name", ["my_suite", ""]) -def test_set_suite_name(testdir, suite_name): +@parametrize_families +def test_set_suite_name(testdir, suite_name, run_and_parse, xunit_family): if suite_name: testdir.makeini( """ [pytest] - junit_suite_name={} + junit_suite_name={suite_name} + junit_family={family} """.format( - suite_name + suite_name=suite_name, family=xunit_family ) ) expected = suite_name @@ -1355,13 +1422,13 @@ def test_set_suite_name(testdir, suite_name): pass """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testsuite") node.assert_attr(name=expected) -def test_escaped_skipreason_issue3533(testdir): +def test_escaped_skipreason_issue3533(testdir, run_and_parse): testdir.makepyfile( """ import pytest @@ -1370,20 +1437,26 @@ def test_escaped_skipreason_issue3533(testdir): pass """ ) - _, dom = runandparse(testdir) + _, dom = run_and_parse() node = dom.find_first_by_tag("testcase") snode = node.find_first_by_tag("skipped") assert "1 <> 2" in snode.text snode.assert_attr(message="1 <> 2") -def test_logging_passing_tests_disabled_does_not_log_test_output(testdir): +@parametrize_families +def test_logging_passing_tests_disabled_does_not_log_test_output( + testdir, run_and_parse, xunit_family +): testdir.makeini( """ [pytest] junit_log_passing_tests=False junit_logging=system-out - """ + junit_family={family} + """.format( + family=xunit_family + ) ) testdir.makepyfile( """ @@ -1397,7 +1470,7 @@ def test_logging_passing_tests_disabled_does_not_log_test_output(testdir): logging.warning('hello') """ ) - result, dom = runandparse(testdir) + result, dom = run_and_parse(family=xunit_family) assert result.ret == 0 node = dom.find_first_by_tag("testcase") assert len(node.find_by_tag("system-err")) == 0 From ba72b480b97179c798339617f0178fa0422f951e Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Mon, 15 Jul 2019 10:21:36 -0300 Subject: [PATCH 60/70] Remove xfail marker from test_runs_twice_xdist Apperently it does not hang, let's see how it fares on CI --- testing/test_junitxml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 069663693..d4a1f6cc3 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1221,7 +1221,6 @@ def test_runs_twice(testdir, run_and_parse): assert first == second -@pytest.mark.xfail(reason="hangs", run=False) def test_runs_twice_xdist(testdir, run_and_parse): pytest.importorskip("xdist") f = testdir.makepyfile( From 9f6da8cbebb8d42c6f1997782b790a6d3b34e362 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 9 Aug 2019 16:34:10 +0200 Subject: [PATCH 61/70] Remove unused function CallSpec2.setall Fixes https://github.com/pytest-dev/pytest/issues/5253. --- src/_pytest/python.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 82ed8b9cf..4e6ae8513 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -873,18 +873,6 @@ class CallSpec2: self._idlist.append(id) self.marks.extend(normalize_mark_list(marks)) - def setall(self, funcargs, id, param): - for x in funcargs: - self._checkargnotcontained(x) - self.funcargs.update(funcargs) - if id is not NOTSET: - self._idlist.append(id) - if param is not NOTSET: - assert self._globalparam is NOTSET - self._globalparam = param - for arg in funcargs: - self._arg2scopenum[arg] = fixtures.scopenum_function - class Metafunc(fixtures.FuncargnamesCompatAttr): """ From beb457c75ec62c2176a3272f0a4f973c092aea20 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Aug 2019 09:26:37 -0300 Subject: [PATCH 62/70] Add new 'improvement' changelog category This creates a separate section from 'features' for small changes which don't usually require user intervention, such as: * Human readable session duration * New junitxml fields * Improved colors in terminal * etc. The idea is to better match user expectations about new actual features in the "Features" section of the changelog. --- .github/PULL_REQUEST_TEMPLATE.md | 2 +- .pre-commit-config.yaml | 2 +- changelog/{5471.feature.rst => 5471.improvement.rst} | 0 changelog/README.rst | 1 + pyproject.toml | 5 +++++ 5 files changed, 8 insertions(+), 2 deletions(-) rename changelog/{5471.feature.rst => 5471.improvement.rst} (100%) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 590cc9958..f516959bc 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -6,7 +6,7 @@ Here is a quick checklist that should be present in PRs. --> - [ ] Target the `master` branch for bug fixes, documentation updates and trivial changes. -- [ ] Target the `features` branch for new features and removals/deprecations. +- [ ] Target the `features` branch for new features, improvements, and removals/deprecations. - [ ] Include documentation when adding new features. - [ ] Include new tests or update existing tests when applicable. diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b82693c18..2768037bc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,7 +59,7 @@ repos: name: changelog filenames language: fail entry: 'changelog files must be named ####.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst' - exclude: changelog/(\d+\.(feature|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst) + exclude: changelog/(\d+\.(feature|improvement|bugfix|doc|deprecation|removal|vendor|trivial).rst|README.rst|_template.rst) files: ^changelog/ - id: py-deprecated name: py library is deprecated diff --git a/changelog/5471.feature.rst b/changelog/5471.improvement.rst similarity index 100% rename from changelog/5471.feature.rst rename to changelog/5471.improvement.rst diff --git a/changelog/README.rst b/changelog/README.rst index e471409b0..5c182758b 100644 --- a/changelog/README.rst +++ b/changelog/README.rst @@ -12,6 +12,7 @@ Each file should be named like ``..rst``, where ```` is an issue number, and ```` is one of: * ``feature``: new user facing features, like new command-line options and new behavior. +* ``improvement``: improvement of existing functionality, usually without requiring user intervention (for example, new fields being written in ``--junitxml``, improved colors in terminal, etc). * ``bugfix``: fixes a reported bug. * ``doc``: documentation improvement, like rewording an entire session or adding missing docs. * ``deprecation``: feature deprecation. diff --git a/pyproject.toml b/pyproject.toml index 2a4cd65c1..552cdfa0f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,11 @@ template = "changelog/_template.rst" name = "Features" showcontent = true + [[tool.towncrier.type]] + directory = "improvement" + name = "Improvements" + showcontent = true + [[tool.towncrier.type]] directory = "bugfix" name = "Bug Fixes" From 345df99db7365b34867e8429accb91287867341c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Aug 2019 09:14:57 -0300 Subject: [PATCH 63/70] Show session duration in human-readable format Fix #5707 --- changelog/5707.improvement.rst | 4 ++++ src/_pytest/pytester.py | 16 ++++++++-------- src/_pytest/terminal.py | 12 +++++++++++- testing/logging/test_reporting.py | 2 +- testing/test_pytester.py | 2 +- testing/test_terminal.py | 19 ++++++++++++++++++- 6 files changed, 43 insertions(+), 12 deletions(-) create mode 100644 changelog/5707.improvement.rst diff --git a/changelog/5707.improvement.rst b/changelog/5707.improvement.rst new file mode 100644 index 000000000..59176e1bb --- /dev/null +++ b/changelog/5707.improvement.rst @@ -0,0 +1,4 @@ +Time taken to run the test suite now includes a human-readable representation when it takes over +60 seconds, for example:: + + ===== 2 failed in 102.70s (0:01:42) ===== diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 2068761fa..80219b1a6 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -340,7 +340,10 @@ def _config_for_test(): config._ensure_unconfigure() # cleanup, e.g. capman closing tmpfiles. -rex_outcome = re.compile(r"(\d+) ([\w-]+)") +# regex to match the session duration string in the summary: "74.34s" +rex_session_duration = re.compile(r"\d+\.\d\ds") +# regex to match all the counts and phrases in the summary line: "34 passed, 111 skipped" +rex_outcome = re.compile(r"(\d+) (\w+)") class RunResult: @@ -379,14 +382,11 @@ class RunResult: """ for line in reversed(self.outlines): - if "seconds" in line: + if rex_session_duration.search(line): outcomes = rex_outcome.findall(line) - if outcomes: - d = {} - for num, cat in outcomes: - d[cat] = int(num) - return d - raise ValueError("Pytest terminal report not found") + return {noun: int(count) for (count, noun) in outcomes} + + raise ValueError("Pytest terminal summary report not found") def assert_outcomes( self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0 diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 05d5427c3..0d9794159 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -4,6 +4,7 @@ This is a good source for looking at the various reporting hooks. """ import argparse import collections +import datetime import platform import sys import time @@ -861,7 +862,7 @@ class TerminalReporter: def summary_stats(self): session_duration = time.time() - self._sessionstarttime (line, color) = build_summary_stats_line(self.stats) - msg = "{} in {:.2f} seconds".format(line, session_duration) + msg = "{} in {}".format(line, format_session_duration(session_duration)) markup = {color: True, "bold": True} if self.verbosity >= 0: @@ -1055,3 +1056,12 @@ def _plugin_nameversions(plugininfo): if name not in values: values.append(name) return values + + +def format_session_duration(seconds): + """Format the given seconds in a human readable manner to show in the final summary""" + if seconds < 60: + return "{:.2f}s".format(seconds) + else: + dt = datetime.timedelta(seconds=int(seconds)) + return "{:.2f}s ({})".format(seconds, dt) diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index bb1aebc09..1ae0bd783 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -946,7 +946,7 @@ def test_collection_collect_only_live_logging(testdir, verbose): expected_lines.extend( [ "*test_collection_collect_only_live_logging.py::test_simple*", - "no tests ran in * seconds", + "no tests ran in 0.[0-9][0-9]s", ] ) elif verbose == "-qq": diff --git a/testing/test_pytester.py b/testing/test_pytester.py index cf92741af..d330ff253 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -278,7 +278,7 @@ def test_assert_outcomes_after_pytest_error(testdir): testdir.makepyfile("def test_foo(): assert True") result = testdir.runpytest("--unexpected-argument") - with pytest.raises(ValueError, match="Pytest terminal report not found"): + with pytest.raises(ValueError, match="Pytest terminal summary report not found"): result.assert_outcomes(passed=0) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 381a5b2e1..88f96f894 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -617,7 +617,7 @@ class TestTerminalFunctional: pluggy.__version__, ), "*test_header_trailer_info.py .*", - "=* 1 passed*in *.[0-9][0-9] seconds *=", + "=* 1 passed*in *.[0-9][0-9]s *=", ] ) if request.config.pluginmanager.list_plugin_distinfo(): @@ -1678,3 +1678,20 @@ def test_line_with_reprcrash(monkeypatch): check("😄😄😄😄😄\n2nd line", 41, "FAILED nodeid::😄::withunicode - 😄😄...") check("😄😄😄😄😄\n2nd line", 42, "FAILED nodeid::😄::withunicode - 😄😄😄...") check("😄😄😄😄😄\n2nd line", 80, "FAILED nodeid::😄::withunicode - 😄😄😄😄😄") + + +@pytest.mark.parametrize( + "seconds, expected", + [ + (10.0, "10.00s"), + (10.34, "10.34s"), + (59.99, "59.99s"), + (60.55, "60.55s (0:01:00)"), + (123.55, "123.55s (0:02:03)"), + (60 * 60 + 0.5, "3600.50s (1:00:00)"), + ], +) +def test_format_session_duration(seconds, expected): + from _pytest.terminal import format_session_duration + + assert format_session_duration(seconds) == expected From cb481a354a2e0c7585caf5d2b72ae036f3d39f1b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 12 Aug 2019 03:41:14 +0200 Subject: [PATCH 64/70] assertrepr_compare: prefer same maxsize Previously it would say: > assert '123456789012...901234567890A' == '1234567890123...901234567890B'" This makes it look like the "3" might be different already. This is clearer, and it is OK to have potentially one less char in the right one: > assert '123456789012...901234567890A' == '123456789012...901234567890B'" --- src/_pytest/assertion/util.py | 6 +++--- testing/test_assertrewrite.py | 10 ++++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 106c44a8a..732194ec2 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -119,9 +119,9 @@ def isiterable(obj): def assertrepr_compare(config, op, left, right): """Return specialised explanations for some operators/operands""" - width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op - left_repr = saferepr(left, maxsize=int(width // 2)) - right_repr = saferepr(right, maxsize=width - len(left_repr)) + maxsize = (80 - 15 - len(op) - 2) // 2 # 15 chars indentation, 1 space around op + left_repr = saferepr(left, maxsize=maxsize) + right_repr = saferepr(right, maxsize=maxsize) summary = "{} {} {}".format(left_repr, op, right_repr) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 9f0979f77..89b23a72c 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -200,6 +200,16 @@ class TestAssertionRewrite: else: assert msg == ["assert cls == 42"] + def test_assertrepr_compare_same_width(self, request): + """Should use same width/truncation with same initial width.""" + + def f(): + assert "1234567890" * 5 + "A" == "1234567890" * 5 + "B" + + assert getmsg(f).splitlines()[0] == ( + "assert '123456789012...901234567890A' == '123456789012...901234567890B'" + ) + def test_dont_rewrite_if_hasattr_fails(self, request): class Y: """ A class whos getattr fails, but not with `AttributeError` """ From 39ba996133b560f520dbae5d6210aca9f66dadab Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 12 Aug 2019 07:47:39 +0200 Subject: [PATCH 65/70] assertion.rewrite: use ast.NameConstant(None) directly `ast.parse("None")` was added/used in 3e6f1fa2d for differences between Python 2/3, but we do not support py2 anymore. --- src/_pytest/assertion/rewrite.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 3ef92704b..0567e8fb8 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -33,6 +33,9 @@ PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version) PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT +AST_IS = ast.Is() +AST_NONE = ast.NameConstant(None) + class AssertionRewritingHook: """PEP302/PEP451 import hook which rewrites asserts.""" @@ -854,10 +857,7 @@ class AssertionRewriter(ast.NodeVisitor): internally already. See issue #3191 for more details. """ - - # Using parse because it is different between py2 and py3. - AST_NONE = ast.parse("None").body[0].value - val_is_none = ast.Compare(node, [ast.Is()], [AST_NONE]) + val_is_none = ast.Compare(node, [AST_IS], [AST_NONE]) send_warning = ast.parse( """\ from _pytest.warning_types import PytestAssertRewriteWarning From 1a61265b1edbb11881a68e7b18b57f729b741126 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 10 Aug 2019 11:03:36 -0300 Subject: [PATCH 66/70] Publish GitHub release notes after deployment Fix #2933 --- .travis.yml | 11 +++- scripts/publish_gh_release_notes.py | 86 +++++++++++++++++++++++++++++ tox.ini | 11 ++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 scripts/publish_gh_release_notes.py diff --git a/.travis.yml b/.travis.yml index af33d672e..5de40f3a4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -72,8 +72,17 @@ jobs: - stage: deploy python: '3.6' - install: pip install -U setuptools setuptools_scm + install: pip install -U setuptools setuptools_scm tox script: skip + # token to upload github release notes: GH_RELEASE_NOTES_TOKEN + env: + - secure: "OjOeL7/0JUDkV00SsTs732e8vQjHynpbG9FKTNtZZJ+1Zn4Cib+hAlwmlBnvVukML0X60YpcfjnC4quDOIGLPsh5zeXnvJmYtAIIUNQXjWz8NhcGYrhyzuP1rqV22U68RTCdmOq3lMYU/W2acwHP7T49PwJtOiUM5kF120UAQ0Zi5EmkqkIvH8oM5mO9Dlver+/U7Htpz9rhKrHBXQNCMZI6yj2aUyukqB2PN2fjAlDbCF//+FmvYw9NjT4GeFOSkTCf4ER9yfqs7yglRfwiLtOCZ2qKQhWZNsSJDB89rxIRXWavJUjJKeY2EW2/NkomYJDpqJLIF4JeFRw/HhA47CYPeo6BJqyyNV+0CovL1frpWfi9UQw2cMbgFUkUIUk3F6DD59PHNIOX2R/HX56dQsw7WKl3QuHlCOkICXYg8F7Ta684IoKjeTX03/6QNOkURfDBwfGszY0FpbxrjCSWKom6RyZdyidnESaxv9RzjcIRZVh1rp8KMrwS1OrwRSdG0zjlsPr49hWMenN/8fKgcHTV4/r1Tj6mip0dorSRCrgUNIeRBKgmui6FS8642ab5JNKOxMteVPVR2sFuhjOQ0Jy+PmvceYY9ZMWc3+/B/KVh0dZ3hwvLGZep/vxDS2PwCA5/xw31714vT5LxidKo8yECjBynMU/wUTTS695D3NY=" + addons: + apt: + packages: + # required by publish_gh_release_notes + - pandoc + after_deploy: tox -e publish_gh_release_notes deploy: provider: pypi user: nicoddemus diff --git a/scripts/publish_gh_release_notes.py b/scripts/publish_gh_release_notes.py new file mode 100644 index 000000000..f6c956fad --- /dev/null +++ b/scripts/publish_gh_release_notes.py @@ -0,0 +1,86 @@ +""" +Script used to publish GitHub release notes extracted from CHANGELOG.rst. + +This script is meant to be executed after a successful deployment in Travis. + +Uses the following environment variables: + +* GIT_TAG: the name of the tag of the current commit. +* GH_RELEASE_NOTES_TOKEN: a personal access token with 'repo' permissions. It should be encrypted using: + + $travis encrypt GH_RELEASE_NOTES_TOKEN= -r pytest-dev/pytest + + And the contents pasted in the ``deploy.env.secure`` section in the ``travis.yml`` file. + +The script also requires ``pandoc`` to be previously installed in the system. + +Requires Python3.6+. +""" +import os +import re +import sys +from pathlib import Path + +import github3 +import pypandoc + + +def publish_github_release(token, tag_name, body): + github = github3.login(token=token) + repo = github.repository("pytest-dev", "pytest") + return repo.create_release(tag_name=tag_name, body=body) + + +def parse_changelog(tag_name): + p = Path(__file__).parent.parent / "CHANGELOG.rst" + changelog_lines = p.read_text(encoding="UTF-8").splitlines() + + title_regex = re.compile(r"pytest (\d\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)") + consuming_version = False + version_lines = [] + for line in changelog_lines: + m = title_regex.match(line) + if m: + # found the version we want: start to consume lines until we find the next version title + if m.group(1) == tag_name: + consuming_version = True + # found a new version title while parsing the version we want: break out + elif consuming_version: + break + if consuming_version: + version_lines.append(line) + + return "\n".join(version_lines) + + +def convert_rst_to_md(text): + return pypandoc.convert_text(text, "md", format="rst") + + +def main(argv): + if len(argv) > 1: + tag_name = argv[1] + else: + tag_name = os.environ.get("TRAVIS_TAG") + if not tag_name: + print("tag_name not given and $TRAVIS_TAG not set", file=sys.stderr) + return 1 + + token = os.environ.get("GH_RELEASE_NOTES_TOKEN") + if not token: + print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr) + return 1 + + rst_body = parse_changelog(tag_name) + md_body = convert_rst_to_md(rst_body) + if not publish_github_release(token, tag_name, md_body): + print("Could not publish release notes:", file=sys.stderr) + print(md_body, file=sys.stderr) + return 5 + + print(f"Release notes for {tag_name} published successfully") + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/tox.ini b/tox.ini index e158ece67..6467ddacd 100644 --- a/tox.ini +++ b/tox.ini @@ -114,6 +114,17 @@ deps = wheel commands = python scripts/release.py {posargs} +[testenv:publish_gh_release_notes] +description = create GitHub release after deployment +basepython = python3.6 +usedevelop = True +passenv = GH_RELEASE_NOTES_TOKEN TRAVIS_TAG +deps = + github3.py + pypandoc +commands = python scripts/publish_gh_release_notes.py + + [pytest] minversion = 2.0 addopts = -ra -p pytester --strict-markers From 1434b66c3543d9b7876501c03913d77ad9404c0f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 13 Aug 2019 17:54:40 +0200 Subject: [PATCH 67/70] pytester: spawn: skip without pexpect.spawn "pexpect" can be imported on Windows, but does not have "pexpect.spawn" then. Ref: https://github.com/pexpect/pexpect/blob/a803933ed53/pexpect/__init__.py#L73-L76 --- src/_pytest/pytester.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 80219b1a6..5835f01d1 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1188,6 +1188,8 @@ class Testdir: pytest.skip("pypy-64 bit not supported") if sys.platform.startswith("freebsd"): pytest.xfail("pexpect does not work reliably on freebsd") + if not hasattr(pexpect, "spawn"): + pytest.skip("pexpect.spawn not available") logfile = self.tmpdir.join("spawn.out").open("wb") # Do not load user config. From cb94fd31c870384e13240d71ee487639e3360ee3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 14 Aug 2019 18:51:00 -0300 Subject: [PATCH 68/70] Use TRAVIS_REPO_SLUG instead of hard-coding pytest-dev/pytest I was doing final tests on the script today, and forgot to change the hardecoded "pytest-dev/pytest", which ended up publishing a `4.99.10` release to the main repository by mistake, as my token has access to both my fork and main repository. I deleted the tag immeditely just a few seconds later, so hopefully this won't cause major problems. This change makes it safer to test this in the future, never publishing to the main repository by mistake (as long as the tags are pushed to the right repositories of course). --- scripts/publish_gh_release_notes.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/scripts/publish_gh_release_notes.py b/scripts/publish_gh_release_notes.py index f6c956fad..23f7b40ad 100644 --- a/scripts/publish_gh_release_notes.py +++ b/scripts/publish_gh_release_notes.py @@ -25,9 +25,10 @@ import github3 import pypandoc -def publish_github_release(token, tag_name, body): +def publish_github_release(slug, token, tag_name, body): github = github3.login(token=token) - repo = github.repository("pytest-dev", "pytest") + owner, repo = slug.split("/") + repo = github.repository(owner, repo) return repo.create_release(tag_name=tag_name, body=body) @@ -71,14 +72,22 @@ def main(argv): print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr) return 1 + slug = os.environ.get("TRAVIS_REPO_SLUG") + if not slug: + print("TRAVIS_REPO_SLUG not set", file=sys.stderr) + return 1 + rst_body = parse_changelog(tag_name) md_body = convert_rst_to_md(rst_body) - if not publish_github_release(token, tag_name, md_body): + if not publish_github_release(slug, token, tag_name, md_body): print("Could not publish release notes:", file=sys.stderr) print(md_body, file=sys.stderr) return 5 - print(f"Release notes for {tag_name} published successfully") + print() + print(f"Release notes for {tag_name} published successfully:") + print(f"https://github.com/{slug}/releases/tag/{tag_name}") + print() return 0 From 1049a38cee2d8642635733f2a9ea0e776c0929db Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 15 Aug 2019 10:05:42 -0300 Subject: [PATCH 69/70] Fix wording as suggested in review of #5741 --- doc/en/skipping.rst | 2 +- src/_pytest/outcomes.py | 2 +- src/_pytest/pytester.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 57f565472..2b654560e 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -180,7 +180,7 @@ Skipping on a missing import dependency ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ You can skip tests on a missing import by using :ref:`pytest.importorskip ref` -at module level or within a test or test setup function. +at module level, within a test, or test setup function. .. code-block:: python diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index 13eb0a295..947136625 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -161,7 +161,7 @@ def importorskip( current test if the module cannot be imported. :param str modname: the name of the module to import - :param str minversion: if given, the imported module ``__version__`` + :param str minversion: if given, the imported module's ``__version__`` attribute must be at least this minimal version, otherwise the test is still skipped. :param str reason: if given, this reason is shown as the message when the diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d9ceb0e78..94058f70c 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -632,7 +632,7 @@ class Testdir: def copy_example(self, name=None): """Copy file from project's directory into the testdir. - :param str name: The name of the file for copy. + :param str name: The name of the file to copy. :return: path to the copied directory (inside ``self.tmpdir``). """ From eaf7ce9a992cea7506553b7d77fdf9622900ae36 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 15 Aug 2019 20:00:09 -0400 Subject: [PATCH 70/70] Preparing release version 5.1.0 --- CHANGELOG.rst | 158 ++++++++++++++++++++++++++++ changelog/4344.bugfix.rst | 1 - changelog/5095.trivial.rst | 2 - changelog/5115.bugfix.rst | 1 - changelog/5180.removal.rst | 26 ----- changelog/5471.improvement.rst | 1 - changelog/5477.bugfix.rst | 1 - changelog/5516.trivial.rst | 1 - changelog/5523.bugfix.rst | 1 - changelog/5524.bugfix.rst | 2 - changelog/5537.bugfix.rst | 2 - changelog/5564.feature.rst | 1 - changelog/5565.removal.rst | 13 --- changelog/5576.feature.rst | 4 - changelog/5578.bugfix.rst | 3 - changelog/5603.trivial.rst | 1 - changelog/5606.bugfix.rst | 2 - changelog/5615.removal.rst | 7 -- changelog/5634.bugfix.rst | 2 - changelog/5650.bugfix.rst | 1 - changelog/5664.trivial.rst | 2 - changelog/5669.doc.rst | 1 - changelog/5684.trivial.rst | 1 - changelog/5701.bugfix.rst | 1 - changelog/5707.improvement.rst | 4 - changelog/5734.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-5.1.0.rst | 56 ++++++++++ doc/en/assert.rst | 6 +- doc/en/builtin.rst | 2 +- doc/en/cache.rst | 54 +++++----- doc/en/capture.rst | 4 +- doc/en/doctest.rst | 4 +- doc/en/example/markers.rst | 32 +++--- doc/en/example/nonpython.rst | 6 +- doc/en/example/parametrize.rst | 32 +++--- doc/en/example/pythoncollection.rst | 6 +- doc/en/example/reportingdemo.rst | 14 +-- doc/en/example/simple.rst | 30 +++--- doc/en/example/special.rst | 2 +- doc/en/fixture.rst | 40 +++---- doc/en/getting-started.rst | 14 +-- doc/en/index.rst | 2 +- doc/en/parametrize.rst | 10 +- doc/en/skipping.rst | 2 +- doc/en/tmpdir.rst | 6 +- doc/en/unittest.rst | 12 +-- doc/en/usage.rst | 6 +- doc/en/warnings.rst | 6 +- doc/en/writing_plugins.rst | 2 +- 50 files changed, 361 insertions(+), 228 deletions(-) delete mode 100644 changelog/4344.bugfix.rst delete mode 100644 changelog/5095.trivial.rst delete mode 100644 changelog/5115.bugfix.rst delete mode 100644 changelog/5180.removal.rst delete mode 100644 changelog/5471.improvement.rst delete mode 100644 changelog/5477.bugfix.rst delete mode 100644 changelog/5516.trivial.rst delete mode 100644 changelog/5523.bugfix.rst delete mode 100644 changelog/5524.bugfix.rst delete mode 100644 changelog/5537.bugfix.rst delete mode 100644 changelog/5564.feature.rst delete mode 100644 changelog/5565.removal.rst delete mode 100644 changelog/5576.feature.rst delete mode 100644 changelog/5578.bugfix.rst delete mode 100644 changelog/5603.trivial.rst delete mode 100644 changelog/5606.bugfix.rst delete mode 100644 changelog/5615.removal.rst delete mode 100644 changelog/5634.bugfix.rst delete mode 100644 changelog/5650.bugfix.rst delete mode 100644 changelog/5664.trivial.rst delete mode 100644 changelog/5669.doc.rst delete mode 100644 changelog/5684.trivial.rst delete mode 100644 changelog/5701.bugfix.rst delete mode 100644 changelog/5707.improvement.rst delete mode 100644 changelog/5734.bugfix.rst create mode 100644 doc/en/announce/release-5.1.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 390fe84ca..6c4479897 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,164 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.1.0 (2019-08-15) +========================= + +Removals +-------- + +- `#5180 `_: As per our policy, the following features have been deprecated in the 4.X series and are now + removed: + + * ``Request.getfuncargvalue``: use ``Request.getfixturevalue`` instead. + + * ``pytest.raises`` and ``pytest.warns`` no longer support strings as the second argument. + + * ``message`` parameter of ``pytest.raises``. + + * ``pytest.raises``, ``pytest.warns`` and ``ParameterSet.param`` now use native keyword-only + syntax. This might change the exception message from previous versions, but they still raise + ``TypeError`` on unknown keyword arguments as before. + + * ``pytest.config`` global variable. + + * ``tmpdir_factory.ensuretemp`` method. + + * ``pytest_logwarning`` hook. + + * ``RemovedInPytest4Warning`` warning type. + + * ``request`` is now a reserved name for fixtures. + + + For more information consult + `Deprecations and Removals `__ in the docs. + + +- `#5565 `_: Removed unused support code for `unittest2 `__. + + The ``unittest2`` backport module is no longer + necessary since Python 3.3+, and the small amount of code in pytest to support it also doesn't seem + to be used: after removed, all tests still pass unchanged. + + Although our policy is to introduce a deprecation period before removing any features or support + for third party libraries, because this code is apparently not used + at all (even if ``unittest2`` is used by a test suite executed by pytest), it was decided to + remove it in this release. + + If you experience a regression because of this, please + `file an issue `__. + + +- `#5615 `_: ``pytest.fail``, ``pytest.xfail`` and ``pytest.skip`` no longer support bytes for the message argument. + + This was supported for Python 2 where it was tempting to use ``"message"`` + instead of ``u"message"``. + + Python 3 code is unlikely to pass ``bytes`` to these functions. If you do, + please decode it to an ``str`` beforehand. + + + +Features +-------- + +- `#5564 `_: New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. + + +- `#5576 `_: New `NUMBER `__ + option for doctests to ignore irrelevant differences in floating-point numbers. + Inspired by Sébastien Boisgérault's `numtest `__ + extension for doctest. + + + +Improvements +------------ + +- `#5471 `_: JUnit XML now includes a timestamp and hostname in the testsuite tag. + + +- `#5707 `_: Time taken to run the test suite now includes a human-readable representation when it takes over + 60 seconds, for example:: + + ===== 2 failed in 102.70s (0:01:42) ===== + + + +Bug Fixes +--------- + +- `#4344 `_: Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only. + + +- `#5115 `_: Warnings issued during ``pytest_configure`` are explicitly not treated as errors, even if configured as such, because it otherwise completely breaks pytest. + + +- `#5477 `_: The XML file produced by ``--junitxml`` now correctly contain a ```` root element. + + +- `#5523 `_: Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+. + + +- `#5524 `_: Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only, + which could lead to pytest crashing when executed a second time with the ``--basetemp`` option. + + +- `#5537 `_: Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the + standard library on Python 3.8+. + + +- `#5578 `_: Improve type checking for some exception-raising functions (``pytest.xfail``, ``pytest.skip``, etc) + so they provide better error messages when users meant to use marks (for example ``@pytest.xfail`` + instead of ``@pytest.mark.xfail``). + + +- `#5606 `_: Fixed internal error when test functions were patched with objects that cannot be compared + for truth values against others, like ``numpy`` arrays. + + +- `#5634 `_: ``pytest.exit`` is now correctly handled in ``unittest`` cases. + This makes ``unittest`` cases handle ``quit`` from pytest's pdb correctly. + + +- `#5650 `_: Improved output when parsing an ini configuration file fails. + + +- `#5701 `_: Fix collection of ``staticmethod`` objects defined with ``functools.partial``. + + +- `#5734 `_: Skip async generator test functions, and update the warning message to refer to ``async def`` functions. + + + +Improved Documentation +---------------------- + +- `#5669 `_: Add docstring for ``Testdir.copy_example``. + + + +Trivial/Internal Changes +------------------------ + +- `#5095 `_: XML files of the ``xunit2`` family are now validated against the schema by pytest's own test suite + to avoid future regressions. + + +- `#5516 `_: Cache node splitting function which can improve collection performance in very large test suites. + + +- `#5603 `_: Simplified internal ``SafeRepr`` class and removed some dead code. + + +- `#5664 `_: When invoking pytest's own testsuite with ``PYTHONDONTWRITEBYTECODE=1``, + the ``test_xfail_handling`` test no longer fails. + + +- `#5684 `_: Replace manual handling of ``OSError.errno`` in the codebase by new ``OSError`` subclasses (``PermissionError``, ``FileNotFoundError``, etc.). + + pytest 5.0.1 (2019-07-04) ========================= diff --git a/changelog/4344.bugfix.rst b/changelog/4344.bugfix.rst deleted file mode 100644 index 644a6f030..000000000 --- a/changelog/4344.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix RuntimeError/StopIteration when trying to collect package with "__init__.py" only. diff --git a/changelog/5095.trivial.rst b/changelog/5095.trivial.rst deleted file mode 100644 index 2256cf9f4..000000000 --- a/changelog/5095.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -XML files of the ``xunit2`` family are now validated against the schema by pytest's own test suite -to avoid future regressions. diff --git a/changelog/5115.bugfix.rst b/changelog/5115.bugfix.rst deleted file mode 100644 index af75499a3..000000000 --- a/changelog/5115.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Warnings issued during ``pytest_configure`` are explicitly not treated as errors, even if configured as such, because it otherwise completely breaks pytest. diff --git a/changelog/5180.removal.rst b/changelog/5180.removal.rst deleted file mode 100644 index 1174a7cba..000000000 --- a/changelog/5180.removal.rst +++ /dev/null @@ -1,26 +0,0 @@ -As per our policy, the following features have been deprecated in the 4.X series and are now -removed: - -* ``Request.getfuncargvalue``: use ``Request.getfixturevalue`` instead. - -* ``pytest.raises`` and ``pytest.warns`` no longer support strings as the second argument. - -* ``message`` parameter of ``pytest.raises``. - -* ``pytest.raises``, ``pytest.warns`` and ``ParameterSet.param`` now use native keyword-only - syntax. This might change the exception message from previous versions, but they still raise - ``TypeError`` on unknown keyword arguments as before. - -* ``pytest.config`` global variable. - -* ``tmpdir_factory.ensuretemp`` method. - -* ``pytest_logwarning`` hook. - -* ``RemovedInPytest4Warning`` warning type. - -* ``request`` is now a reserved name for fixtures. - - -For more information consult -`Deprecations and Removals `__ in the docs. diff --git a/changelog/5471.improvement.rst b/changelog/5471.improvement.rst deleted file mode 100644 index 154b64ea7..000000000 --- a/changelog/5471.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -JUnit XML now includes a timestamp and hostname in the testsuite tag. diff --git a/changelog/5477.bugfix.rst b/changelog/5477.bugfix.rst deleted file mode 100644 index c9c9386e9..000000000 --- a/changelog/5477.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -The XML file produced by ``--junitxml`` now correctly contain a ```` root element. diff --git a/changelog/5516.trivial.rst b/changelog/5516.trivial.rst deleted file mode 100644 index 2f6b4e35e..000000000 --- a/changelog/5516.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Cache node splitting function which can improve collection performance in very large test suites. diff --git a/changelog/5523.bugfix.rst b/changelog/5523.bugfix.rst deleted file mode 100644 index 5155b92b1..000000000 --- a/changelog/5523.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed using multiple short options together in the command-line (for example ``-vs``) in Python 3.8+. diff --git a/changelog/5524.bugfix.rst b/changelog/5524.bugfix.rst deleted file mode 100644 index 96ebbd43e..000000000 --- a/changelog/5524.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix issue where ``tmp_path`` and ``tmpdir`` would not remove directories containing files marked as read-only, -which could lead to pytest crashing when executed a second time with the ``--basetemp`` option. diff --git a/changelog/5537.bugfix.rst b/changelog/5537.bugfix.rst deleted file mode 100644 index 0263e8cdf..000000000 --- a/changelog/5537.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the -standard library on Python 3.8+. diff --git a/changelog/5564.feature.rst b/changelog/5564.feature.rst deleted file mode 100644 index e2f365a33..000000000 --- a/changelog/5564.feature.rst +++ /dev/null @@ -1 +0,0 @@ -New ``Config.invocation_args`` attribute containing the unchanged arguments passed to ``pytest.main()``. diff --git a/changelog/5565.removal.rst b/changelog/5565.removal.rst deleted file mode 100644 index 33de2b4e7..000000000 --- a/changelog/5565.removal.rst +++ /dev/null @@ -1,13 +0,0 @@ -Removed unused support code for `unittest2 `__. - -The ``unittest2`` backport module is no longer -necessary since Python 3.3+, and the small amount of code in pytest to support it also doesn't seem -to be used: after removed, all tests still pass unchanged. - -Although our policy is to introduce a deprecation period before removing any features or support -for third party libraries, because this code is apparently not used -at all (even if ``unittest2`` is used by a test suite executed by pytest), it was decided to -remove it in this release. - -If you experience a regression because of this, please -`file an issue `__. diff --git a/changelog/5576.feature.rst b/changelog/5576.feature.rst deleted file mode 100644 index 267a28292..000000000 --- a/changelog/5576.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -New `NUMBER `__ -option for doctests to ignore irrelevant differences in floating-point numbers. -Inspired by Sébastien Boisgérault's `numtest `__ -extension for doctest. diff --git a/changelog/5578.bugfix.rst b/changelog/5578.bugfix.rst deleted file mode 100644 index 5f6c39185..000000000 --- a/changelog/5578.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Improve type checking for some exception-raising functions (``pytest.xfail``, ``pytest.skip``, etc) -so they provide better error messages when users meant to use marks (for example ``@pytest.xfail`` -instead of ``@pytest.mark.xfail``). diff --git a/changelog/5603.trivial.rst b/changelog/5603.trivial.rst deleted file mode 100644 index 310e88562..000000000 --- a/changelog/5603.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Simplified internal ``SafeRepr`` class and removed some dead code. diff --git a/changelog/5606.bugfix.rst b/changelog/5606.bugfix.rst deleted file mode 100644 index 82332ba99..000000000 --- a/changelog/5606.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fixed internal error when test functions were patched with objects that cannot be compared -for truth values against others, like ``numpy`` arrays. diff --git a/changelog/5615.removal.rst b/changelog/5615.removal.rst deleted file mode 100644 index 6dd9aec1d..000000000 --- a/changelog/5615.removal.rst +++ /dev/null @@ -1,7 +0,0 @@ -``pytest.fail``, ``pytest.xfail`` and ``pytest.skip`` no longer support bytes for the message argument. - -This was supported for Python 2 where it was tempting to use ``"message"`` -instead of ``u"message"``. - -Python 3 code is unlikely to pass ``bytes`` to these functions. If you do, -please decode it to an ``str`` beforehand. diff --git a/changelog/5634.bugfix.rst b/changelog/5634.bugfix.rst deleted file mode 100644 index a2a282f93..000000000 --- a/changelog/5634.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -``pytest.exit`` is now correctly handled in ``unittest`` cases. -This makes ``unittest`` cases handle ``quit`` from pytest's pdb correctly. diff --git a/changelog/5650.bugfix.rst b/changelog/5650.bugfix.rst deleted file mode 100644 index db57a40b9..000000000 --- a/changelog/5650.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Improved output when parsing an ini configuration file fails. diff --git a/changelog/5664.trivial.rst b/changelog/5664.trivial.rst deleted file mode 100644 index 3928454ef..000000000 --- a/changelog/5664.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -When invoking pytest's own testsuite with ``PYTHONDONTWRITEBYTECODE=1``, -the ``test_xfail_handling`` test no longer fails. diff --git a/changelog/5669.doc.rst b/changelog/5669.doc.rst deleted file mode 100644 index 0ec9626ae..000000000 --- a/changelog/5669.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add docstring for ``Testdir.copy_example``. diff --git a/changelog/5684.trivial.rst b/changelog/5684.trivial.rst deleted file mode 100644 index 393fa3205..000000000 --- a/changelog/5684.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Replace manual handling of ``OSError.errno`` in the codebase by new ``OSError`` subclasses (``PermissionError``, ``FileNotFoundError``, etc.). diff --git a/changelog/5701.bugfix.rst b/changelog/5701.bugfix.rst deleted file mode 100644 index b654e7447..000000000 --- a/changelog/5701.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix collection of ``staticmethod`` objects defined with ``functools.partial``. diff --git a/changelog/5707.improvement.rst b/changelog/5707.improvement.rst deleted file mode 100644 index 59176e1bb..000000000 --- a/changelog/5707.improvement.rst +++ /dev/null @@ -1,4 +0,0 @@ -Time taken to run the test suite now includes a human-readable representation when it takes over -60 seconds, for example:: - - ===== 2 failed in 102.70s (0:01:42) ===== diff --git a/changelog/5734.bugfix.rst b/changelog/5734.bugfix.rst deleted file mode 100644 index dc20e6b52..000000000 --- a/changelog/5734.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Skip async generator test functions, and update the warning message to refer to ``async def`` functions. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index d372c92fa..7c6220b24 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.1.0 release-5.0.1 release-5.0.0 release-4.6.5 diff --git a/doc/en/announce/release-5.1.0.rst b/doc/en/announce/release-5.1.0.rst new file mode 100644 index 000000000..73e956d77 --- /dev/null +++ b/doc/en/announce/release-5.1.0.rst @@ -0,0 +1,56 @@ +pytest-5.1.0 +======================================= + +The pytest team is proud to announce the 5.1.0 release! + +pytest is a mature Python testing tool with more than a 2000 tests +against itself, passing on many different interpreters and platforms. + +This release contains a number of bugs fixes and improvements, so users are encouraged +to take a look at the CHANGELOG: + + https://docs.pytest.org/en/latest/changelog.html + +For complete documentation, please visit: + + https://docs.pytest.org/en/latest/ + +As usual, you can upgrade from pypi via: + + pip install -U pytest + +Thanks to all who contributed to this release, among them: + +* Albert Tugushev +* Alexey Zankevich +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* David Röthlisberger +* Florian Bruhin +* Ilya Stepin +* Jon Dufresne +* Kaiqi +* Max R +* Miro Hrončok +* Oliver Bestwalter +* Ran Benita +* Ronny Pfannschmidt +* Samuel Searles-Bryant +* Semen Zhydenko +* Steffen Schroeder +* Thomas Grainger +* Tim Hoffmann +* William Woodall +* Wojtek Erbetowski +* Xixi Zhao +* Yash Todi +* boris +* dmitry.dygalo +* helloocc +* martbln +* mei-li + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/assert.rst b/doc/en/assert.rst index bc7e75256..bd6e9b3b3 100644 --- a/doc/en/assert.rst +++ b/doc/en/assert.rst @@ -47,7 +47,7 @@ you will see the return value of the function call: E + where 3 = f() test_assert1.py:6: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + ============================ 1 failed in 0.05s ============================= ``pytest`` has support for showing the values of the most common subexpressions including calls, attributes, comparisons, and binary and unary @@ -208,7 +208,7 @@ if you run this module: E Use -v to get the full diff test_assert2.py:6: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + ============================ 1 failed in 0.05s ============================= Special comparisons are done for a number of cases: @@ -279,7 +279,7 @@ the conftest file: E vals: 1 != 2 test_foocompare.py:12: AssertionError - 1 failed in 0.12 seconds + 1 failed in 0.05s .. _assert-details: .. _`assert introspection`: diff --git a/doc/en/builtin.rst b/doc/en/builtin.rst index 4309a16ea..4638cf784 100644 --- a/doc/en/builtin.rst +++ b/doc/en/builtin.rst @@ -160,7 +160,7 @@ For information about fixtures, see :ref:`fixtures`. To see a complete list of a in python < 3.6 this is a pathlib2.Path - no tests ran in 0.12 seconds + no tests ran in 0.01s You can also interactively ask for help, e.g. by typing on the Python interactive prompt something like: diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 84b3fa009..c6b3e3c47 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -60,10 +60,10 @@ If you run this for the first time you will see two failures: @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed + test_50.py:7: Failed _______________________________ test_num[25] _______________________________ i = 25 @@ -71,11 +71,11 @@ If you run this for the first time you will see two failures: @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed - 2 failed, 48 passed in 0.12 seconds + test_50.py:7: Failed + 2 failed, 48 passed in 0.16s If you then run it with ``--lf``: @@ -99,10 +99,10 @@ If you then run it with ``--lf``: @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed + test_50.py:7: Failed _______________________________ test_num[25] _______________________________ i = 25 @@ -110,11 +110,11 @@ If you then run it with ``--lf``: @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed - ================= 2 failed, 48 deselected in 0.12 seconds ================== + test_50.py:7: Failed + ===================== 2 failed, 48 deselected in 0.07s ===================== You have run only the two failing tests from the last run, while the 48 passing tests have not been run ("deselected"). @@ -143,10 +143,10 @@ of ``FF`` and dots): @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed + test_50.py:7: Failed _______________________________ test_num[25] _______________________________ i = 25 @@ -154,11 +154,11 @@ of ``FF`` and dots): @pytest.mark.parametrize("i", range(50)) def test_num(i): if i in (17, 25): - > pytest.fail("bad luck") - E Failed: bad luck + > pytest.fail("bad luck") + E Failed: bad luck - test_50.py:6: Failed - =================== 2 failed, 48 passed in 0.12 seconds ==================== + test_50.py:7: Failed + ======================= 2 failed, 48 passed in 0.15s ======================= .. _`config.cache`: @@ -227,10 +227,10 @@ If you run this command for the first time, you can see the print statement: > assert mydata == 23 E assert 42 == 23 - test_caching.py:17: AssertionError + test_caching.py:20: AssertionError -------------------------- Captured stdout setup --------------------------- running expensive computation... - 1 failed in 0.12 seconds + 1 failed in 0.05s If you run it a second time, the value will be retrieved from the cache and nothing will be printed: @@ -248,8 +248,8 @@ the cache and nothing will be printed: > assert mydata == 23 E assert 42 == 23 - test_caching.py:17: AssertionError - 1 failed in 0.12 seconds + test_caching.py:20: AssertionError + 1 failed in 0.05s See the :ref:`cache-api` for more details. @@ -283,7 +283,7 @@ You can always peek at the content of the cache using the example/value contains: 42 - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.00s =========================== ``--cache-show`` takes an optional argument to specify a glob pattern for filtering: @@ -300,7 +300,7 @@ filtering: example/value contains: 42 - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== Clearing Cache content ---------------------- diff --git a/doc/en/capture.rst b/doc/en/capture.rst index 55714c25b..72bdea983 100644 --- a/doc/en/capture.rst +++ b/doc/en/capture.rst @@ -88,10 +88,10 @@ of the failing function and hide the other one: > assert False E assert False - test_module.py:9: AssertionError + test_module.py:12: AssertionError -------------------------- Captured stdout setup --------------------------- setting up - ==================== 1 failed, 1 passed in 0.12 seconds ==================== + ======================= 1 failed, 1 passed in 0.05s ======================== Accessing captured output from a test function --------------------------------------------------- diff --git a/doc/en/doctest.rst b/doc/en/doctest.rst index 2020b30b3..2718e1e63 100644 --- a/doc/en/doctest.rst +++ b/doc/en/doctest.rst @@ -36,7 +36,7 @@ then you can just invoke ``pytest`` directly: test_example.txt . [100%] - ========================= 1 passed in 0.12 seconds ========================= + ============================ 1 passed in 0.02s ============================= By default, pytest will collect ``test*.txt`` files looking for doctest directives, but you can pass additional globs using the ``--doctest-glob`` option (multi-allowed). @@ -66,7 +66,7 @@ and functions, including from test modules: mymodule.py . [ 50%] test_example.txt . [100%] - ========================= 2 passed in 0.12 seconds ========================= + ============================ 2 passed in 0.03s ============================= You can make these changes permanent in your project by putting them into a pytest.ini file like this: diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 909f23a2e..38d02ed0c 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -52,7 +52,7 @@ You can then restrict a test run to only run tests marked with ``webtest``: test_server.py::test_send_http PASSED [100%] - ================== 1 passed, 3 deselected in 0.12 seconds ================== + ===================== 1 passed, 3 deselected in 0.01s ====================== Or the inverse, running all tests except the webtest ones: @@ -69,7 +69,7 @@ Or the inverse, running all tests except the webtest ones: test_server.py::test_another PASSED [ 66%] test_server.py::TestClass::test_method PASSED [100%] - ================== 3 passed, 1 deselected in 0.12 seconds ================== + ===================== 3 passed, 1 deselected in 0.02s ====================== Selecting tests based on their node ID -------------------------------------- @@ -89,7 +89,7 @@ tests based on their module, class, method, or function name: test_server.py::TestClass::test_method PASSED [100%] - ========================= 1 passed in 0.12 seconds ========================= + ============================ 1 passed in 0.01s ============================= You can also select on the class: @@ -104,7 +104,7 @@ You can also select on the class: test_server.py::TestClass::test_method PASSED [100%] - ========================= 1 passed in 0.12 seconds ========================= + ============================ 1 passed in 0.01s ============================= Or select multiple nodes: @@ -120,7 +120,7 @@ Or select multiple nodes: test_server.py::TestClass::test_method PASSED [ 50%] test_server.py::test_send_http PASSED [100%] - ========================= 2 passed in 0.12 seconds ========================= + ============================ 2 passed in 0.02s ============================= .. _node-id: @@ -159,7 +159,7 @@ select tests based on their names: test_server.py::test_send_http PASSED [100%] - ================== 1 passed, 3 deselected in 0.12 seconds ================== + ===================== 1 passed, 3 deselected in 0.01s ====================== And you can also run all tests except the ones that match the keyword: @@ -176,7 +176,7 @@ And you can also run all tests except the ones that match the keyword: test_server.py::test_another PASSED [ 66%] test_server.py::TestClass::test_method PASSED [100%] - ================== 3 passed, 1 deselected in 0.12 seconds ================== + ===================== 3 passed, 1 deselected in 0.02s ====================== Or to select "http" and "quick" tests: @@ -192,7 +192,7 @@ Or to select "http" and "quick" tests: test_server.py::test_send_http PASSED [ 50%] test_server.py::test_something_quick PASSED [100%] - ================== 2 passed, 2 deselected in 0.12 seconds ================== + ===================== 2 passed, 2 deselected in 0.02s ====================== .. note:: @@ -413,7 +413,7 @@ the test needs: test_someenv.py s [100%] - ======================== 1 skipped in 0.12 seconds ========================= + ============================ 1 skipped in 0.01s ============================ and here is one that specifies exactly the environment needed: @@ -428,7 +428,7 @@ and here is one that specifies exactly the environment needed: test_someenv.py . [100%] - ========================= 1 passed in 0.12 seconds ========================= + ============================ 1 passed in 0.01s ============================= The ``--markers`` option always gives you a list of available markers: @@ -499,7 +499,7 @@ The output is as follows: $ pytest -q -s Mark(name='my_marker', args=(,), kwargs={}) . - 1 passed in 0.12 seconds + 1 passed in 0.01s We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``. @@ -551,7 +551,7 @@ Let's run this without capturing output and see what we get: glob args=('class',) kwargs={'x': 2} glob args=('module',) kwargs={'x': 1} . - 1 passed in 0.12 seconds + 1 passed in 0.01s marking platform specific tests with pytest -------------------------------------------------------------- @@ -623,7 +623,7 @@ then you will see two tests skipped and two executed tests as expected: ========================= short test summary info ========================== SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux - =================== 2 passed, 2 skipped in 0.12 seconds ==================== + ======================= 2 passed, 2 skipped in 0.02s ======================= Note that if you specify a platform via the marker-command line option like this: @@ -638,7 +638,7 @@ Note that if you specify a platform via the marker-command line option like this test_plat.py . [100%] - ================== 1 passed, 3 deselected in 0.12 seconds ================== + ===================== 1 passed, 3 deselected in 0.01s ====================== then the unmarked-tests will not be run. It is thus a way to restrict the run to the specific tests. @@ -711,7 +711,7 @@ We can now use the ``-m option`` to select one set: test_module.py:8: in test_interface_complex assert 0 E assert 0 - ================== 2 failed, 2 deselected in 0.12 seconds ================== + ===================== 2 failed, 2 deselected in 0.07s ====================== or to select both "event" and "interface" tests: @@ -739,4 +739,4 @@ or to select both "event" and "interface" tests: test_module.py:12: in test_event_simple assert 0 E assert 0 - ================== 3 failed, 1 deselected in 0.12 seconds ================== + ===================== 3 failed, 1 deselected in 0.07s ====================== diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index 75dc764e9..9c9e462f6 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -41,7 +41,7 @@ now execute the test specification: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.12 seconds ==================== + ======================= 1 failed, 1 passed in 0.06s ======================== .. regendoc:wipe @@ -77,7 +77,7 @@ consulted when reporting in ``verbose`` mode: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.12 seconds ==================== + ======================= 1 failed, 1 passed in 0.07s ======================== .. regendoc:wipe @@ -97,4 +97,4 @@ interesting to just look at the collection tree: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.05s =========================== diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 387e3f9de..08b414880 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -54,7 +54,7 @@ This means that we only run 2 tests if we do not pass ``--all``: $ pytest -q test_compute.py .. [100%] - 2 passed in 0.12 seconds + 2 passed in 0.01s We run only two computations, so we see two dots. let's run the full monty: @@ -72,8 +72,8 @@ let's run the full monty: > assert param1 < 4 E assert 4 < 4 - test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.12 seconds + test_compute.py:4: AssertionError + 1 failed, 4 passed in 0.06s As expected when running the full range of ``param1`` values we'll get an error on the last one. @@ -172,7 +172,7 @@ objects, they are still using the default pytest representation: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.02s =========================== In ``test_timedistance_v3``, we used ``pytest.param`` to specify the test IDs together with the actual data, instead of listing them separately. @@ -229,7 +229,7 @@ this is a fully self-contained example which you can run with: test_scenarios.py .... [100%] - ========================= 4 passed in 0.12 seconds ========================= + ============================ 4 passed in 0.02s ============================= If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function: @@ -248,7 +248,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.02s =========================== Note that we told ``metafunc.parametrize()`` that your scenario values should be considered class-scoped. With pytest-2.3 this leads to a @@ -323,7 +323,7 @@ Let's first see how it looks like at collection time: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== And then when we run the test: @@ -342,8 +342,8 @@ And then when we run the test: > pytest.fail("deliberately failing for demo purposes") E Failed: deliberately failing for demo purposes - test_backends.py:6: Failed - 1 failed, 1 passed in 0.12 seconds + test_backends.py:8: Failed + 1 failed, 1 passed in 0.05s The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``db`` fixture function 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. @@ -394,7 +394,7 @@ The result of this test will be successful: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== .. regendoc:wipe @@ -453,8 +453,8 @@ argument sets to use for each test function. Let's run it: > assert a == b E assert 1 == 2 - test_parametrize.py:18: AssertionError - 1 failed, 2 passed in 0.12 seconds + test_parametrize.py:21: AssertionError + 1 failed, 2 passed in 0.07s Indirect parametrization with multiple fixtures -------------------------------------------------------------- @@ -479,7 +479,7 @@ Running it results in some skips if we don't have all the python interpreters in ========================= short test summary info ========================== SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.7' not found - 3 passed, 24 skipped in 0.12 seconds + 3 passed, 24 skipped in 0.43s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -547,8 +547,8 @@ If you run this with reporting for skips enabled: test_module.py .s [100%] ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2': No module named 'opt2' - =================== 1 passed, 1 skipped in 0.12 seconds ==================== + SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:13: could not import 'opt2': No module named 'opt2' + ======================= 1 passed, 1 skipped in 0.02s ======================= You'll see that we don't have an ``opt2`` module and thus the second test run of our ``test_func1`` was skipped. A few notes: @@ -610,7 +610,7 @@ Then run ``pytest`` with verbose mode and with only the ``basic`` marker: test_pytest_param_example.py::test_eval[basic_2+4] PASSED [ 66%] test_pytest_param_example.py::test_eval[basic_6*9] XFAIL [100%] - ============ 2 passed, 15 deselected, 1 xfailed in 0.12 seconds ============ + =============== 2 passed, 15 deselected, 1 xfailed in 0.23s ================ As the result: diff --git a/doc/en/example/pythoncollection.rst b/doc/en/example/pythoncollection.rst index cd4e34352..95faae34b 100644 --- a/doc/en/example/pythoncollection.rst +++ b/doc/en/example/pythoncollection.rst @@ -158,7 +158,7 @@ The test collection would look like this: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== You can check for multiple glob patterns by adding a space between the patterns: @@ -221,7 +221,7 @@ You can always peek at the collection tree without running tests like this: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== .. _customizing-test-collection: @@ -297,7 +297,7 @@ file will be left out: rootdir: $REGENDOC_TMPDIR, inifile: pytest.ini collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.04s =========================== It's also possible to ignore files based on Unix shell-style wildcards by adding patterns to ``collect_ignore_glob``. diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 05d06ecb6..1ad7c6966 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -119,7 +119,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: a = "1" * 100 + "a" + "2" * 100 b = "1" * 100 + "b" + "2" * 100 > assert a == b - E AssertionError: assert '111111111111...2222222222222' == '1111111111111...2222222222222' + E AssertionError: assert '111111111111...2222222222222' == '111111111111...2222222222222' E Skipping 90 identical leading characters in diff, use -v to show E Skipping 91 identical trailing characters in diff, use -v to show E - 1111111111a222222222 @@ -136,7 +136,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: a = "1\n" * 100 + "a" + "2\n" * 100 b = "1\n" * 100 + "b" + "2\n" * 100 > assert a == b - E AssertionError: assert '1\n1\n1\n1\n...n2\n2\n2\n2\n' == '1\n1\n1\n1\n1...n2\n2\n2\n2\n' + E AssertionError: assert '1\n1\n1\n1\n...n2\n2\n2\n2\n' == '1\n1\n1\n1\n...n2\n2\n2\n2\n' E Skipping 190 identical leading characters in diff, use -v to show E Skipping 191 identical trailing characters in diff, use -v to show E 1 @@ -235,7 +235,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_not_in_text_multiline(self): text = "some multiline\ntext\nwhich\nincludes foo\nand a\ntail" > assert "foo" not in text - E AssertionError: assert 'foo' not in 'some multiline\ntext\nw...ncludes foo\nand a\ntail' + E AssertionError: assert 'foo' not in 'some multil...nand a\ntail' E 'foo' is contained here: E some multiline E text @@ -267,7 +267,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_not_in_text_single_long(self): text = "head " * 50 + "foo " + "tail " * 20 > assert "foo" not in text - E AssertionError: assert 'foo' not in 'head head head head hea...ail tail tail tail tail ' + E AssertionError: assert 'foo' not in 'head head h...l tail tail ' E 'foo' is contained here: E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ @@ -280,7 +280,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_not_in_text_single_long_term(self): text = "head " * 50 + "f" * 70 + "tail " * 20 > assert "f" * 70 not in text - E AssertionError: assert 'fffffffffff...ffffffffffff' not in 'head head he...l tail tail ' + E AssertionError: assert 'fffffffffff...ffffffffffff' not in 'head head h...l tail tail ' E 'ffffffffffffffffff...fffffffffffffffffff' is contained here: E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -301,7 +301,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: left = Foo(1, "b") right = Foo(1, "c") > assert left == right - E AssertionError: assert TestSpecialis...oo(a=1, b='b') == TestSpecialise...oo(a=1, b='c') + E AssertionError: assert TestSpecialis...oo(a=1, b='b') == TestSpecialis...oo(a=1, b='c') E Omitting 1 identical items, use -vv to show E Differing attributes: E b: 'b' != 'c' @@ -650,4 +650,4 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a failure_demo.py:282: AssertionError - ======================== 44 failed in 0.12 seconds ========================= + ============================ 44 failed in 0.82s ============================ diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index b4baa2b9b..ce2fbff54 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -65,7 +65,7 @@ Let's run this without supplying our new option: test_sample.py:6: AssertionError --------------------------- Captured stdout call --------------------------- first - 1 failed in 0.12 seconds + 1 failed in 0.06s And now with supplying a command line option: @@ -89,7 +89,7 @@ And now with supplying a command line option: test_sample.py:6: AssertionError --------------------------- Captured stdout call --------------------------- second - 1 failed in 0.12 seconds + 1 failed in 0.06s You can see that the command line option arrived in our test. This completes the basic pattern. However, one often rather wants to process @@ -132,7 +132,7 @@ directory with the above conftest.py: rootdir: $REGENDOC_TMPDIR collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.01s =========================== .. _`excontrolskip`: @@ -201,7 +201,7 @@ and when running it will see a skipped "slow" test: ========================= short test summary info ========================== SKIPPED [1] test_module.py:8: need --runslow option to run - =================== 1 passed, 1 skipped in 0.12 seconds ==================== + ======================= 1 passed, 1 skipped in 0.01s ======================= Or run it including the ``slow`` marked test: @@ -216,7 +216,7 @@ Or run it including the ``slow`` marked test: test_module.py .. [100%] - ========================= 2 passed in 0.12 seconds ========================= + ============================ 2 passed in 0.01s ============================= Writing well integrated assertion helpers -------------------------------------------------- @@ -261,7 +261,7 @@ Let's run our little function: E Failed: not configured: 42 test_checkconfig.py:11: Failed - 1 failed in 0.12 seconds + 1 failed in 0.05s If you only want to hide certain exceptions, you can set ``__tracebackhide__`` to a callable which gets the ``ExceptionInfo`` object. You can for example use @@ -358,7 +358,7 @@ which will add the string to the test header accordingly: rootdir: $REGENDOC_TMPDIR collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.00s =========================== .. regendoc:wipe @@ -388,7 +388,7 @@ which will add info only when run with "--v": rootdir: $REGENDOC_TMPDIR collecting ... collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.00s =========================== and nothing when run plainly: @@ -401,7 +401,7 @@ and nothing when run plainly: rootdir: $REGENDOC_TMPDIR collected 0 items - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.00s =========================== profiling test duration -------------------------- @@ -445,9 +445,9 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 - 0.20s call test_some_are_slow.py::test_funcslow1 + 0.25s call test_some_are_slow.py::test_funcslow1 0.10s call test_some_are_slow.py::test_funcfast - ========================= 3 passed in 0.12 seconds ========================= + ============================ 3 passed in 0.68s ============================= incremental testing - test steps --------------------------------------------------- @@ -531,7 +531,7 @@ If we run this: ========================= short test summary info ========================== XFAIL test_step.py::TestUserHandling::test_deletion reason: previous test failed (test_modification) - ============== 1 failed, 2 passed, 1 xfailed in 0.12 seconds =============== + ================== 1 failed, 2 passed, 1 xfailed in 0.07s ================== We'll see that ``test_deletion`` was not executed because ``test_modification`` failed. It is reported as an "expected failure". @@ -644,7 +644,7 @@ We can run this: E assert 0 a/test_db2.py:2: AssertionError - ========== 3 failed, 2 passed, 1 xfailed, 1 error in 0.12 seconds ========== + ============= 3 failed, 2 passed, 1 xfailed, 1 error in 0.10s ============== The two test modules in the ``a`` directory see the same ``db`` fixture instance while the one test in the sister-directory ``b`` doesn't see it. We could of course @@ -733,7 +733,7 @@ and run them: E assert 0 test_module.py:6: AssertionError - ========================= 2 failed in 0.12 seconds ========================= + ============================ 2 failed in 0.07s ============================= you will have a "failures" file which contains the failing test ids: @@ -848,7 +848,7 @@ and run it: E assert 0 test_module.py:19: AssertionError - ==================== 2 failed, 1 error in 0.12 seconds ===================== + ======================== 2 failed, 1 error in 0.07s ======================== You'll see that the fixture finalizers could use the precise reporting information. diff --git a/doc/en/example/special.rst b/doc/en/example/special.rst index 5161c43ab..5142d08b9 100644 --- a/doc/en/example/special.rst +++ b/doc/en/example/special.rst @@ -81,4 +81,4 @@ If you run this without output capturing: .test other .test_unit1 method called . - 4 passed in 0.12 seconds + 4 passed in 0.02s diff --git a/doc/en/fixture.rst b/doc/en/fixture.rst index b494ec0fe..b8469ad46 100644 --- a/doc/en/fixture.rst +++ b/doc/en/fixture.rst @@ -92,11 +92,11 @@ marked ``smtp_connection`` fixture function. Running the test looks like this: def test_ehlo(smtp_connection): response, msg = smtp_connection.ehlo() assert response == 250 - > assert 0 # for demo purposes + > assert 0 # for demo purposes E assert 0 - test_smtpsimple.py:11: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + test_smtpsimple.py:14: AssertionError + ============================ 1 failed in 0.57s ============================= In the failure traceback we see that the test function was called with a ``smtp_connection`` argument, the ``smtplib.SMTP()`` instance created by the fixture @@ -246,7 +246,7 @@ inspect what is going on and can now run the tests: > assert 0 # for demo purposes E assert 0 - test_module.py:6: AssertionError + test_module.py:7: AssertionError ________________________________ test_noop _________________________________ smtp_connection = @@ -257,8 +257,8 @@ inspect what is going on and can now run the tests: > assert 0 # for demo purposes E assert 0 - test_module.py:11: AssertionError - ========================= 2 failed in 0.12 seconds ========================= + test_module.py:13: AssertionError + ============================ 2 failed in 0.76s ============================= You see the two ``assert 0`` failing and more importantly you can also see that the same (module-scoped) ``smtp_connection`` object was passed into the @@ -361,7 +361,7 @@ Let's execute it: $ pytest -s -q --tb=no FFteardown smtp - 2 failed in 0.12 seconds + 2 failed in 0.76s We see that the ``smtp_connection`` instance is finalized after the two tests finished execution. Note that if we decorated our fixture @@ -515,7 +515,7 @@ again, nothing much has changed: $ pytest -s -q --tb=no FFfinalizing (smtp.gmail.com) - 2 failed in 0.12 seconds + 2 failed in 0.76s Let's quickly create another test module that actually sets the server URL in its module namespace: @@ -538,7 +538,7 @@ Running it: F [100%] ================================= FAILURES ================================= ______________________________ test_showhelo _______________________________ - test_anothersmtp.py:5: in test_showhelo + test_anothersmtp.py:6: in test_showhelo assert 0, smtp_connection.helo() E AssertionError: (250, b'mail.python.org') E assert 0 @@ -654,7 +654,7 @@ So let's just do another run: > assert 0 # for demo purposes E assert 0 - test_module.py:6: AssertionError + test_module.py:7: AssertionError ________________________ test_noop[smtp.gmail.com] _________________________ smtp_connection = @@ -665,7 +665,7 @@ So let's just do another run: > assert 0 # for demo purposes E assert 0 - test_module.py:11: AssertionError + test_module.py:13: AssertionError ________________________ test_ehlo[mail.python.org] ________________________ smtp_connection = @@ -676,7 +676,7 @@ So let's just do another run: > assert b"smtp.gmail.com" in msg E AssertionError: assert b'smtp.gmail.com' in b'mail.python.org\nPIPELINING\nSIZE 51200000\nETRN\nSTARTTLS\nAUTH DIGEST-MD5 NTLM CRAM-MD5\nENHANCEDSTATUSCODES\n8BITMIME\nDSN\nSMTPUTF8\nCHUNKING' - test_module.py:5: AssertionError + test_module.py:6: AssertionError -------------------------- Captured stdout setup --------------------------- finalizing ________________________ test_noop[mail.python.org] ________________________ @@ -689,10 +689,10 @@ So let's just do another run: > assert 0 # for demo purposes E assert 0 - test_module.py:11: AssertionError + test_module.py:13: AssertionError ------------------------- Captured stdout teardown ------------------------- finalizing - 4 failed in 0.12 seconds + 4 failed in 1.77s We see that our two test functions each ran twice, against the different ``smtp_connection`` instances. Note also, that with the ``mail.python.org`` @@ -771,7 +771,7 @@ Running the above tests results in the following test IDs being used: - ======================= no tests ran in 0.12 seconds ======================= + ========================== no tests ran in 0.04s =========================== .. _`fixture-parametrize-marks`: @@ -812,7 +812,7 @@ Running this test will *skip* the invocation of ``data_set`` with value ``2``: test_fixture_marks.py::test_data[1] PASSED [ 66%] test_fixture_marks.py::test_data[2] SKIPPED [100%] - =================== 2 passed, 1 skipped in 0.12 seconds ==================== + ======================= 2 passed, 1 skipped in 0.01s ======================= .. _`interdependent fixtures`: @@ -861,7 +861,7 @@ Here we declare an ``app`` fixture which receives the previously defined test_appsetup.py::test_smtp_connection_exists[smtp.gmail.com] PASSED [ 50%] test_appsetup.py::test_smtp_connection_exists[mail.python.org] PASSED [100%] - ========================= 2 passed in 0.12 seconds ========================= + ============================ 2 passed in 0.79s ============================= Due to the parametrization of ``smtp_connection``, the test will run twice with two different ``App`` instances and respective smtp servers. There is no @@ -971,7 +971,7 @@ Let's run the tests in verbose mode and with looking at the print-output: TEARDOWN modarg mod2 - ========================= 8 passed in 0.12 seconds ========================= + ============================ 8 passed in 0.02s ============================= You can see that the parametrized module-scoped ``modarg`` resource caused an ordering of test execution that lead to the fewest possible "active" resources. @@ -1043,7 +1043,7 @@ to verify our fixture is activated and the tests pass: $ pytest -q .. [100%] - 2 passed in 0.12 seconds + 2 passed in 0.02s You can specify multiple fixtures like this: @@ -1151,7 +1151,7 @@ If we run it, we get two passing tests: $ pytest -q .. [100%] - 2 passed in 0.12 seconds + 2 passed in 0.02s Here is how autouse fixtures work in other scopes: diff --git a/doc/en/getting-started.rst b/doc/en/getting-started.rst index f1c28769f..bf1f6ac3d 100644 --- a/doc/en/getting-started.rst +++ b/doc/en/getting-started.rst @@ -28,7 +28,7 @@ Install ``pytest`` .. code-block:: bash $ pytest --version - This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.x/site-packages/pytest.py + This is pytest version 5.x.y, imported from $PYTHON_PREFIX/lib/python3.6/site-packages/pytest.py .. _`simpletest`: @@ -68,8 +68,8 @@ That’s it. You can now execute the test function: E assert 4 == 5 E + where 4 = func(3) - test_sample.py:5: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + test_sample.py:6: AssertionError + ============================ 1 failed in 0.05s ============================= This test returns a failure report because ``func(3)`` does not return ``5``. @@ -108,7 +108,7 @@ Execute the test function with “quiet” reporting mode: $ pytest -q test_sysexit.py . [100%] - 1 passed in 0.12 seconds + 1 passed in 0.01s Group multiple tests in a class -------------------------------------------------------------- @@ -140,12 +140,12 @@ Once you develop multiple tests, you may want to group them into a class. pytest def test_two(self): x = "hello" - > assert hasattr(x, 'check') + > assert hasattr(x, "check") E AssertionError: assert False E + where False = hasattr('hello', 'check') test_class.py:8: AssertionError - 1 failed, 1 passed in 0.12 seconds + 1 failed, 1 passed in 0.05s The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure. @@ -180,7 +180,7 @@ List the name ``tmpdir`` in the test function signature and ``pytest`` will look test_tmpdir.py:3: AssertionError --------------------------- Captured stdout call --------------------------- PYTEST_TMPDIR/test_needsfiles0 - 1 failed in 0.12 seconds + 1 failed in 0.05s More info on tmpdir handling is available at :ref:`Temporary directories and files `. diff --git a/doc/en/index.rst b/doc/en/index.rst index 6c7c84865..8b8e8b337 100644 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -44,7 +44,7 @@ To execute it: E + where 4 = inc(3) test_sample.py:6: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + ============================ 1 failed in 0.06s ============================= Due to ``pytest``'s detailed assertion introspection, only plain ``assert`` statements are used. See :ref:`Getting Started ` for more examples. diff --git a/doc/en/parametrize.rst b/doc/en/parametrize.rst index 2e2d846ea..79716b379 100644 --- a/doc/en/parametrize.rst +++ b/doc/en/parametrize.rst @@ -75,7 +75,7 @@ them in turn: E + where 54 = eval('6*9') test_expectation.py:6: AssertionError - ==================== 1 failed, 2 passed in 0.12 seconds ==================== + ======================= 1 failed, 2 passed in 0.05s ======================== .. note:: @@ -128,7 +128,7 @@ Let's run this: test_expectation.py ..x [100%] - =================== 2 passed, 1 xfailed in 0.12 seconds ==================== + ======================= 2 passed, 1 xfailed in 0.06s ======================= The one parameter set which caused a failure previously now shows up as an "xfailed (expected to fail)" test. @@ -205,7 +205,7 @@ If we now pass two stringinput values, our test will run twice: $ pytest -q --stringinput="hello" --stringinput="world" test_strings.py .. [100%] - 2 passed in 0.12 seconds + 2 passed in 0.01s Let's also run with a stringinput that will lead to a failing test: @@ -225,7 +225,7 @@ Let's also run with a stringinput that will lead to a failing test: E + where = '!'.isalpha test_strings.py:4: AssertionError - 1 failed in 0.12 seconds + 1 failed in 0.05s As expected our test function fails. @@ -239,7 +239,7 @@ list: s [100%] ========================= short test summary info ========================== SKIPPED [1] test_strings.py: got empty parameter set ['stringinput'], function test_valid_string at $REGENDOC_TMPDIR/test_strings.py:2 - 1 skipped in 0.12 seconds + 1 skipped in 0.01s Note that when calling ``metafunc.parametrize`` multiple times with different parameter sets, all parameter names across those sets cannot be duplicated, otherwise an error will be raised. diff --git a/doc/en/skipping.rst b/doc/en/skipping.rst index 2b654560e..7be1eb364 100644 --- a/doc/en/skipping.rst +++ b/doc/en/skipping.rst @@ -371,7 +371,7 @@ Running it with the report-on-xfail option gives this output: XFAIL xfail_demo.py::test_hello6 reason: reason XFAIL xfail_demo.py::test_hello7 - ======================== 7 xfailed in 0.12 seconds ========================= + ============================ 7 xfailed in 0.17s ============================ .. _`skip/xfail with parametrize`: diff --git a/doc/en/tmpdir.rst b/doc/en/tmpdir.rst index 1b565cee8..4084fc015 100644 --- a/doc/en/tmpdir.rst +++ b/doc/en/tmpdir.rst @@ -64,7 +64,7 @@ Running this would result in a passed test except for the last E assert 0 test_tmp_path.py:13: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + ============================ 1 failed in 0.06s ============================= .. _`tmp_path_factory example`: @@ -132,8 +132,8 @@ Running this would result in a passed test except for the last > assert 0 E assert 0 - test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.12 seconds ========================= + test_tmpdir.py:9: AssertionError + ============================ 1 failed in 0.05s ============================= .. _`tmpdir factory example`: diff --git a/doc/en/unittest.rst b/doc/en/unittest.rst index 18b6a721b..b1d58071a 100644 --- a/doc/en/unittest.rst +++ b/doc/en/unittest.rst @@ -151,22 +151,22 @@ the ``self.db`` values in the traceback: def test_method1(self): assert hasattr(self, "db") - > assert 0, self.db # fail for demo purposes + > assert 0, self.db # fail for demo purposes E AssertionError: .DummyDB object at 0xdeadbeef> E assert 0 - test_unittest_db.py:9: AssertionError + test_unittest_db.py:10: AssertionError ___________________________ MyTest.test_method2 ____________________________ self = def test_method2(self): - > assert 0, self.db # fail for demo purposes + > assert 0, self.db # fail for demo purposes E AssertionError: .DummyDB object at 0xdeadbeef> E assert 0 - test_unittest_db.py:12: AssertionError - ========================= 2 failed in 0.12 seconds ========================= + test_unittest_db.py:13: AssertionError + ============================ 2 failed in 0.07s ============================= This default pytest traceback shows that the two test methods share the same ``self.db`` instance which was our intention @@ -219,7 +219,7 @@ Running this test module ...: $ pytest -q test_unittest_cleandir.py . [100%] - 1 passed in 0.12 seconds + 1 passed in 0.02s ... gives us one passed test because the ``initdir`` fixture function was executed ahead of the ``test_method``. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index d5ff8a984..0ad70ff27 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -247,7 +247,7 @@ Example: XPASS test_example.py::test_xpass always xfail ERROR test_example.py::test_error - assert 0 FAILED test_example.py::test_fail - assert 0 - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.08s === The ``-r`` options accepts a number of characters after it, with ``a`` used above meaning "all except passes". @@ -297,7 +297,7 @@ More than one character can be used, so for example to only see failed and skipp ========================= short test summary info ========================== FAILED test_example.py::test_fail - assert 0 SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.08s === Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had captured output: @@ -336,7 +336,7 @@ captured output: ok ========================= short test summary info ========================== PASSED test_example.py::test_ok - = 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12 seconds = + == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.08s === .. _pdb-option: diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index d48cd4688..e54d9f027 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -41,7 +41,7 @@ Running pytest now produces this output: warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: https://docs.pytest.org/en/latest/warnings.html - =================== 1 passed, 1 warnings in 0.12 seconds =================== + ====================== 1 passed, 1 warnings in 0.01s ======================= The ``-W`` flag can be passed to control which warnings will be displayed or even turn them into errors: @@ -64,7 +64,7 @@ them into errors: E UserWarning: api v1, should use functions from v2 test_show_warnings.py:5: UserWarning - 1 failed in 0.12 seconds + 1 failed in 0.05s The same option can be set in the ``pytest.ini`` file using the ``filterwarnings`` ini option. For example, the configuration below will ignore all user warnings, but will transform @@ -407,7 +407,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta class Test: -- Docs: https://docs.pytest.org/en/latest/warnings.html - 1 warnings in 0.12 seconds + 1 warnings in 0.01s These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 7975f5826..9f3d3115e 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -442,7 +442,7 @@ additionally it is possible to copy examples for an example folder before runnin testdir.copy_example("test_example.py") -- Docs: https://docs.pytest.org/en/latest/warnings.html - =================== 2 passed, 1 warnings in 0.12 seconds =================== + ====================== 2 passed, 1 warnings in 0.28s ======================= For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult