From 661055105c35039774c06be129fdf868b56dbdac Mon Sep 17 00:00:00 2001 From: Niclas Olofsson Date: Sat, 26 Sep 2015 14:30:16 +0200 Subject: [PATCH 01/40] Restructured project. --- pytest_stepwise/__init__.py | 1 + pytest_stepwise/compat.py | 4 + pytest_stepwise/plugin.py | 90 ++++++++++++++++++++++ tests/conftest.py | 1 + tests/test_pytest_stepwise.py | 136 ++++++++++++++++++++++++++++++++++ 5 files changed, 232 insertions(+) create mode 100644 pytest_stepwise/__init__.py create mode 100644 pytest_stepwise/compat.py create mode 100644 pytest_stepwise/plugin.py create mode 100644 tests/conftest.py create mode 100644 tests/test_pytest_stepwise.py diff --git a/pytest_stepwise/__init__.py b/pytest_stepwise/__init__.py new file mode 100644 index 00000000000..58d168b07f6 --- /dev/null +++ b/pytest_stepwise/__init__.py @@ -0,0 +1 @@ +__version__ = '0.4' diff --git a/pytest_stepwise/compat.py b/pytest_stepwise/compat.py new file mode 100644 index 00000000000..31f132c7078 --- /dev/null +++ b/pytest_stepwise/compat.py @@ -0,0 +1,4 @@ +try: + from _pytest.cacheprovider import Cache +except ImportError: + from pytest_cache import Cache diff --git a/pytest_stepwise/plugin.py b/pytest_stepwise/plugin.py new file mode 100644 index 00000000000..949ca3a6764 --- /dev/null +++ b/pytest_stepwise/plugin.py @@ -0,0 +1,90 @@ +import pytest +from .compat import Cache + + +def pytest_addoption(parser): + group = parser.getgroup('general') + group.addoption('--sw', action='store_true', dest='stepwise', + help='alias for --stepwise') + group.addoption('--stepwise', action='store_true', dest='stepwise', + help='exit on test fail and continue from last failing test next time') + group.addoption('--skip', action='store_true', dest='skip', + help='ignore the first failing test but stop on the next failing test') + + +@pytest.hookimpl(tryfirst=True) +def pytest_configure(config): + config.cache = Cache(config) + config.pluginmanager.register(StepwisePlugin(config), 'stepwiseplugin') + + +class StepwisePlugin: + def __init__(self, config): + self.config = config + self.active = config.getvalue('stepwise') + self.session = None + + if self.active: + self.lastfailed = config.cache.get('cache/stepwise', set()) + self.skip = config.getvalue('skip') + + def pytest_sessionstart(self, session): + self.session = session + + def pytest_collection_modifyitems(self, session, config, items): + if not self.active or not self.lastfailed: + return + + already_passed = [] + found = False + + # Make a list of all tests that has been runned before the last failing one. + for item in items: + if item.nodeid in self.lastfailed: + found = True + break + else: + already_passed.append(item) + + # If the previously failed test was not found among the test items, + # do not skip any tests. + if not found: + already_passed = [] + + for item in already_passed: + items.remove(item) + + config.hook.pytest_deselected(items=already_passed) + + def pytest_collectreport(self, report): + if self.active and report.failed: + self.session.shouldstop = 'Error when collecting test, stopping test execution.' + + def pytest_runtest_logreport(self, report): + # Skip this hook if plugin is not active or the test is xfailed. + if not self.active or 'xfail' in report.keywords: + return + + if report.failed: + if self.skip: + # Remove test from the failed ones (if it exists) and unset the skip option + # to make sure the following tests will not be skipped. + self.lastfailed.discard(report.nodeid) + self.skip = False + else: + # Mark test as the last failing and interrupt the test session. + self.lastfailed.add(report.nodeid) + self.session.shouldstop = 'Test failed, continuing from this test next run.' + + else: + # If the test was actually run and did pass. + if report.when == 'call': + # Remove test from the failed ones, if exists. + self.lastfailed.discard(report.nodeid) + + def pytest_sessionfinish(self, session): + if self.active: + self.config.cache.set('cache/stepwise', self.lastfailed) + else: + # Clear the list of failing tests if the plugin is not active. + self.config.cache.set('cache/stepwise', set()) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000000..bc711e55fef --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = 'pytester' diff --git a/tests/test_pytest_stepwise.py b/tests/test_pytest_stepwise.py new file mode 100644 index 00000000000..96b376c3fa8 --- /dev/null +++ b/tests/test_pytest_stepwise.py @@ -0,0 +1,136 @@ +import pytest + + +@pytest.fixture +def stepwise_testdir(testdir): + # Rather than having to modify our testfile between tests, we introduce + # a flag for wether or not the second test should fail. + testdir.makeconftest(''' +def pytest_addoption(parser): + group = parser.getgroup('general') + group.addoption('--fail', action='store_true', dest='fail') + group.addoption('--fail-last', action='store_true', dest='fail_last') +''') + + # Create a simple test suite. + testdir.makepyfile(test_stepwise=''' +def test_success_before_fail(): + assert 1 + +def test_fail_on_flag(request): + assert not request.config.getvalue('fail') + +def test_success_after_fail(): + assert 1 + +def test_fail_last_on_flag(request): + assert not request.config.getvalue('fail_last') + +def test_success_after_last_fail(): + assert 1 +''') + + testdir.makepyfile(testfile_b=''' +def test_success(): + assert 1 +''') + + return testdir + + +@pytest.fixture +def error_testdir(testdir): + testdir.makepyfile(test_stepwise=''' +def test_error(nonexisting_fixture): + assert 1 + +def test_success_after_fail(): + assert 1 +''') + + return testdir + + +@pytest.fixture +def broken_testdir(testdir): + testdir.makepyfile(working_testfile='def test_proper(): assert 1', broken_testfile='foobar') + return testdir + + +def test_run_without_stepwise(stepwise_testdir): + result = stepwise_testdir.runpytest('-v', '--strict', '--fail') + + assert not result.errlines + result.stdout.fnmatch_lines(['*test_success_before_fail PASSED*']) + result.stdout.fnmatch_lines(['*test_fail_on_flag FAILED*']) + result.stdout.fnmatch_lines(['*test_success_after_fail PASSED*']) + + +def test_fail_and_continue_with_stepwise(stepwise_testdir): + # Run the tests with a failing second test. + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail') + assert not result.errlines + + stdout = result.stdout.str() + # Make sure we stop after first failing test. + assert 'test_success_before_fail PASSED' in stdout + assert 'test_fail_on_flag FAILED' in stdout + assert 'test_success_after_fail' not in stdout + + # "Fix" the test that failed in the last run and run it again. + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise') + assert not result.errlines + + stdout = result.stdout.str() + # Make sure the latest failing test runs and then continues. + assert 'test_success_before_fail' not in stdout + assert 'test_fail_on_flag PASSED' in stdout + assert 'test_success_after_fail PASSED' in stdout + + +def test_run_with_skip_option(stepwise_testdir): + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--skip', + '--fail', '--fail-last') + assert not result.errlines + + stdout = result.stdout.str() + # Make sure first fail is ignore and second fail stops the test run. + assert 'test_fail_on_flag FAILED' in stdout + assert 'test_success_after_fail PASSED' in stdout + assert 'test_fail_last_on_flag FAILED' in stdout + assert 'test_success_after_last_fail' not in stdout + + +def test_fail_on_errors(error_testdir): + result = error_testdir.runpytest('-v', '--strict', '--stepwise') + + assert not result.errlines + stdout = result.stdout.str() + + assert 'test_error ERROR' in stdout + assert 'test_success_after_fail' not in stdout + + +def test_change_testfile(stepwise_testdir): + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail', + 'test_stepwise.py') + assert not result.errlines + + stdout = result.stdout.str() + assert 'test_fail_on_flag FAILED' in stdout + + # Make sure the second test run starts from the beginning, since the + # test to continue from does not exist in testfile_b. + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', + 'testfile_b.py') + assert not result.errlines + + stdout = result.stdout.str() + assert 'test_success PASSED' in stdout + + +def test_stop_on_collection_errors(broken_testdir): + result = broken_testdir.runpytest('-v', '--strict', '--stepwise', 'working_testfile.py', 'broken_testfile.py') + + stdout = result.stdout.str() + assert 'Error when collecting test' in stdout From 1d23bef3fb0bf3bf5efba933ccb7ac1bf5b65efa Mon Sep 17 00:00:00 2001 From: Niclas Olofsson Date: Sat, 26 Sep 2015 14:32:11 +0200 Subject: [PATCH 02/40] Use a single node ID rather than a set for failed tests. --- pytest_stepwise/plugin.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pytest_stepwise/plugin.py b/pytest_stepwise/plugin.py index 949ca3a6764..89cd5125bf0 100644 --- a/pytest_stepwise/plugin.py +++ b/pytest_stepwise/plugin.py @@ -25,7 +25,7 @@ def __init__(self, config): self.session = None if self.active: - self.lastfailed = config.cache.get('cache/stepwise', set()) + self.lastfailed = config.cache.get('cache/stepwise', None) self.skip = config.getvalue('skip') def pytest_sessionstart(self, session): @@ -40,7 +40,7 @@ def pytest_collection_modifyitems(self, session, config, items): # Make a list of all tests that has been runned before the last failing one. for item in items: - if item.nodeid in self.lastfailed: + if item.nodeid == self.lastfailed: found = True break else: @@ -69,22 +69,25 @@ def pytest_runtest_logreport(self, report): if self.skip: # Remove test from the failed ones (if it exists) and unset the skip option # to make sure the following tests will not be skipped. - self.lastfailed.discard(report.nodeid) + if report.nodeid == self.lastfailed: + self.lastfailed = None + self.skip = False else: # Mark test as the last failing and interrupt the test session. - self.lastfailed.add(report.nodeid) + self.lastfailed = report.nodeid self.session.shouldstop = 'Test failed, continuing from this test next run.' else: # If the test was actually run and did pass. if report.when == 'call': # Remove test from the failed ones, if exists. - self.lastfailed.discard(report.nodeid) + if report.nodeid == self.lastfailed: + self.lastfailed = None def pytest_sessionfinish(self, session): if self.active: self.config.cache.set('cache/stepwise', self.lastfailed) else: # Clear the list of failing tests if the plugin is not active. - self.config.cache.set('cache/stepwise', set()) + self.config.cache.set('cache/stepwise', []) From 33f1ff4e8cb49f05c0fe8df38765741e926dd12e Mon Sep 17 00:00:00 2001 From: Niclas Olofsson Date: Sat, 26 Sep 2015 14:59:28 +0200 Subject: [PATCH 03/40] Use result.stderr in tests since result.errlines has changed behaviour. --- tests/test_pytest_stepwise.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/test_pytest_stepwise.py b/tests/test_pytest_stepwise.py index 96b376c3fa8..1d0c4e8a8b1 100644 --- a/tests/test_pytest_stepwise.py +++ b/tests/test_pytest_stepwise.py @@ -60,7 +60,6 @@ def broken_testdir(testdir): def test_run_without_stepwise(stepwise_testdir): result = stepwise_testdir.runpytest('-v', '--strict', '--fail') - assert not result.errlines result.stdout.fnmatch_lines(['*test_success_before_fail PASSED*']) result.stdout.fnmatch_lines(['*test_fail_on_flag FAILED*']) result.stdout.fnmatch_lines(['*test_success_after_fail PASSED*']) @@ -69,7 +68,7 @@ def test_run_without_stepwise(stepwise_testdir): def test_fail_and_continue_with_stepwise(stepwise_testdir): # Run the tests with a failing second test. result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() # Make sure we stop after first failing test. @@ -79,7 +78,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): # "Fix" the test that failed in the last run and run it again. result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() # Make sure the latest failing test runs and then continues. @@ -91,7 +90,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): def test_run_with_skip_option(stepwise_testdir): result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--skip', '--fail', '--fail-last') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() # Make sure first fail is ignore and second fail stops the test run. @@ -104,7 +103,7 @@ def test_run_with_skip_option(stepwise_testdir): def test_fail_on_errors(error_testdir): result = error_testdir.runpytest('-v', '--strict', '--stepwise') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() assert 'test_error ERROR' in stdout @@ -114,7 +113,7 @@ def test_fail_on_errors(error_testdir): def test_change_testfile(stepwise_testdir): result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail', 'test_stepwise.py') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() assert 'test_fail_on_flag FAILED' in stdout @@ -123,7 +122,7 @@ def test_change_testfile(stepwise_testdir): # test to continue from does not exist in testfile_b. result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', 'testfile_b.py') - assert not result.errlines + assert not result.stderr.str() stdout = result.stdout.str() assert 'test_success PASSED' in stdout From bd9495486b7c45a1aadd5fa94a95110ac450a143 Mon Sep 17 00:00:00 2001 From: Niclas Olofsson Date: Sat, 26 Sep 2015 15:23:11 +0200 Subject: [PATCH 04/40] pytest 2.7 compatibility. --- pytest_stepwise/compat.py | 8 ++++++++ pytest_stepwise/plugin.py | 5 ++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/pytest_stepwise/compat.py b/pytest_stepwise/compat.py index 31f132c7078..ce28f74747d 100644 --- a/pytest_stepwise/compat.py +++ b/pytest_stepwise/compat.py @@ -1,4 +1,12 @@ +import pytest + try: from _pytest.cacheprovider import Cache except ImportError: from pytest_cache import Cache + + +if hasattr(pytest, 'hookimpl'): + tryfirst = pytest.hookimpl(tryfirst=True) +else: + tryfirst = pytest.mark.tryfirst diff --git a/pytest_stepwise/plugin.py b/pytest_stepwise/plugin.py index 89cd5125bf0..1f0137a463c 100644 --- a/pytest_stepwise/plugin.py +++ b/pytest_stepwise/plugin.py @@ -1,5 +1,4 @@ -import pytest -from .compat import Cache +from .compat import Cache, tryfirst def pytest_addoption(parser): @@ -12,7 +11,7 @@ def pytest_addoption(parser): help='ignore the first failing test but stop on the next failing test') -@pytest.hookimpl(tryfirst=True) +@tryfirst def pytest_configure(config): config.cache = Cache(config) config.pluginmanager.register(StepwisePlugin(config), 'stepwiseplugin') From d9c428c1ded12bae5ac98c6780e20bb0d211c90a Mon Sep 17 00:00:00 2001 From: David Szotten Date: Wed, 1 Aug 2018 11:48:15 +0100 Subject: [PATCH 05/40] add compat for pytest 3.7 and tox config for (some of) the versions i could still get working --- pytest_stepwise/compat.py | 6 ++++++ tests/test_pytest_stepwise.py | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pytest_stepwise/compat.py b/pytest_stepwise/compat.py index ce28f74747d..a1cc1e986d1 100644 --- a/pytest_stepwise/compat.py +++ b/pytest_stepwise/compat.py @@ -5,6 +5,12 @@ except ImportError: from pytest_cache import Cache +try: + # pytest 3.7+ + Cache = Cache.for_config +except AttributeError: + pass + if hasattr(pytest, 'hookimpl'): tryfirst = pytest.hookimpl(tryfirst=True) diff --git a/tests/test_pytest_stepwise.py b/tests/test_pytest_stepwise.py index 1d0c4e8a8b1..cb52e9eadf1 100644 --- a/tests/test_pytest_stepwise.py +++ b/tests/test_pytest_stepwise.py @@ -132,4 +132,7 @@ def test_stop_on_collection_errors(broken_testdir): result = broken_testdir.runpytest('-v', '--strict', '--stepwise', 'working_testfile.py', 'broken_testfile.py') stdout = result.stdout.str() - assert 'Error when collecting test' in stdout + if pytest.__version__ < '3.0.0': + assert 'Error when collecting test' in stdout + else: + assert 'errors during collection' in stdout From c56d7ac40e795494a0f6b445402dec5d36b9f5ed Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 09:23:21 +0100 Subject: [PATCH 06/40] move files into the pytest file structure --- pytest_stepwise/__init__.py | 1 - pytest_stepwise/compat.py | 18 ------------------ .../plugin.py => src/_pytest/stepwise.py | 0 .../test_stepwise.py | 0 tests/conftest.py | 1 - 5 files changed, 20 deletions(-) delete mode 100644 pytest_stepwise/__init__.py delete mode 100644 pytest_stepwise/compat.py rename pytest_stepwise/plugin.py => src/_pytest/stepwise.py (100%) rename tests/test_pytest_stepwise.py => testing/test_stepwise.py (100%) delete mode 100644 tests/conftest.py diff --git a/pytest_stepwise/__init__.py b/pytest_stepwise/__init__.py deleted file mode 100644 index 58d168b07f6..00000000000 --- a/pytest_stepwise/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.4' diff --git a/pytest_stepwise/compat.py b/pytest_stepwise/compat.py deleted file mode 100644 index a1cc1e986d1..00000000000 --- a/pytest_stepwise/compat.py +++ /dev/null @@ -1,18 +0,0 @@ -import pytest - -try: - from _pytest.cacheprovider import Cache -except ImportError: - from pytest_cache import Cache - -try: - # pytest 3.7+ - Cache = Cache.for_config -except AttributeError: - pass - - -if hasattr(pytest, 'hookimpl'): - tryfirst = pytest.hookimpl(tryfirst=True) -else: - tryfirst = pytest.mark.tryfirst diff --git a/pytest_stepwise/plugin.py b/src/_pytest/stepwise.py similarity index 100% rename from pytest_stepwise/plugin.py rename to src/_pytest/stepwise.py diff --git a/tests/test_pytest_stepwise.py b/testing/test_stepwise.py similarity index 100% rename from tests/test_pytest_stepwise.py rename to testing/test_stepwise.py diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index bc711e55fef..00000000000 --- a/tests/conftest.py +++ /dev/null @@ -1 +0,0 @@ -pytest_plugins = 'pytester' From 63c01d1541eda890cdb10909af09195711c8a36a Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 12:58:11 +0100 Subject: [PATCH 07/40] update for builtin plugin --- src/_pytest/config/__init__.py | 1 + src/_pytest/stepwise.py | 15 +++++++-------- testing/test_stepwise.py | 12 ++++++------ 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 88cbf14bab0..29227cc6b7a 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -126,6 +126,7 @@ def directory_arg(path, optname): "freeze_support", "setuponly", "setupplan", + "stepwise", "warnings", "logging", ) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 1f0137a463c..f408e1fa904 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,19 +1,18 @@ -from .compat import Cache, tryfirst +from _pytest.cacheprovider import Cache +import pytest def pytest_addoption(parser): group = parser.getgroup('general') - group.addoption('--sw', action='store_true', dest='stepwise', - help='alias for --stepwise') - group.addoption('--stepwise', action='store_true', dest='stepwise', + group.addoption('--sw', '--stepwise', action='store_true', dest='stepwise', help='exit on test fail and continue from last failing test next time') - group.addoption('--skip', action='store_true', dest='skip', + group.addoption('--stepwise-skip', action='store_true', dest='stepwise_skip', help='ignore the first failing test but stop on the next failing test') -@tryfirst +@pytest.hookimpl(tryfirst=True) def pytest_configure(config): - config.cache = Cache(config) + config.cache = Cache.for_config(config) config.pluginmanager.register(StepwisePlugin(config), 'stepwiseplugin') @@ -25,7 +24,7 @@ def __init__(self, config): if self.active: self.lastfailed = config.cache.get('cache/stepwise', None) - self.skip = config.getvalue('skip') + self.skip = config.getvalue('stepwise_skip') def pytest_sessionstart(self, session): self.session = session diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index cb52e9eadf1..0e1e53226a4 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -13,7 +13,7 @@ def pytest_addoption(parser): ''') # Create a simple test suite. - testdir.makepyfile(test_stepwise=''' + testdir.makepyfile(test_a=''' def test_success_before_fail(): assert 1 @@ -30,7 +30,7 @@ def test_success_after_last_fail(): assert 1 ''') - testdir.makepyfile(testfile_b=''' + testdir.makepyfile(test_b=''' def test_success(): assert 1 ''') @@ -40,7 +40,7 @@ def test_success(): @pytest.fixture def error_testdir(testdir): - testdir.makepyfile(test_stepwise=''' + testdir.makepyfile(test_a=''' def test_error(nonexisting_fixture): assert 1 @@ -88,7 +88,7 @@ def test_fail_and_continue_with_stepwise(stepwise_testdir): def test_run_with_skip_option(stepwise_testdir): - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--skip', + result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--stepwise-skip', '--fail', '--fail-last') assert not result.stderr.str() @@ -112,7 +112,7 @@ def test_fail_on_errors(error_testdir): def test_change_testfile(stepwise_testdir): result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail', - 'test_stepwise.py') + 'test_a.py') assert not result.stderr.str() stdout = result.stdout.str() @@ -121,7 +121,7 @@ def test_change_testfile(stepwise_testdir): # Make sure the second test run starts from the beginning, since the # test to continue from does not exist in testfile_b. result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', - 'testfile_b.py') + 'test_b.py') assert not result.stderr.str() stdout = result.stdout.str() From fd66f69c1997fb9dc38a7891ec59210ecbb09558 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 18:50:06 +0100 Subject: [PATCH 08/40] draft doc --- doc/en/cache.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 08f20465573..245edfc1b9b 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -260,3 +260,9 @@ by adding the ``--cache-clear`` option like this:: This is recommended for invocations from Continuous Integration servers where isolation and correctness is more important than speed. + + +Stepwise +-------- + +As an alternative to ``--lf -x``, especially for cases where you expect a large part of the test suite will fail, ``--sw``, ``--stepwise`` allows you to fix them one at a time. The test suite will run until the first failure and then stop. At the next invocation, tests will continue from the last failing test and then run until the next failing test. You may use the ``--stepwise-skip`` option to ignore one failing test and stop the test execution on the second failing test instead. This is useful if you get stuck on a failing test and just want to ignore it until later. From 8c059dbc48d201cbaf24a9fc6cc95f357c31abed Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 19:04:50 +0100 Subject: [PATCH 09/40] draft changelog --- changelog/xxx.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/xxx.feature.rst diff --git a/changelog/xxx.feature.rst b/changelog/xxx.feature.rst new file mode 100644 index 00000000000..812898f909b --- /dev/null +++ b/changelog/xxx.feature.rst @@ -0,0 +1 @@ +Add ``-sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation `_ for more info. From 126bb0760e3f489562d7f7658f26362dcecddc32 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 19:04:55 +0100 Subject: [PATCH 10/40] authors --- AUTHORS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS b/AUTHORS index 988d0e5feb5..ae375228bdc 100644 --- a/AUTHORS +++ b/AUTHORS @@ -59,6 +59,7 @@ Danielle Jenkins Dave Hunt David Díaz-Barquero David Mohr +David Szotten David Vierra Daw-Ran Liou Denis Kirisov @@ -161,6 +162,7 @@ Miro Hrončok Nathaniel Waisbrot Ned Batchelder Neven Mundar +Niclas Olofsson Nicolas Delaby Oleg Pidsadnyi Oleg Sushchenko From 4f652c9045ad9cbae3d7f67a1ffd319c95c7face Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 19:57:36 +0100 Subject: [PATCH 11/40] we have a pr number now --- changelog/{xxx.feature.rst => 4147.feature.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{xxx.feature.rst => 4147.feature.rst} (100%) diff --git a/changelog/xxx.feature.rst b/changelog/4147.feature.rst similarity index 100% rename from changelog/xxx.feature.rst rename to changelog/4147.feature.rst From e773c8ceda6ca576bca148f9018b6d287d709e3a Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 20:48:46 +0100 Subject: [PATCH 12/40] linting --- src/_pytest/stepwise.py | 43 ++++++++++++------- testing/test_stepwise.py | 93 +++++++++++++++++++++++----------------- 2 files changed, 81 insertions(+), 55 deletions(-) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index f408e1fa904..9af975ce143 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -3,28 +3,37 @@ def pytest_addoption(parser): - group = parser.getgroup('general') - group.addoption('--sw', '--stepwise', action='store_true', dest='stepwise', - help='exit on test fail and continue from last failing test next time') - group.addoption('--stepwise-skip', action='store_true', dest='stepwise_skip', - help='ignore the first failing test but stop on the next failing test') + group = parser.getgroup("general") + group.addoption( + "--sw", + "--stepwise", + action="store_true", + dest="stepwise", + help="exit on test fail and continue from last failing test next time", + ) + group.addoption( + "--stepwise-skip", + action="store_true", + dest="stepwise_skip", + help="ignore the first failing test but stop on the next failing test", + ) @pytest.hookimpl(tryfirst=True) def pytest_configure(config): config.cache = Cache.for_config(config) - config.pluginmanager.register(StepwisePlugin(config), 'stepwiseplugin') + config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") class StepwisePlugin: def __init__(self, config): self.config = config - self.active = config.getvalue('stepwise') + self.active = config.getvalue("stepwise") self.session = None if self.active: - self.lastfailed = config.cache.get('cache/stepwise', None) - self.skip = config.getvalue('stepwise_skip') + self.lastfailed = config.cache.get("cache/stepwise", None) + self.skip = config.getvalue("stepwise_skip") def pytest_sessionstart(self, session): self.session = session @@ -56,11 +65,13 @@ def pytest_collection_modifyitems(self, session, config, items): def pytest_collectreport(self, report): if self.active and report.failed: - self.session.shouldstop = 'Error when collecting test, stopping test execution.' + self.session.shouldstop = ( + "Error when collecting test, stopping test execution." + ) def pytest_runtest_logreport(self, report): # Skip this hook if plugin is not active or the test is xfailed. - if not self.active or 'xfail' in report.keywords: + if not self.active or "xfail" in report.keywords: return if report.failed: @@ -74,18 +85,20 @@ def pytest_runtest_logreport(self, report): else: # Mark test as the last failing and interrupt the test session. self.lastfailed = report.nodeid - self.session.shouldstop = 'Test failed, continuing from this test next run.' + self.session.shouldstop = ( + "Test failed, continuing from this test next run." + ) else: # If the test was actually run and did pass. - if report.when == 'call': + if report.when == "call": # Remove test from the failed ones, if exists. if report.nodeid == self.lastfailed: self.lastfailed = None def pytest_sessionfinish(self, session): if self.active: - self.config.cache.set('cache/stepwise', self.lastfailed) + self.config.cache.set("cache/stepwise", self.lastfailed) else: # Clear the list of failing tests if the plugin is not active. - self.config.cache.set('cache/stepwise', []) + self.config.cache.set("cache/stepwise", []) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 0e1e53226a4..0e52911f4f6 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -5,15 +5,18 @@ def stepwise_testdir(testdir): # Rather than having to modify our testfile between tests, we introduce # a flag for wether or not the second test should fail. - testdir.makeconftest(''' + testdir.makeconftest( + """ def pytest_addoption(parser): group = parser.getgroup('general') group.addoption('--fail', action='store_true', dest='fail') group.addoption('--fail-last', action='store_true', dest='fail_last') -''') +""" + ) # Create a simple test suite. - testdir.makepyfile(test_a=''' + testdir.makepyfile( + test_a=""" def test_success_before_fail(): assert 1 @@ -28,111 +31,121 @@ def test_fail_last_on_flag(request): def test_success_after_last_fail(): assert 1 -''') +""" + ) - testdir.makepyfile(test_b=''' + testdir.makepyfile( + test_b=""" def test_success(): assert 1 -''') +""" + ) return testdir @pytest.fixture def error_testdir(testdir): - testdir.makepyfile(test_a=''' + testdir.makepyfile( + test_a=""" def test_error(nonexisting_fixture): assert 1 def test_success_after_fail(): assert 1 -''') +""" + ) return testdir @pytest.fixture def broken_testdir(testdir): - testdir.makepyfile(working_testfile='def test_proper(): assert 1', broken_testfile='foobar') + testdir.makepyfile( + working_testfile="def test_proper(): assert 1", broken_testfile="foobar" + ) return testdir def test_run_without_stepwise(stepwise_testdir): - result = stepwise_testdir.runpytest('-v', '--strict', '--fail') + result = stepwise_testdir.runpytest("-v", "--strict", "--fail") - result.stdout.fnmatch_lines(['*test_success_before_fail PASSED*']) - result.stdout.fnmatch_lines(['*test_fail_on_flag FAILED*']) - result.stdout.fnmatch_lines(['*test_success_after_fail PASSED*']) + result.stdout.fnmatch_lines(["*test_success_before_fail PASSED*"]) + result.stdout.fnmatch_lines(["*test_fail_on_flag FAILED*"]) + result.stdout.fnmatch_lines(["*test_success_after_fail PASSED*"]) def test_fail_and_continue_with_stepwise(stepwise_testdir): # Run the tests with a failing second test. - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail') + result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "--fail") assert not result.stderr.str() stdout = result.stdout.str() # Make sure we stop after first failing test. - assert 'test_success_before_fail PASSED' in stdout - assert 'test_fail_on_flag FAILED' in stdout - assert 'test_success_after_fail' not in stdout + assert "test_success_before_fail PASSED" in stdout + assert "test_fail_on_flag FAILED" in stdout + assert "test_success_after_fail" not in stdout # "Fix" the test that failed in the last run and run it again. - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise') + result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise") assert not result.stderr.str() stdout = result.stdout.str() # Make sure the latest failing test runs and then continues. - assert 'test_success_before_fail' not in stdout - assert 'test_fail_on_flag PASSED' in stdout - assert 'test_success_after_fail PASSED' in stdout + assert "test_success_before_fail" not in stdout + assert "test_fail_on_flag PASSED" in stdout + assert "test_success_after_fail PASSED" in stdout def test_run_with_skip_option(stepwise_testdir): - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--stepwise-skip', - '--fail', '--fail-last') + result = stepwise_testdir.runpytest( + "-v", "--strict", "--stepwise", "--stepwise-skip", "--fail", "--fail-last" + ) assert not result.stderr.str() stdout = result.stdout.str() # Make sure first fail is ignore and second fail stops the test run. - assert 'test_fail_on_flag FAILED' in stdout - assert 'test_success_after_fail PASSED' in stdout - assert 'test_fail_last_on_flag FAILED' in stdout - assert 'test_success_after_last_fail' not in stdout + assert "test_fail_on_flag FAILED" in stdout + assert "test_success_after_fail PASSED" in stdout + assert "test_fail_last_on_flag FAILED" in stdout + assert "test_success_after_last_fail" not in stdout def test_fail_on_errors(error_testdir): - result = error_testdir.runpytest('-v', '--strict', '--stepwise') + result = error_testdir.runpytest("-v", "--strict", "--stepwise") assert not result.stderr.str() stdout = result.stdout.str() - assert 'test_error ERROR' in stdout - assert 'test_success_after_fail' not in stdout + assert "test_error ERROR" in stdout + assert "test_success_after_fail" not in stdout def test_change_testfile(stepwise_testdir): - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', '--fail', - 'test_a.py') + result = stepwise_testdir.runpytest( + "-v", "--strict", "--stepwise", "--fail", "test_a.py" + ) assert not result.stderr.str() stdout = result.stdout.str() - assert 'test_fail_on_flag FAILED' in stdout + assert "test_fail_on_flag FAILED" in stdout # Make sure the second test run starts from the beginning, since the # test to continue from does not exist in testfile_b. - result = stepwise_testdir.runpytest('-v', '--strict', '--stepwise', - 'test_b.py') + result = stepwise_testdir.runpytest("-v", "--strict", "--stepwise", "test_b.py") assert not result.stderr.str() stdout = result.stdout.str() - assert 'test_success PASSED' in stdout + assert "test_success PASSED" in stdout def test_stop_on_collection_errors(broken_testdir): - result = broken_testdir.runpytest('-v', '--strict', '--stepwise', 'working_testfile.py', 'broken_testfile.py') + result = broken_testdir.runpytest( + "-v", "--strict", "--stepwise", "working_testfile.py", "broken_testfile.py" + ) stdout = result.stdout.str() - if pytest.__version__ < '3.0.0': - assert 'Error when collecting test' in stdout + if pytest.__version__ < "3.0.0": + assert "Error when collecting test" in stdout else: - assert 'errors during collection' in stdout + assert "errors during collection" in stdout From 8187c148d96de08bac2d1cdad34b825d3675fdef Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 21:58:30 +0100 Subject: [PATCH 13/40] now pinned to pytest version --- testing/test_stepwise.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index 0e52911f4f6..ad9b77296bc 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -145,7 +145,4 @@ def test_stop_on_collection_errors(broken_testdir): ) stdout = result.stdout.str() - if pytest.__version__ < "3.0.0": - assert "Error when collecting test" in stdout - else: - assert "errors during collection" in stdout + assert "errors during collection" in stdout From d67d189d00c913218cdec3626460536ecae7d351 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Sun, 14 Oct 2018 21:59:33 +0100 Subject: [PATCH 14/40] grammar --- src/_pytest/stepwise.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 9af975ce143..3365af1b570 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -45,7 +45,7 @@ def pytest_collection_modifyitems(self, session, config, items): already_passed = [] found = False - # Make a list of all tests that has been runned before the last failing one. + # Make a list of all tests that have been run before the last failing one. for item in items: if item.nodeid == self.lastfailed: found = True From c25310d34f3ef454b7c3e363e0bd6802dab78e6e Mon Sep 17 00:00:00 2001 From: David Szotten Date: Mon, 15 Oct 2018 20:39:51 +0100 Subject: [PATCH 15/40] fix cacheprovider test --- testing/test_cacheprovider.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 2444d8bc1ca..114a63683f5 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -63,7 +63,8 @@ def test_error(): ) result = testdir.runpytest("-rw") assert result.ret == 1 - result.stdout.fnmatch_lines(["*could not create cache path*", "*2 warnings*"]) + # warnings from nodeids, lastfailed, and stepwise + result.stdout.fnmatch_lines(["*could not create cache path*", "*3 warnings*"]) def test_config_cache(self, testdir): testdir.makeconftest( From e478f66d8b9d0e25af7aa4695192bf1adf063ba4 Mon Sep 17 00:00:00 2001 From: David Szotten Date: Wed, 17 Oct 2018 09:08:40 +0100 Subject: [PATCH 16/40] cache is set by the cacheprovider --- src/_pytest/stepwise.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytest/stepwise.py b/src/_pytest/stepwise.py index 3365af1b570..1efa2e7ca74 100644 --- a/src/_pytest/stepwise.py +++ b/src/_pytest/stepwise.py @@ -1,4 +1,3 @@ -from _pytest.cacheprovider import Cache import pytest @@ -19,9 +18,8 @@ def pytest_addoption(parser): ) -@pytest.hookimpl(tryfirst=True) +@pytest.hookimpl def pytest_configure(config): - config.cache = Cache.for_config(config) config.pluginmanager.register(StepwisePlugin(config), "stepwiseplugin") From f694d8d6ad4320276001f2a4e40c717a8291d705 Mon Sep 17 00:00:00 2001 From: Sven-Hendrik Haase Date: Thu, 18 Oct 2018 03:27:30 +0200 Subject: [PATCH 17/40] Make --color more colorful --- AUTHORS | 1 + changelog/4188.feature.rst | 1 + src/_pytest/terminal.py | 26 +++++++++++++------------- 3 files changed, 15 insertions(+), 13 deletions(-) create mode 100644 changelog/4188.feature.rst diff --git a/AUTHORS b/AUTHORS index c63c0a00591..e5bf56a65d3 100644 --- a/AUTHORS +++ b/AUTHORS @@ -202,6 +202,7 @@ Stefan Zimmermann Stefano Taschini Steffen Allner Stephan Obermann +Sven-Hendrik Haase Tadek Teleżyński Tarcisio Fischer Tareq Alayan diff --git a/changelog/4188.feature.rst b/changelog/4188.feature.rst new file mode 100644 index 00000000000..d3169efc06e --- /dev/null +++ b/changelog/4188.feature.rst @@ -0,0 +1 @@ +Make ``--color`` emit colorful dots when not running in verbose mode. Earlier, it would only colorize the test-by-test output if ``--verbose`` was also passed. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 8deb330cc78..d207dd78507 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -263,7 +263,7 @@ def hasopt(self, char): char = {"xfailed": "x", "skipped": "s"}.get(char, char) return char in self.reportchars - def write_fspath_result(self, nodeid, res): + def write_fspath_result(self, nodeid, res, **markup): fspath = self.config.rootdir.join(nodeid.split("::")[0]) if fspath != self.currentfspath: if self.currentfspath is not None and self._show_progress_info: @@ -272,7 +272,7 @@ def write_fspath_result(self, nodeid, res): fspath = self.startdir.bestrelpath(fspath) self._tw.line() self._tw.write(fspath + " ") - self._tw.write(res) + self._tw.write(res, **markup) def write_ensure_prefix(self, prefix, extra="", **kwargs): if self.currentfspath != prefix: @@ -386,22 +386,22 @@ def pytest_runtest_logreport(self, report): # probably passed setup/teardown return running_xdist = hasattr(rep, "node") + if markup is None: + if rep.passed: + markup = {"green": True} + elif rep.failed: + markup = {"red": True} + elif rep.skipped: + markup = {"yellow": True} + else: + markup = {} if self.verbosity <= 0: if not running_xdist and self.showfspath: - self.write_fspath_result(rep.nodeid, letter) + self.write_fspath_result(rep.nodeid, letter, **markup) else: - self._tw.write(letter) + self._tw.write(letter, **markup) else: self._progress_nodeids_reported.add(rep.nodeid) - if markup is None: - if rep.passed: - markup = {"green": True} - elif rep.failed: - markup = {"red": True} - elif rep.skipped: - markup = {"yellow": True} - else: - markup = {} line = self._locationline(rep.nodeid, *rep.location) if not running_xdist: self.write_ensure_prefix(line, word, **markup) From 7bb51b8ceb1df7dad1363ddbaa9abaeb333b4522 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 20 Oct 2018 12:09:44 -0300 Subject: [PATCH 18/40] Issue a warning when a fixture named 'request' is collected Fix #611 --- changelog/611.bugfix.rst | 2 ++ src/_pytest/deprecated.py | 10 +++++++++- src/_pytest/fixtures.py | 5 ++++- testing/deprecated_test.py | 12 ++++++++++++ .../deprecated/test_fixture_named_request.py | 10 ++++++++++ 5 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 changelog/611.bugfix.rst create mode 100644 testing/example_scripts/deprecated/test_fixture_named_request.py diff --git a/changelog/611.bugfix.rst b/changelog/611.bugfix.rst new file mode 100644 index 00000000000..1b39f4aa91c --- /dev/null +++ b/changelog/611.bugfix.rst @@ -0,0 +1,2 @@ +Naming a fixture ``request`` will now raise a warning: the ``request`` fixture is internal and +should not be overwritten as it will lead to internal errors. diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 54886c9999d..e9e32616dab 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -11,7 +11,11 @@ from __future__ import absolute_import, division, print_function -from _pytest.warning_types import UnformattedWarning, RemovedInPytest4Warning +from _pytest.warning_types import ( + UnformattedWarning, + RemovedInPytest4Warning, + PytestDeprecationWarning, +) MAIN_STR_ARGS = RemovedInPytest4Warning( @@ -55,6 +59,10 @@ "See https://docs.pytest.org/en/latest/fixture.html for more information.", ) +FIXTURE_NAMED_REQUEST = PytestDeprecationWarning( + "'request' is a reserved name for fixtures and will raise an error in future versions" +) + CFG_PYTEST_SECTION = UnformattedWarning( RemovedInPytest4Warning, "[pytest] section in {filename} files is deprecated, use [tool:pytest] instead.", diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 29eda351f02..565f8d06166 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -32,7 +32,7 @@ get_real_method, _PytestWrapper, ) -from _pytest.deprecated import FIXTURE_FUNCTION_CALL +from _pytest.deprecated import FIXTURE_FUNCTION_CALL, FIXTURE_NAMED_REQUEST from _pytest.outcomes import fail, TEST_OUTCOME FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}' @@ -1029,6 +1029,9 @@ def __call__(self, function): function = wrap_function_to_warning_if_called_directly(function, self) + name = self.name or function.__name__ + if name == "request": + warnings.warn(FIXTURE_NAMED_REQUEST) function._pytestfixturefunction = self return function diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 649ebcd38c1..0942c3a9c7b 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -4,6 +4,8 @@ import pytest +pytestmark = pytest.mark.pytester_example_path("deprecated") + @pytest.mark.filterwarnings("default") def test_yield_tests_deprecation(testdir): @@ -392,3 +394,13 @@ def _makeitem(self, *k): with pytest.warns(RemovedInPytest4Warning): collector.makeitem("foo", "bar") assert collector.called + + +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/deprecated/test_fixture_named_request.py new file mode 100644 index 00000000000..75514bf8b8c --- /dev/null +++ b/testing/example_scripts/deprecated/test_fixture_named_request.py @@ -0,0 +1,10 @@ +import pytest + + +@pytest.fixture +def request(): + pass + + +def test(): + pass From b51ee48f787680a41f025118e87a37ca9388a1f3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 23 Oct 2018 17:35:52 +0200 Subject: [PATCH 19/40] minor: remove unused _shutil_rmtree_remove_writable --- src/_pytest/pathlib.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 081fce90465..8356a6f5c98 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -13,7 +13,6 @@ from os.path import expanduser, expandvars, isabs, sep from posixpath import sep as posix_sep import fnmatch -import stat from .compat import PY36 @@ -40,17 +39,10 @@ def ensure_reset_dir(path): path.mkdir() -def _shutil_rmtree_remove_writable(func, fspath, _): - "Clear the readonly bit and reattempt the removal" - os.chmod(fspath, stat.S_IWRITE) - func(fspath) - - def rmtree(path, force=False): if force: - # ignore_errors leaves dead folders around - # python needs a rm -rf as a followup - # the trick with _shutil_rmtree_remove_writable is unreliable + # NOTE: ignore_errors might leave dead folders around. + # Python needs a rm -rf as a followup. shutil.rmtree(str(path), ignore_errors=True) else: shutil.rmtree(str(path)) From f8f4c16020b8aff3fca398e9f8c6d16f8c001fed Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 24 Oct 2018 16:00:33 +0200 Subject: [PATCH 20/40] TerminalWriter: write "collecting" msg only once every 0.1s Running `pytest -k doesnotmatch` on pytest's own tests takes ~3s with Kitty terminal for me, but only ~1s with `-q`. It also is faster with urxvt, but still takes 2.2s there. This patch only calls `report_collect` every 0.1s, which is good enough for reporting collection progress, and improves the time with both Kitty and urxvt to ~1.2s for me. --- changelog/4225.feature.rst | 3 +++ src/_pytest/terminal.py | 15 +++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 changelog/4225.feature.rst diff --git a/changelog/4225.feature.rst b/changelog/4225.feature.rst new file mode 100644 index 00000000000..ffdf0e83f5c --- /dev/null +++ b/changelog/4225.feature.rst @@ -0,0 +1,3 @@ +Improve performance with collection reporting in non-quiet mode with terminals. + +The "collecting …" message is only printed/updated every 0.5s. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index d207dd78507..47d8656d7e3 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -248,6 +248,7 @@ def __init__(self, config, file=None): self.isatty = file.isatty() self._progress_nodeids_reported = set() self._show_progress_info = self._determine_show_progress_info() + self._collect_report_last_write = None def _determine_show_progress_info(self): """Return True if we should display progress information based on the current config""" @@ -474,7 +475,11 @@ def _width_of_current_line(self): return self._tw.chars_on_current_line def pytest_collection(self): - if not self.isatty and self.config.option.verbose >= 1: + if self.isatty: + if self.config.option.verbose >= 0: + self.write("collecting ... ", bold=True) + self._collect_report_last_write = time.time() + elif self.config.option.verbose >= 1: self.write("collecting ... ", bold=True) def pytest_collectreport(self, report): @@ -485,13 +490,19 @@ def pytest_collectreport(self, report): items = [x for x in report.result if isinstance(x, pytest.Item)] self._numcollected += len(items) if self.isatty: - # self.write_fspath_result(report.nodeid, 'E') self.report_collect() def report_collect(self, final=False): if self.config.option.verbose < 0: return + if not final: + # Only write "collecting" report every 0.5s. + t = time.time() + if self._collect_report_last_write > t - 0.5: + return + self._collect_report_last_write = t + errors = len(self.stats.get("error", [])) skipped = len(self.stats.get("skipped", [])) deselected = len(self.stats.get("deselected", [])) From d40cd3ec6b9afe563ad0ec14173ace2d665861d1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 24 Oct 2018 18:15:55 +0200 Subject: [PATCH 21/40] Use functools.lru_cache with _getconftest_pathlist For pytest's own suite the `cache_info()` looks as follows: > session.config._getconftest_pathlist.cache_info() CacheInfo(hits=231, misses=19, maxsize=None, currsize=19) While it does not really make a difference for me this might help with larger test suites / the case mentioned in https://github.com/pytest-dev/pytest/issues/2206#issuecomment-432623646. --- src/_pytest/config/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 88cbf14bab0..9769eb813fe 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1,6 +1,7 @@ """ command line options, ini-file and conftest.py processing. """ from __future__ import absolute_import, division, print_function import argparse +import functools import inspect import shlex import types @@ -893,6 +894,7 @@ def _getini(self, name): assert type is None return value + @functools.lru_cache(maxsize=None) def _getconftest_pathlist(self, name, path): try: mod, relroots = self.pluginmanager._rget_with_confmod(name, path) From 1786ad16a7851cd28d1f3f2ebdbbefd30bab7177 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 24 Oct 2018 18:59:54 -0300 Subject: [PATCH 22/40] functools.lru_cache does not exist on Python 2, apply for Python 3 only --- src/_pytest/config/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 9769eb813fe..1c2f921a842 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -894,7 +894,6 @@ def _getini(self, name): assert type is None return value - @functools.lru_cache(maxsize=None) def _getconftest_pathlist(self, name, path): try: mod, relroots = self.pluginmanager._rget_with_confmod(name, path) @@ -909,6 +908,10 @@ def _getconftest_pathlist(self, name, path): values.append(relroot) return values + if six.PY3: + # once we drop Python 2, please change this to use the normal decorator syntax (#4227) + _getconftest_pathlist = functools.lru_cache(maxsize=None)(_getconftest_pathlist) + def _get_override_ini_value(self, name): value = None # override_ini is a list of "ini=value" options From a4ea66cb1fcbb8a7723c764d67a817a0afdee36d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 26 Jul 2017 17:18:29 +0200 Subject: [PATCH 23/40] pdb: resume capturing after `continue` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After `pdb.set_trace()` capturing is turned off. This patch resumes it after using the `continue` (or `c` / `cont`) command. Store _pytest_capman on the class, for pdbpp's do_debug hack to keep it. Without this, `debug …` would fail like this: /usr/lib/python3.6/cmd.py:217: in onecmd return func(arg) .venv/lib/python3.6/site-packages/pdb.py:608: in do_debug return orig_do_debug(self, arg) /usr/lib/python3.6/pdb.py:1099: in do_debug sys.call_tracing(p.run, (arg, globals, locals)) /usr/lib/python3.6/bdb.py:434: in run exec(cmd, globals, locals) /usr/lib/python3.6/bdb.py:51: in trace_dispatch return self.dispatch_line(frame) /usr/lib/python3.6/bdb.py:69: in dispatch_line self.user_line(frame) /usr/lib/python3.6/pdb.py:261: in user_line self.interaction(frame, None) .venv/lib/python3.6/site-packages/pdb.py:203: in interaction self.setup(frame, traceback) E AttributeError: 'PytestPdb' object has no attribute '_pytest_capman' - add pytest_leave_pdb hook - fixes test_pdb_interaction_capturing_twice: would fail on master now, but works here --- changelog/2619.feature.rst | 1 + src/_pytest/debugging.py | 38 +++++++++++++++++++++++++++++++++++++- src/_pytest/hookspec.py | 10 ++++++++++ testing/test_pdb.py | 28 ++++++++++++++++++++++------ 4 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 changelog/2619.feature.rst diff --git a/changelog/2619.feature.rst b/changelog/2619.feature.rst new file mode 100644 index 00000000000..df8137a669c --- /dev/null +++ b/changelog/2619.feature.rst @@ -0,0 +1 @@ +Resume capturing output after ``continue`` with ``__import__("pdb").set_trace()``. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index cc9bf5c2a0f..da35688b9a7 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -96,8 +96,44 @@ def set_trace(cls, set_break=True): tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config) + + class _PdbWrapper(cls._pdb_cls, object): + _pytest_capman = capman + _continued = False + + def do_continue(self, arg): + ret = super(_PdbWrapper, self).do_continue(arg) + if self._pytest_capman: + tw = _pytest.config.create_terminal_writer(cls._config) + tw.line() + tw.sep(">", "PDB continue (IO-capturing resumed)") + self._pytest_capman.resume_global_capture() + cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config) + self._continued = True + return ret + + do_c = do_cont = do_continue + + def setup(self, f, tb): + """Suspend on setup(). + + Needed after do_continue resumed, and entering another + breakpoint again. + """ + ret = super(_PdbWrapper, self).setup(f, tb) + if not ret and self._continued: + # pdb.setup() returns True if the command wants to exit + # from the interaction: do not suspend capturing then. + if self._pytest_capman: + self._pytest_capman.suspend_global_capture(in_=True) + return ret + + _pdb = _PdbWrapper() + else: + _pdb = cls._pdb_cls() + if set_break: - cls._pdb_cls().set_trace(frame) + _pdb.set_trace(frame) class PdbInvoke(object): diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 533806964d5..ae289f0a3d0 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -609,3 +609,13 @@ def pytest_enter_pdb(config): :param _pytest.config.Config config: pytest config object """ + + +def pytest_leave_pdb(config): + """ called when leaving pdb (e.g. with continue after pdb.set_trace()). + + Can be used by plugins to take special action just after the python + debugger leaves interactive mode. + + :param _pytest.config.Config config: pytest config object + """ diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 57a6cb9a300..19f95959caa 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -158,6 +158,7 @@ def test_not_called_due_to_quit(): assert "= 1 failed in" in rest assert "def test_1" not in rest assert "Exit: Quitting debugger" in rest + assert "PDB continue (IO-capturing resumed)" not in rest self.flush(child) @staticmethod @@ -489,18 +490,23 @@ def test_1(): """ ) child = testdir.spawn_pytest(str(p1)) + child.expect(r"PDB set_trace \(IO-capturing turned off\)") child.expect("test_1") child.expect("x = 3") child.expect("Pdb") child.sendline("c") + child.expect(r"PDB continue \(IO-capturing resumed\)") + child.expect(r"PDB set_trace \(IO-capturing turned off\)") child.expect("x = 4") child.expect("Pdb") child.sendeof() + child.expect("_ test_1 _") + child.expect("def test_1") + child.expect("Captured stdout call") rest = child.read().decode("utf8") - assert "1 failed" in rest - assert "def test_1" in rest assert "hello17" in rest # out is captured assert "hello18" in rest # out is captured + assert "1 failed" in rest self.flush(child) def test_pdb_used_outside_test(self, testdir): @@ -541,15 +547,19 @@ def test_pdb_collection_failure_is_shown(self, testdir): ["E NameError: *xxx*", "*! *Exit: Quitting debugger !*"] # due to EOF ) - def test_enter_pdb_hook_is_called(self, testdir): + def test_enter_leave_pdb_hooks_are_called(self, testdir): testdir.makeconftest( """ + def pytest_configure(config): + config.testing_verification = 'configured' + def pytest_enter_pdb(config): assert config.testing_verification == 'configured' print('enter_pdb_hook') - def pytest_configure(config): - config.testing_verification = 'configured' + def pytest_leave_pdb(config): + assert config.testing_verification == 'configured' + print('leave_pdb_hook') """ ) p1 = testdir.makepyfile( @@ -558,11 +568,17 @@ def pytest_configure(config): def test_foo(): pytest.set_trace() + assert 0 """ ) child = testdir.spawn_pytest(str(p1)) child.expect("enter_pdb_hook") - child.send("c\n") + child.sendline("c") + child.expect(r"PDB continue \(IO-capturing resumed\)") + child.expect("Captured stdout call") + rest = child.read().decode("utf8") + assert "leave_pdb_hook" in rest + assert "1 failed" in rest child.sendeof() self.flush(child) From ede3a4e850e8d2d3dedbb90eca84bc80a6f6bc27 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 24 Oct 2018 23:27:14 +0200 Subject: [PATCH 24/40] pytest_{enter,leave}_pdb: pass through pdb instance --- changelog/2619.feature.rst | 3 +++ src/_pytest/debugging.py | 6 ++++-- src/_pytest/hookspec.py | 6 ++++-- testing/test_pdb.py | 14 ++++++++++++-- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/changelog/2619.feature.rst b/changelog/2619.feature.rst index df8137a669c..d2ce9c5ed38 100644 --- a/changelog/2619.feature.rst +++ b/changelog/2619.feature.rst @@ -1 +1,4 @@ Resume capturing output after ``continue`` with ``__import__("pdb").set_trace()``. + +This also adds a new ``pytest_leave_pdb`` hook, and passes in ``pdb`` to the +existing ``pytest_enter_pdb`` hook. diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index da35688b9a7..5542fef78fe 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -95,7 +95,6 @@ def set_trace(cls, set_break=True): tw = _pytest.config.create_terminal_writer(cls._config) tw.line() tw.sep(">", "PDB set_trace (IO-capturing turned off)") - cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config) class _PdbWrapper(cls._pdb_cls, object): _pytest_capman = capman @@ -108,7 +107,9 @@ def do_continue(self, arg): tw.line() tw.sep(">", "PDB continue (IO-capturing resumed)") self._pytest_capman.resume_global_capture() - cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config) + cls._pluginmanager.hook.pytest_leave_pdb( + config=cls._config, pdb=self + ) self._continued = True return ret @@ -129,6 +130,7 @@ def setup(self, f, tb): return ret _pdb = _PdbWrapper() + cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb) else: _pdb = cls._pdb_cls() diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index ae289f0a3d0..6e04557208b 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -603,19 +603,21 @@ def pytest_exception_interact(node, call, report): """ -def pytest_enter_pdb(config): +def pytest_enter_pdb(config, pdb): """ called upon pdb.set_trace(), can be used by plugins to take special action just before the python debugger enters in interactive mode. :param _pytest.config.Config config: pytest config object + :param pdb.Pdb pdb: Pdb instance """ -def pytest_leave_pdb(config): +def pytest_leave_pdb(config, pdb): """ called when leaving pdb (e.g. with continue after pdb.set_trace()). Can be used by plugins to take special action just after the python debugger leaves interactive mode. :param _pytest.config.Config config: pytest config object + :param pdb.Pdb pdb: Pdb instance """ diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 19f95959caa..3f0f744101e 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -550,16 +550,26 @@ def test_pdb_collection_failure_is_shown(self, testdir): def test_enter_leave_pdb_hooks_are_called(self, testdir): testdir.makeconftest( """ + mypdb = None + def pytest_configure(config): config.testing_verification = 'configured' - def pytest_enter_pdb(config): + def pytest_enter_pdb(config, pdb): assert config.testing_verification == 'configured' print('enter_pdb_hook') - def pytest_leave_pdb(config): + global mypdb + mypdb = pdb + mypdb.set_attribute = "bar" + + def pytest_leave_pdb(config, pdb): assert config.testing_verification == 'configured' print('leave_pdb_hook') + + global mypdb + assert mypdb is pdb + assert mypdb.set_attribute == "bar" """ ) p1 = testdir.makepyfile( From 0dc6cb298e4e35b8f4192973ffa73db0b24575d8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 25 Oct 2018 18:20:36 +0200 Subject: [PATCH 25/40] Move lru_cache wrapper to compat Ref: https://github.com/pytest-dev/pytest/pull/4227#discussion_r228060373 --- src/_pytest/compat.py | 13 +++++++++++++ src/_pytest/config/__init__.py | 7 ++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 78bf1bc040c..3c7c40070df 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -417,3 +417,16 @@ class FuncargnamesCompatAttr(object): def funcargnames(self): """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" return self.fixturenames + + +if six.PY2: + + def lru_cache(*_, **__): + def dec(fn): + return fn + + return dec + + +else: + from functools import lru_cache # noqa: F401 diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index bb5d2851f01..5df8f273454 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1,7 +1,6 @@ """ command line options, ini-file and conftest.py processing. """ from __future__ import absolute_import, division, print_function import argparse -import functools import inspect import shlex import types @@ -20,6 +19,7 @@ import _pytest.assertion from pluggy import PluginManager, HookimplMarker, HookspecMarker from _pytest._code import ExceptionInfo, filter_traceback +from _pytest.compat import lru_cache from _pytest.compat import safe_str from .exceptions import UsageError, PrintHelp from .findpaths import determine_setup, exists @@ -894,6 +894,7 @@ def _getini(self, name): assert type is None return value + @lru_cache(maxsize=None) def _getconftest_pathlist(self, name, path): try: mod, relroots = self.pluginmanager._rget_with_confmod(name, path) @@ -908,10 +909,6 @@ def _getconftest_pathlist(self, name, path): values.append(relroot) return values - if six.PY3: - # once we drop Python 2, please change this to use the normal decorator syntax (#4227) - _getconftest_pathlist = functools.lru_cache(maxsize=None)(_getconftest_pathlist) - def _get_override_ini_value(self, name): value = None # override_ini is a list of "ini=value" options From e04182364338ed9cd2a63945204701c99e297f21 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 25 Oct 2018 20:03:14 +0200 Subject: [PATCH 26/40] Move handling of duplicate files This removes the hack added in https://github.com/pytest-dev/pytest/pull/3802. Adjusts test: - it appears to not have been changed to 7 intentionally. - removes XXX comment, likely not relevant anymore since 6dac7743. --- src/_pytest/main.py | 19 ++++++++++--------- src/_pytest/python.py | 9 --------- testing/test_session.py | 2 +- 3 files changed, 11 insertions(+), 19 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 8d4176aeafe..fb9328714c7 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -278,15 +278,6 @@ def pytest_ignore_collect(path, config): if _in_venv(path) and not allow_in_venv: return True - # Skip duplicate paths. - keepduplicates = config.getoption("keepduplicates") - duplicate_paths = config.pluginmanager._duplicatepaths - if not keepduplicates: - if path in duplicate_paths: - return True - else: - duplicate_paths.add(path) - return False @@ -556,6 +547,16 @@ def _collectfile(self, path): if not self.isinitpath(path): if ihook.pytest_ignore_collect(path=path, config=self.config): return () + + # Skip duplicate paths. + keepduplicates = self.config.getoption("keepduplicates") + if not keepduplicates: + duplicate_paths = self.config.pluginmanager._duplicatepaths + if path in duplicate_paths: + return () + else: + duplicate_paths.add(path) + return ihook.pytest_collect_file(path=path, parent=self) def _recurse(self, path): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ef3e3a730b5..c7b4f073359 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -554,15 +554,6 @@ def isinitpath(self, path): return path in self.session._initialpaths def collect(self): - # XXX: HACK! - # Before starting to collect any files from this package we need - # to cleanup the duplicate paths added by the session's collect(). - # Proper fix is to not track these as duplicates in the first place. - for path in list(self.session.config.pluginmanager._duplicatepaths): - # if path.parts()[:len(self.fspath.dirpath().parts())] == self.fspath.dirpath().parts(): - if path.dirname.startswith(self.name): - self.session.config.pluginmanager._duplicatepaths.remove(path) - this_path = self.fspath.dirpath() init_module = this_path.join("__init__.py") if init_module.check(file=1) and path_matches_patterns( diff --git a/testing/test_session.py b/testing/test_session.py index 50ce91534bf..72d5cda729c 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -218,7 +218,7 @@ class TestY(TestX): started = reprec.getcalls("pytest_collectstart") finished = reprec.getreports("pytest_collectreport") assert len(started) == len(finished) - assert len(started) == 7 # XXX extra TopCollector + assert len(started) == 8 colfail = [x for x in finished if x.failed] assert len(colfail) == 1 From dcdf86ef5bdfffddbed6e5748ea83553a5294370 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 26 Oct 2018 16:06:16 +0200 Subject: [PATCH 27/40] python: collect: revisit --- src/_pytest/python.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 6b17b36fff7..c280fc0a1bd 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -560,19 +560,16 @@ def collect(self): yield Module(init_module, self) pkg_prefixes = set() for path in this_path.visit(rec=self._recurse, bf=True, sort=True): - # we will visit our own __init__.py file, in which case we skip it - skip = False - if path.basename == "__init__.py" and path.dirpath() == this_path: - continue - - for pkg_prefix in pkg_prefixes: - if ( - pkg_prefix in path.parts() - and pkg_prefix.join("__init__.py") != path - ): - skip = True + # We will visit our own __init__.py file, in which case we skip it. + if path.isfile(): + if path.basename == "__init__.py" and path.dirpath() == this_path: + continue - if skip: + parts = path.parts() + if any( + pkg_prefix in parts and pkg_prefix.join("__init__.py") != path + for pkg_prefix in pkg_prefixes + ): continue if path.isdir() and path.join("__init__.py").check(file=1): From 40228fce5a7b3aaab07870ca73998c46eb410460 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 25 Oct 2018 17:41:47 +0200 Subject: [PATCH 28/40] collection: _recurse: skip __pycache__ --- src/_pytest/main.py | 29 +++++++++++++++++++++-------- src/_pytest/python.py | 14 ++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 7e5d096a5b7..1c41f7e6e06 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -517,8 +517,19 @@ def _collect(self, arg): # Let the Package collector deal with subnodes, don't collect here. if argpath.check(dir=1): assert not names, "invalid arg %r" % (arg,) + + if six.PY2: + + def filter_(f): + return f.check(file=1) and not f.strpath.endswith("*.pyc") + + else: + + def filter_(f): + return f.check(file=1) + for path in argpath.visit( - fil=lambda x: x.check(file=1), rec=self._recurse, bf=True, sort=True + fil=filter_, rec=self._recurse, bf=True, sort=True ): pkginit = path.dirpath().join("__init__.py") if pkginit.exists() and not any(x in pkginit.parts() for x in paths): @@ -562,15 +573,17 @@ def _collectfile(self, path): return ihook.pytest_collect_file(path=path, parent=self) - def _recurse(self, path): - ihook = self.gethookproxy(path.dirpath()) - if ihook.pytest_ignore_collect(path=path, config=self.config): - return + def _recurse(self, dirpath): + if dirpath.basename == "__pycache__": + return False + ihook = self.gethookproxy(dirpath.dirpath()) + if ihook.pytest_ignore_collect(path=dirpath, config=self.config): + return False for pat in self._norecursepatterns: - if path.check(fnmatch=pat): + if dirpath.check(fnmatch=pat): return False - ihook = self.gethookproxy(path) - ihook.pytest_collect_directory(path=path, parent=self) + ihook = self.gethookproxy(dirpath) + ihook.pytest_collect_directory(path=dirpath, parent=self) return True def _tryconvertpyarg(self, x): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c280fc0a1bd..b866532cce7 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -516,15 +516,17 @@ def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): self._norecursepatterns = session._norecursepatterns self.fspath = fspath - def _recurse(self, path): - ihook = self.gethookproxy(path.dirpath()) - if ihook.pytest_ignore_collect(path=path, config=self.config): + def _recurse(self, dirpath): + if dirpath.basename == "__pycache__": return False + ihook = self.gethookproxy(dirpath.dirpath()) + if ihook.pytest_ignore_collect(path=dirpath, config=self.config): + return for pat in self._norecursepatterns: - if path.check(fnmatch=pat): + if dirpath.check(fnmatch=pat): return False - ihook = self.gethookproxy(path) - ihook.pytest_collect_directory(path=path, parent=self) + ihook = self.gethookproxy(dirpath) + ihook.pytest_collect_directory(path=dirpath, parent=self) return True def gethookproxy(self, fspath): From 1f1d4aaf6881dd40e69a0388d42b78b072ba4e8c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 30 Oct 2018 22:59:11 +0100 Subject: [PATCH 29/40] cacheprovider: display cachedir also in non-verbose mode if customized --- src/_pytest/cacheprovider.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 8e7f7a80454..a78d857f603 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -319,7 +319,8 @@ def cache(request): def pytest_report_header(config): - if config.option.verbose: + """Display cachedir with --cache-show and if non-default.""" + if config.option.verbose or config.getini("cache_dir") != ".pytest_cache": cachedir = config.cache._cachedir # TODO: evaluate generating upward relative paths # starting with .., ../.. if sensible From e0038b82f7e76b73f2555fc8b065f94513570020 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 31 Oct 2018 16:59:07 +0100 Subject: [PATCH 30/40] pdb: improve msg about output capturing with set_trace Do not display "IO-capturing turned off/on" when ``-s`` is used to avoid confusion. --- src/_pytest/capture.py | 3 +++ src/_pytest/debugging.py | 10 ++++++++-- testing/test_pdb.py | 16 ++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index bc50ccc3f08..e4187ee295b 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -102,6 +102,9 @@ def _getcapture(self, method): # Global capturing control + def is_globally_capturing(self): + return self._method != "no" + def start_global_capturing(self): assert self._global_capturing is None self._global_capturing = self._getcapture(self._method) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 1ae221aa058..94866de5633 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -80,7 +80,10 @@ def set_trace(cls, set_break=True): capman.suspend_global_capture(in_=True) tw = _pytest.config.create_terminal_writer(cls._config) tw.line() - tw.sep(">", "PDB set_trace (IO-capturing turned off)") + if capman and capman.is_globally_capturing(): + tw.sep(">", "PDB set_trace (IO-capturing turned off)") + else: + tw.sep(">", "PDB set_trace") class _PdbWrapper(cls._pdb_cls, object): _pytest_capman = capman @@ -91,7 +94,10 @@ def do_continue(self, arg): if self._pytest_capman: tw = _pytest.config.create_terminal_writer(cls._config) tw.line() - tw.sep(">", "PDB continue (IO-capturing resumed)") + if self._pytest_capman.is_globally_capturing(): + tw.sep(">", "PDB continue (IO-capturing resumed)") + else: + tw.sep(">", "PDB continue") self._pytest_capman.resume_global_capture() cls._pluginmanager.hook.pytest_leave_pdb( config=cls._config, pdb=self diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 98185ab17d4..4c236c55d52 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -518,6 +518,22 @@ def test_1(): assert "1 failed" in rest self.flush(child) + def test_pdb_without_capture(self, testdir): + p1 = testdir.makepyfile( + """ + import pytest + def test_1(): + pytest.set_trace() + """ + ) + child = testdir.spawn_pytest("-s %s" % p1) + child.expect(r">>> PDB set_trace >>>") + child.expect("Pdb") + child.sendline("c") + child.expect(r">>> PDB continue >>>") + child.expect("1 passed") + self.flush(child) + def test_pdb_used_outside_test(self, testdir): p1 = testdir.makepyfile( """ From 65817dd7975610d459467c4911b45f6bffe5352d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 31 Oct 2018 23:08:59 +0100 Subject: [PATCH 31/40] changelog [ci skip] --- changelog/4277.trivial.rst | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/4277.trivial.rst diff --git a/changelog/4277.trivial.rst b/changelog/4277.trivial.rst new file mode 100644 index 00000000000..69118788202 --- /dev/null +++ b/changelog/4277.trivial.rst @@ -0,0 +1,4 @@ +pdb: improve message about output capturing with ``set_trace``. + +Do not display "IO-capturing turned off/on" when ``-s`` is used to avoid +confusion. From ce1cc3dddb384d7f73d8409280931342e9c539e2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 24 Oct 2018 18:50:59 +0200 Subject: [PATCH 32/40] _getconftestmodules: use functools.lru_cache Also renames `_path2confmods` to `_dirpath2confmods` for clarity (it is expected to be a dirpath in `_importconftest`). Uses an explicit maxsize, since it appears to be only relevant for a short period [1]. Removes the lru_cache on _getconftest_pathlist, which makes no difference when caching _getconftestmodules, at least with the performance test of 100x10 files (#4237). 1: https://github.com/pytest-dev/pytest/pull/4237#discussion_r228528007 --- src/_pytest/config/__init__.py | 53 ++++++++++++++++++---------------- testing/test_conftest.py | 10 +++---- 2 files changed, 33 insertions(+), 30 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 806a9d63f1d..6fbf8144a8f 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -214,7 +214,7 @@ def __init__(self): self._conftest_plugins = set() # state related to local conftest plugins - self._path2confmods = {} + self._dirpath2confmods = {} self._conftestpath2mod = {} self._confcutdir = None self._noconftest = False @@ -385,31 +385,35 @@ def _try_load_conftest(self, anchor): if x.check(dir=1): self._getconftestmodules(x) + @lru_cache(maxsize=128) def _getconftestmodules(self, path): if self._noconftest: return [] - try: - return self._path2confmods[path] - except KeyError: - if path.isfile(): - directory = path.dirpath() - else: - directory = path - # XXX these days we may rather want to use config.rootdir - # and allow users to opt into looking into the rootdir parent - # directories instead of requiring to specify confcutdir - clist = [] - for parent in directory.realpath().parts(): - if self._confcutdir and self._confcutdir.relto(parent): - continue - conftestpath = parent.join("conftest.py") - if conftestpath.isfile(): - mod = self._importconftest(conftestpath) - clist.append(mod) - - self._path2confmods[path] = clist - return clist + if path.isfile(): + directory = path.dirpath() + else: + directory = path + + if six.PY2: # py2 is not using lru_cache. + try: + return self._dirpath2confmods[directory] + except KeyError: + pass + + # XXX these days we may rather want to use config.rootdir + # and allow users to opt into looking into the rootdir parent + # directories instead of requiring to specify confcutdir + clist = [] + for parent in directory.realpath().parts(): + if self._confcutdir and self._confcutdir.relto(parent): + continue + conftestpath = parent.join("conftest.py") + if conftestpath.isfile(): + mod = self._importconftest(conftestpath) + clist.append(mod) + self._dirpath2confmods[directory] = clist + return clist def _rget_with_confmod(self, name, path): modules = self._getconftestmodules(path) @@ -450,8 +454,8 @@ def _importconftest(self, conftestpath): self._conftest_plugins.add(mod) self._conftestpath2mod[conftestpath] = mod dirpath = conftestpath.dirpath() - if dirpath in self._path2confmods: - for path, mods in self._path2confmods.items(): + if dirpath in self._dirpath2confmods: + for path, mods in self._dirpath2confmods.items(): if path and path.relto(dirpath) or path == dirpath: assert mod not in mods mods.append(mod) @@ -902,7 +906,6 @@ def _getini(self, name): assert type is None return value - @lru_cache(maxsize=None) def _getconftest_pathlist(self, name, path): try: mod, relroots = self.pluginmanager._rget_with_confmod(name, path) diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 7d14d790d3f..2b66d8fa713 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -49,14 +49,14 @@ def test_basic_init(self, basedir): def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): conftest = PytestPluginManager() - len(conftest._path2confmods) + len(conftest._dirpath2confmods) conftest._getconftestmodules(basedir) - snap1 = len(conftest._path2confmods) - # assert len(conftest._path2confmods) == snap1 + 1 + snap1 = len(conftest._dirpath2confmods) + # assert len(conftest._dirpath2confmods) == snap1 + 1 conftest._getconftestmodules(basedir.join("adir")) - assert len(conftest._path2confmods) == snap1 + 1 + assert len(conftest._dirpath2confmods) == snap1 + 1 conftest._getconftestmodules(basedir.join("b")) - assert len(conftest._path2confmods) == snap1 + 2 + assert len(conftest._dirpath2confmods) == snap1 + 2 def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) From f8a2452247266c25947901073db5a1490fa9dcac Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 1 Nov 2018 15:53:25 +0100 Subject: [PATCH 33/40] changelog [ci skip] --- changelog/4272.trivial.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/4272.trivial.rst diff --git a/changelog/4272.trivial.rst b/changelog/4272.trivial.rst new file mode 100644 index 00000000000..4709f141d6d --- /dev/null +++ b/changelog/4272.trivial.rst @@ -0,0 +1 @@ +Display cachedir also in non-verbose mode if non-default. From a41820fbf0ca5ba25fc64bc0f976c3b3d4af53ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Hovm=C3=B6ller?= Date: Thu, 25 Oct 2018 15:09:14 +0200 Subject: [PATCH 34/40] collection: performance: use optimized parts function Time: 8.53s => 5.73s --- src/_pytest/main.py | 9 +++++---- src/_pytest/pathlib.py | 5 +++++ src/_pytest/python.py | 9 +++------ 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 1c41f7e6e06..dbe1ccf4277 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -18,6 +18,7 @@ from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.outcomes import exit +from _pytest.pathlib import parts from _pytest.runner import collect_one_node @@ -469,8 +470,8 @@ def _perform_collect(self, args, genitems): return items def collect(self): - for parts in self._initialparts: - arg = "::".join(map(str, parts)) + for initialpart in self._initialparts: + arg = "::".join(map(str, initialpart)) self.trace("processing argument", arg) self.trace.root.indent += 1 try: @@ -532,12 +533,12 @@ def filter_(f): fil=filter_, rec=self._recurse, bf=True, sort=True ): pkginit = path.dirpath().join("__init__.py") - if pkginit.exists() and not any(x in pkginit.parts() for x in paths): + if pkginit.exists() and not any(x in parts(pkginit.strpath) for x in paths): for x in root._collectfile(pkginit): yield x paths.append(x.fspath.dirpath()) - if not any(x in path.parts() for x in paths): + if not any(x in parts(path.strpath) for x in paths): for x in root._collectfile(path): if (type(x), x.fspath) in self._node_cache: yield self._node_cache[(type(x), x.fspath)] diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index f5c1da8c506..c907b495cff 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -303,3 +303,8 @@ def fnmatch_ex(pattern, path): else: name = six.text_type(path) return fnmatch.fnmatch(name, pattern) + + +def parts(s): + parts = s.split(sep) + return [sep.join(parts[:i+1]) or sep for i in range(len(parts))] diff --git a/src/_pytest/python.py b/src/_pytest/python.py index b866532cce7..2eb5f0b87ec 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -41,6 +41,7 @@ from _pytest.mark.structures import normalize_mark_list from _pytest.mark.structures import transfer_markers from _pytest.outcomes import fail +from _pytest.pathlib import parts from _pytest.warning_types import PytestWarning from _pytest.warning_types import RemovedInPytest4Warning @@ -562,14 +563,10 @@ def collect(self): yield Module(init_module, self) pkg_prefixes = set() for path in this_path.visit(rec=self._recurse, bf=True, sort=True): - # We will visit our own __init__.py file, in which case we skip it. - if path.isfile(): - if path.basename == "__init__.py" and path.dirpath() == this_path: - continue - parts = path.parts() + parts_ = parts(path.strpath) if any( - pkg_prefix in parts and pkg_prefix.join("__init__.py") != path + pkg_prefix in parts_ and pkg_prefix.join("__init__.py") != path for pkg_prefix in pkg_prefixes ): continue From 2b50911c9d9e7cc2c3ffa38c70bff398109718ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anders=20Hovm=C3=B6ller?= Date: Thu, 25 Oct 2018 15:16:10 +0200 Subject: [PATCH 35/40] Minor refactor for readability Time: 5.73s => 5.88s/5.82s --- src/_pytest/main.py | 11 +++++++---- src/_pytest/python.py | 4 ++++ 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index dbe1ccf4277..9a0162f0611 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -533,17 +533,20 @@ def filter_(f): fil=filter_, rec=self._recurse, bf=True, sort=True ): pkginit = path.dirpath().join("__init__.py") - if pkginit.exists() and not any(x in parts(pkginit.strpath) for x in paths): + if pkginit.exists() and not any( + x in parts(pkginit.strpath) for x in paths + ): for x in root._collectfile(pkginit): yield x paths.append(x.fspath.dirpath()) if not any(x in parts(path.strpath) for x in paths): for x in root._collectfile(path): - if (type(x), x.fspath) in self._node_cache: - yield self._node_cache[(type(x), x.fspath)] + key = (type(x), x.fspath) + if key in self._node_cache: + yield self._node_cache[key] else: - self._node_cache[(type(x), x.fspath)] = x + self._node_cache[key] = x yield x else: assert argpath.check(file=1) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2eb5f0b87ec..6b113cacd3d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -563,6 +563,10 @@ def collect(self): yield Module(init_module, self) pkg_prefixes = set() for path in this_path.visit(rec=self._recurse, bf=True, sort=True): + # We will visit our own __init__.py file, in which case we skip it. + if path.isfile(): + if path.basename == "__init__.py" and path.dirpath() == this_path: + continue parts_ = parts(path.strpath) if any( From 6ffa347c77344a57cbb99ff43d7c27b78a7b9511 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 25 Oct 2018 17:19:19 +0200 Subject: [PATCH 36/40] Handle dirs only once Time: 5.73s/5.88s => 5.36s (Before rebase: 4.86s => 4.45s) --- src/_pytest/main.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 9a0162f0611..d67468887cc 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -529,16 +529,20 @@ def filter_(f): def filter_(f): return f.check(file=1) + seen_dirs = set() for path in argpath.visit( fil=filter_, rec=self._recurse, bf=True, sort=True ): - pkginit = path.dirpath().join("__init__.py") - if pkginit.exists() and not any( - x in parts(pkginit.strpath) for x in paths - ): - for x in root._collectfile(pkginit): - yield x - paths.append(x.fspath.dirpath()) + dirpath = path.dirpath() + if dirpath not in seen_dirs: + seen_dirs.add(dirpath) + pkginit = dirpath.join("__init__.py") + if pkginit.exists() and not any( + x in parts(pkginit.strpath) for x in paths + ): + for x in root._collectfile(pkginit): + yield x + paths.append(x.fspath.dirpath()) if not any(x in parts(path.strpath) for x in paths): for x in root._collectfile(path): From 023e1c78df64e0cbdc7fa9ff8a912c56a43a033b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 25 Oct 2018 17:34:41 +0200 Subject: [PATCH 37/40] paths: use set and isdisjoint Time: 5.36s => 4.85s (before rebase: 4.45s => 3.55s) --- src/_pytest/main.py | 10 ++++------ src/_pytest/pathlib.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index d67468887cc..de07407445d 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -489,7 +489,7 @@ def _collect(self, arg): names = self._parsearg(arg) argpath = names.pop(0).realpath() - paths = [] + paths = set() root = self # Start with a Session root, and delve to argpath item (dir or file) @@ -537,14 +537,12 @@ def filter_(f): if dirpath not in seen_dirs: seen_dirs.add(dirpath) pkginit = dirpath.join("__init__.py") - if pkginit.exists() and not any( - x in parts(pkginit.strpath) for x in paths - ): + if pkginit.exists() and parts(pkginit.strpath).isdisjoint(paths): for x in root._collectfile(pkginit): yield x - paths.append(x.fspath.dirpath()) + paths.add(x.fspath.dirpath()) - if not any(x in parts(path.strpath) for x in paths): + if parts(path.strpath).isdisjoint(paths): for x in root._collectfile(path): key = (type(x), x.fspath) if key in self._node_cache: diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index c907b495cff..430e1ec1dab 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -307,4 +307,4 @@ def fnmatch_ex(pattern, path): def parts(s): parts = s.split(sep) - return [sep.join(parts[:i+1]) or sep for i in range(len(parts))] + return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} From 1ec6805112fdec201fc94d2a586c40ba8aaf57dd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 3 Nov 2018 13:48:10 +0000 Subject: [PATCH 38/40] Fix escape in code sample --- doc/en/example/multipython.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/en/example/multipython.py b/doc/en/example/multipython.py index a5360ed5a6a..7846ddb9894 100644 --- a/doc/en/example/multipython.py +++ b/doc/en/example/multipython.py @@ -33,7 +33,7 @@ def dumps(self, obj): dumpfile = self.picklefile.dirpath("dump.py") dumpfile.write( textwrap.dedent( - r"""\ + r""" import pickle f = open({!r}, 'wb') s = pickle.dump({!r}, f, protocol=2) @@ -49,7 +49,7 @@ def load_and_is_true(self, expression): loadfile = self.picklefile.dirpath("load.py") loadfile.write( textwrap.dedent( - r"""\ + r""" import pickle f = open({!r}, 'rb') obj = pickle.load(f) From c2e906ec97becc49047f6207582815cd4337c7a3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 3 Nov 2018 13:51:39 +0000 Subject: [PATCH 39/40] Preparing release version 3.10.0 --- CHANGELOG.rst | 66 ++++++++++++++++++++++++++++++ changelog/2619.feature.rst | 4 -- changelog/2701.bugfix.rst | 1 - changelog/4046.bugfix.rst | 1 - changelog/4147.feature.rst | 1 - changelog/4188.feature.rst | 1 - changelog/4225.feature.rst | 3 -- changelog/4255.doc.rst | 1 - changelog/4260.bugfix.rst | 1 - changelog/4262.bugfix.rst | 1 - changelog/4272.trivial.rst | 1 - changelog/4277.trivial.rst | 4 -- changelog/4279.trivial.rst | 1 - changelog/611.bugfix.rst | 2 - doc/en/announce/index.rst | 1 + doc/en/announce/release-3.10.0.rst | 43 +++++++++++++++++++ doc/en/cache.rst | 2 + doc/en/example/nonpython.rst | 7 ++-- doc/en/writing_plugins.rst | 14 +------ 19 files changed, 117 insertions(+), 38 deletions(-) delete mode 100644 changelog/2619.feature.rst delete mode 100644 changelog/2701.bugfix.rst delete mode 100644 changelog/4046.bugfix.rst delete mode 100644 changelog/4147.feature.rst delete mode 100644 changelog/4188.feature.rst delete mode 100644 changelog/4225.feature.rst delete mode 100644 changelog/4255.doc.rst delete mode 100644 changelog/4260.bugfix.rst delete mode 100644 changelog/4262.bugfix.rst delete mode 100644 changelog/4272.trivial.rst delete mode 100644 changelog/4277.trivial.rst delete mode 100644 changelog/4279.trivial.rst delete mode 100644 changelog/611.bugfix.rst create mode 100644 doc/en/announce/release-3.10.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6b06cbfb526..d98151b4263 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,72 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 3.10.0 (2018-11-03) +========================== + +Features +-------- + +- `#2619 `_: Resume capturing output after ``continue`` with ``__import__("pdb").set_trace()``. + + This also adds a new ``pytest_leave_pdb`` hook, and passes in ``pdb`` to the + existing ``pytest_enter_pdb`` hook. + + +- `#4147 `_: Add ``-sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation `_ for more info. + + +- `#4188 `_: Make ``--color`` emit colorful dots when not running in verbose mode. Earlier, it would only colorize the test-by-test output if ``--verbose`` was also passed. + + +- `#4225 `_: Improve performance with collection reporting in non-quiet mode with terminals. + + The "collecting …" message is only printed/updated every 0.5s. + + + +Bug Fixes +--------- + +- `#2701 `_: Fix false ``RemovedInPytest4Warning: usage of Session... is deprecated, please use pytest`` warnings. + + +- `#4046 `_: Fix problems with running tests in package ``__init__.py`` files. + + +- `#4260 `_: Swallow warnings during anonymous compilation of source. + + +- `#4262 `_: Fix access denied error when deleting stale directories created by ``tmpdir`` / ``tmp_path``. + + +- `#611 `_: Naming a fixture ``request`` will now raise a warning: the ``request`` fixture is internal and + should not be overwritten as it will lead to internal errors. + + + +Improved Documentation +---------------------- + +- `#4255 `_: Added missing documentation about the fact that module names passed to filter warnings are not regex-escaped. + + + +Trivial/Internal Changes +------------------------ + +- `#4272 `_: Display cachedir also in non-verbose mode if non-default. + + +- `#4277 `_: pdb: improve message about output capturing with ``set_trace``. + + Do not display "IO-capturing turned off/on" when ``-s`` is used to avoid + confusion. + + +- `#4279 `_: Improve message and stack level of warnings issued by ``monkeypatch.setenv`` when the value of the environment variable is not a ``str``. + + pytest 3.9.3 (2018-10-27) ========================= diff --git a/changelog/2619.feature.rst b/changelog/2619.feature.rst deleted file mode 100644 index d2ce9c5ed38..00000000000 --- a/changelog/2619.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Resume capturing output after ``continue`` with ``__import__("pdb").set_trace()``. - -This also adds a new ``pytest_leave_pdb`` hook, and passes in ``pdb`` to the -existing ``pytest_enter_pdb`` hook. diff --git a/changelog/2701.bugfix.rst b/changelog/2701.bugfix.rst deleted file mode 100644 index a942234fdb3..00000000000 --- a/changelog/2701.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix false ``RemovedInPytest4Warning: usage of Session... is deprecated, please use pytest`` warnings. diff --git a/changelog/4046.bugfix.rst b/changelog/4046.bugfix.rst deleted file mode 100644 index 2b0da70cd0e..00000000000 --- a/changelog/4046.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix problems with running tests in package ``__init__.py`` files. diff --git a/changelog/4147.feature.rst b/changelog/4147.feature.rst deleted file mode 100644 index 812898f909b..00000000000 --- a/changelog/4147.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``-sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation `_ for more info. diff --git a/changelog/4188.feature.rst b/changelog/4188.feature.rst deleted file mode 100644 index d3169efc06e..00000000000 --- a/changelog/4188.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Make ``--color`` emit colorful dots when not running in verbose mode. Earlier, it would only colorize the test-by-test output if ``--verbose`` was also passed. diff --git a/changelog/4225.feature.rst b/changelog/4225.feature.rst deleted file mode 100644 index ffdf0e83f5c..00000000000 --- a/changelog/4225.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Improve performance with collection reporting in non-quiet mode with terminals. - -The "collecting …" message is only printed/updated every 0.5s. diff --git a/changelog/4255.doc.rst b/changelog/4255.doc.rst deleted file mode 100644 index 673027cf53f..00000000000 --- a/changelog/4255.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Added missing documentation about the fact that module names passed to filter warnings are not regex-escaped. diff --git a/changelog/4260.bugfix.rst b/changelog/4260.bugfix.rst deleted file mode 100644 index e1e1a009f2d..00000000000 --- a/changelog/4260.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Swallow warnings during anonymous compilation of source. diff --git a/changelog/4262.bugfix.rst b/changelog/4262.bugfix.rst deleted file mode 100644 index 1487138b75e..00000000000 --- a/changelog/4262.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix access denied error when deleting stale directories created by ``tmpdir`` / ``tmp_path``. diff --git a/changelog/4272.trivial.rst b/changelog/4272.trivial.rst deleted file mode 100644 index 4709f141d6d..00000000000 --- a/changelog/4272.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Display cachedir also in non-verbose mode if non-default. diff --git a/changelog/4277.trivial.rst b/changelog/4277.trivial.rst deleted file mode 100644 index 69118788202..00000000000 --- a/changelog/4277.trivial.rst +++ /dev/null @@ -1,4 +0,0 @@ -pdb: improve message about output capturing with ``set_trace``. - -Do not display "IO-capturing turned off/on" when ``-s`` is used to avoid -confusion. diff --git a/changelog/4279.trivial.rst b/changelog/4279.trivial.rst deleted file mode 100644 index 9f4c4c4735b..00000000000 --- a/changelog/4279.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Improve message and stack level of warnings issued by ``monkeypatch.setenv`` when the value of the environment variable is not a ``str``. diff --git a/changelog/611.bugfix.rst b/changelog/611.bugfix.rst deleted file mode 100644 index 1b39f4aa91c..00000000000 --- a/changelog/611.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Naming a fixture ``request`` will now raise a warning: the ``request`` fixture is internal and -should not be overwritten as it will lead to internal errors. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 3d019ad80a8..8f583c5f527 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-3.10.0 release-3.9.3 release-3.9.2 release-3.9.1 diff --git a/doc/en/announce/release-3.10.0.rst b/doc/en/announce/release-3.10.0.rst new file mode 100644 index 00000000000..b53df270219 --- /dev/null +++ b/doc/en/announce/release-3.10.0.rst @@ -0,0 +1,43 @@ +pytest-3.10.0 +======================================= + +The pytest team is proud to announce the 3.10.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: + +* Anders Hovmöller +* Andreu Vallbona Plazas +* Ankit Goel +* Anthony Sottile +* Bernardo Gomes +* Brianna Laugher +* Bruno Oliveira +* Daniel Hahler +* David Szotten +* Mick Koch +* Niclas Olofsson +* Palash Chatterjee +* Ronny Pfannschmidt +* Sven-Hendrik Haase +* Ville Skyttä +* William Jamir Silva + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/cache.rst b/doc/en/cache.rst index 245edfc1b9b..4a917d45a18 100644 --- a/doc/en/cache.rst +++ b/doc/en/cache.rst @@ -244,6 +244,8 @@ You can always peek at the content of the cache using the {'test_caching.py::test_function': True} cache/nodeids contains: ['test_caching.py::test_function'] + cache/stepwise contains: + [] example/value contains: 42 diff --git a/doc/en/example/nonpython.rst b/doc/en/example/nonpython.rst index bda15065ae7..8bcb75b4385 100644 --- a/doc/en/example/nonpython.rst +++ b/doc/en/example/nonpython.rst @@ -85,8 +85,9 @@ interesting to just look at the collection tree:: rootdir: $REGENDOC_TMPDIR/nonpython, inifile: collected 2 items - - - + + + + ======================= no tests ran in 0.12 seconds ======================= diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 464f8eb0017..527a7263ab5 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -421,21 +421,9 @@ additionally it is possible to copy examples for an example folder before runnin test_example.py::test_plugin $REGENDOC_TMPDIR/test_example.py:4: PytestExperimentalApiWarning: testdir.copy_example is an experimental api that may change over time testdir.copy_example("test_example.py") - $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Class is deprecated, please use pytest.Class instead - return getattr(object, name, default) - $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.File is deprecated, please use pytest.File instead - return getattr(object, name, default) - $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Function is deprecated, please use pytest.Function instead - return getattr(object, name, default) - $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Instance is deprecated, please use pytest.Instance instead - return getattr(object, name, default) - $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Item is deprecated, please use pytest.Item instead - return getattr(object, name, default) - $PYTHON_PREFIX/lib/python3.6/site-packages/_pytest/compat.py:332: RemovedInPytest4Warning: usage of Session.Module is deprecated, please use pytest.Module instead - return getattr(object, name, default) -- Docs: https://docs.pytest.org/en/latest/warnings.html - =================== 2 passed, 7 warnings in 0.12 seconds =================== + =================== 2 passed, 1 warnings in 0.12 seconds =================== For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult From 3d88d1827b42b7f7b0e774d19ac58f02e5c67d0a Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 3 Nov 2018 19:50:19 -0300 Subject: [PATCH 40/40] Fixed linting --- CHANGELOG.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d98151b4263..eef3b42e99e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -30,7 +30,7 @@ Features existing ``pytest_enter_pdb`` hook. -- `#4147 `_: Add ``-sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation `_ for more info. +- `#4147 `_: Add ``-sw``, ``--stepwise`` as an alternative to ``--lf -x`` for stopping at the first failure, but starting the next test invocation from that test. See `the documentation `__ for more info. - `#4188 `_: Make ``--color`` emit colorful dots when not running in verbose mode. Earlier, it would only colorize the test-by-test output if ``--verbose`` was also passed. @@ -432,7 +432,7 @@ Features the standard warnings filters to manage those warnings. This introduces ``PytestWarning``, ``PytestDeprecationWarning`` and ``RemovedInPytest4Warning`` warning types as part of the public API. - Consult `the documentation `_ for more info. + Consult `the documentation `__ for more info. - `#2908 `_: ``DeprecationWarning`` and ``PendingDeprecationWarning`` are now shown by default if no other warning filter is