From 63d517645ce0ceb36ab36a778d4f33c93a2c77a8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 19 Jul 2019 02:54:54 +0200 Subject: [PATCH 001/153] doctest: handle BdbQuit Map `BdbQuit` exception to `outcomes.Exit`. This is necessary since we are not wrapping `pdb.set_trace` there, and therefore our `do_quit` is not called. --- src/_pytest/doctest.py | 4 ++++ testing/test_pdb.py | 24 +++++++++++++++++++++++- 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index cf886f906..1bd2642ae 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -1,4 +1,5 @@ """ discover and run doctests in modules and test files.""" +import bdb import inspect import platform import sys @@ -7,6 +8,7 @@ import warnings from contextlib import contextmanager import pytest +from _pytest import outcomes from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprFileLocation from _pytest._code.code import TerminalRepr @@ -155,6 +157,8 @@ def _init_runner_class(): def report_unexpected_exception(self, out, test, example, exc_info): if isinstance(exc_info[1], Skipped): raise exc_info[1] + if isinstance(exc_info[1], bdb.BdbQuit): + outcomes.exit("Quitting debugger") failure = doctest.UnexpectedException(test, example, exc_info) if self.continue_on_failure: out.append(failure) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 8d327cbb3..c31b6b0b4 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -458,7 +458,6 @@ class TestPDB: def test_pdb_interaction_doctest(self, testdir, monkeypatch): p1 = testdir.makepyfile( """ - import pytest def function_1(): ''' >>> i = 0 @@ -477,9 +476,32 @@ class TestPDB: child.sendeof() rest = child.read().decode("utf8") + assert "! _pytest.outcomes.Exit: Quitting debugger !" in rest + assert "BdbQuit" not in rest assert "1 failed" in rest self.flush(child) + def test_doctest_set_trace_quit(self, testdir, monkeypatch): + p1 = testdir.makepyfile( + """ + def function_1(): + ''' + >>> __import__('pdb').set_trace() + ''' + """ + ) + # NOTE: does not use pytest.set_trace, but Python's patched pdb, + # therefore "-s" is required. + child = testdir.spawn_pytest("--doctest-modules --pdb -s %s" % p1) + child.expect("Pdb") + child.sendline("q") + rest = child.read().decode("utf8") + + assert "! _pytest.outcomes.Exit: Quitting debugger !" in rest + assert "= no tests ran in" in rest + assert "BdbQuit" not in rest + assert "UNEXPECTED EXCEPTION" not in rest + def test_pdb_interaction_capturing_twice(self, testdir): p1 = testdir.makepyfile( """ From 07f20ccab618fbb3c594601c7135cccaf324f270 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 6 Oct 2019 15:34:23 +0200 Subject: [PATCH 002/153] Allow for "pdb" module to be rewritten --- src/_pytest/debugging.py | 10 ++++++++-- testing/acceptance_test.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 2e3d49c37..a56ad4b83 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -1,8 +1,6 @@ """ interactive debugging with PDB, the Python Debugger. """ import argparse -import pdb import sys -from doctest import UnexpectedException from _pytest import outcomes from _pytest.config import hookimpl @@ -45,6 +43,8 @@ def pytest_addoption(parser): def pytest_configure(config): + import pdb + if config.getvalue("trace"): config.pluginmanager.register(PdbTrace(), "pdbtrace") if config.getvalue("usepdb"): @@ -87,6 +87,8 @@ class pytestPDB: @classmethod def _import_pdb_cls(cls, capman): if not cls._config: + import pdb + # Happens when using pytest.set_trace outside of a test. return pdb.Pdb @@ -113,6 +115,8 @@ class pytestPDB: "--pdbcls: could not import {!r}: {}".format(value, exc) ) else: + import pdb + pdb_cls = pdb.Pdb wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman) @@ -313,6 +317,8 @@ def _enter_pdb(node, excinfo, rep): def _postmortem_traceback(excinfo): + from doctest import UnexpectedException + if isinstance(excinfo.value, UnexpectedException): # A doctest.UnexpectedException is not useful for post_mortem. # Use the underlying exception instead: diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ad9c37737..1b11a8af0 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1238,3 +1238,40 @@ def test_warn_on_async_gen_function(testdir): assert ( result.stdout.str().count("async def functions are not natively supported") == 1 ) + + +def test_pdb_can_be_rewritten(testdir): + testdir.makepyfile( + **{ + "conftest.py": """ + import pytest + pytest.register_assert_rewrite("pdb") + """, + "__init__.py": "", + "pdb.py": """ + def check(): + assert 1 == 2 + """, + "test_pdb.py": """ + def test(): + import pdb + assert pdb.check() + """, + } + ) + # Disable debugging plugin itself to avoid: + # > INTERNALERROR> AttributeError: module 'pdb' has no attribute 'set_trace' + result = testdir.runpytest_subprocess("-p", "no:debugging", "-vv") + result.stdout.fnmatch_lines( + [ + " def check():", + "> assert 1 == 2", + "E assert 1 == 2", + "E -1", + "E +2", + "", + "pdb.py:2: AssertionError", + "*= 1 failed in *", + ] + ) + assert result.ret == 1 From 0c18e244334d1c04fa2a7942fe5e2eb179ba6915 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 5 Oct 2019 12:17:20 -0300 Subject: [PATCH 003/153] Introduce no_fnmatch_line/no_re_match_line in pytester The current idiom is to use: assert re.match(pat, result.stdout.str()) Or assert line in result.stdout.str() But this does not really give good results when it fails. Those new functions produce similar output to ther other match lines functions. --- changelog/5914.feature.rst | 19 ++++++++++++++++ src/_pytest/pytester.py | 43 +++++++++++++++++++++++++++++++---- testing/test_pytester.py | 46 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 changelog/5914.feature.rst diff --git a/changelog/5914.feature.rst b/changelog/5914.feature.rst new file mode 100644 index 000000000..68cd66f99 --- /dev/null +++ b/changelog/5914.feature.rst @@ -0,0 +1,19 @@ +``pytester`` learned two new functions, `no_fnmatch_line `_ and +`no_re_match_line `_. + +The functions are used to ensure the captured text *does not* match the given +pattern. + +The previous idiom was to use ``re.match``: + +.. code-block:: python + + assert re.match(pat, result.stdout.str()) is None + +Or the ``in`` operator: + +.. code-block:: python + + assert text in result.stdout.str() + +But the new functions produce best output on failure. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 0f3460741..a050dad09 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1318,8 +1318,7 @@ class LineMatcher: The argument is a list of lines which have to match and can use glob wildcards. If they do not match a pytest.fail() is called. The - matches and non-matches are also printed on stdout. - + matches and non-matches are also shown as part of the error message. """ __tracebackhide__ = True self._match_lines(lines2, fnmatch, "fnmatch") @@ -1330,8 +1329,7 @@ class LineMatcher: The argument is a list of lines which have to match using ``re.match``. If they do not match a pytest.fail() is called. - The matches and non-matches are also printed on stdout. - + The matches and non-matches are also shown as part of the error message. """ __tracebackhide__ = True self._match_lines(lines2, lambda name, pat: re.match(pat, name), "re.match") @@ -1374,3 +1372,40 @@ class LineMatcher: else: self._log("remains unmatched: {!r}".format(line)) pytest.fail(self._log_text) + + def no_fnmatch_line(self, pat): + """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. + + :param str pat: the pattern to match lines. + """ + __tracebackhide__ = True + self._no_match_line(pat, fnmatch, "fnmatch") + + def no_re_match_line(self, pat): + """Ensure captured lines do not match the given pattern, using ``re.match``. + + :param str pat: the regular expression to match lines. + """ + __tracebackhide__ = True + self._no_match_line(pat, lambda name, pat: re.match(pat, name), "re.match") + + def _no_match_line(self, pat, match_func, match_nickname): + """Ensure captured lines does not have a the given pattern, using ``fnmatch.fnmatch`` + + :param str pat: the pattern to match lines + """ + __tracebackhide__ = True + nomatch_printed = False + try: + for line in self.lines: + if match_func(line, pat): + self._log("%s:" % match_nickname, repr(pat)) + self._log(" with:", repr(line)) + pytest.fail(self._log_text) + else: + if not nomatch_printed: + self._log("nomatch:", repr(pat)) + nomatch_printed = True + self._log(" and:", repr(line)) + finally: + self._log_output = [] diff --git a/testing/test_pytester.py b/testing/test_pytester.py index d330ff253..f8b0896c5 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -457,6 +457,52 @@ def test_linematcher_with_nonlist(): assert lm._getlines(set()) == set() +@pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"]) +def test_no_matching(function): + """""" + if function == "no_fnmatch_line": + match_func_name = "fnmatch" + good_pattern = "*.py OK*" + bad_pattern = "*X.py OK*" + else: + assert function == "no_re_match_line" + match_func_name = "re.match" + good_pattern = r".*py OK" + bad_pattern = r".*Xpy OK" + + lm = LineMatcher( + [ + "cachedir: .pytest_cache", + "collecting ... collected 1 item", + "", + "show_fixtures_per_test.py OK", + "=== elapsed 1s ===", + ] + ) + + def check_failure_lines(lines): + expected = [ + "nomatch: '{}'".format(good_pattern), + " and: 'cachedir: .pytest_cache'", + " and: 'collecting ... collected 1 item'", + " and: ''", + "{}: '{}'".format(match_func_name, good_pattern), + " with: 'show_fixtures_per_test.py OK'", + ] + assert lines == expected + + # check the function twice to ensure we don't accumulate the internal buffer + for i in range(2): + with pytest.raises(pytest.fail.Exception) as e: + func = getattr(lm, function) + func(good_pattern) + obtained = str(e.value).splitlines() + check_failure_lines(obtained) + + func = getattr(lm, function) + func(bad_pattern) # bad pattern does not match any line: passes + + def test_pytester_addopts(request, monkeypatch): monkeypatch.setenv("PYTEST_ADDOPTS", "--orig-unused") From 47c2091ecd2f341e10f38f1d505c21fb3323c140 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 5 Oct 2019 14:18:51 -0300 Subject: [PATCH 004/153] Use new no-match functions to replace previous idiom --- testing/acceptance_test.py | 6 +++--- testing/code/test_excinfo.py | 5 +++-- testing/logging/test_fixture.py | 2 +- testing/logging/test_reporting.py | 16 ++++++++-------- testing/python/collect.py | 2 +- testing/python/fixtures.py | 6 +++--- testing/python/setup_only.py | 2 +- testing/python/show_fixtures_per_test.py | 4 ++-- testing/test_assertion.py | 4 ++-- testing/test_assertrewrite.py | 6 +++--- testing/test_cacheprovider.py | 6 +++--- testing/test_capture.py | 14 +++++++------- testing/test_collection.py | 12 ++++++------ testing/test_conftest.py | 4 ++-- testing/test_doctest.py | 8 ++++---- testing/test_junitxml.py | 6 +++--- testing/test_runner.py | 12 ++++++------ testing/test_runner_xunit.py | 2 +- testing/test_skipping.py | 2 +- testing/test_terminal.py | 24 ++++++++++++------------ testing/test_unittest.py | 10 +++++----- 21 files changed, 77 insertions(+), 76 deletions(-) diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index ad9c37737..c82699651 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -246,7 +246,7 @@ class TestGeneralUsage: ) result = testdir.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED - assert "should not be seen" not in result.stdout.str() + result.stdout.no_fnmatch_line("*should not be seen*") assert "stderr42" not in result.stderr.str() def test_conftest_printing_shows_if_error(self, testdir): @@ -954,7 +954,7 @@ class TestDurations: result.stdout.fnmatch_lines(["*Interrupted: 1 errors during collection*"]) # Collection errors abort test execution, therefore no duration is # output - assert "duration" not in result.stdout.str() + result.stdout.no_fnmatch_line("*duration*") def test_with_not(self, testdir): testdir.makepyfile(self.source) @@ -1008,7 +1008,7 @@ def test_zipimport_hook(testdir, tmpdir): result = testdir.runpython(target) assert result.ret == 0 result.stderr.fnmatch_lines(["*not found*foo*"]) - assert "INTERNALERROR>" not in result.stdout.str() + result.stdout.no_fnmatch_line("*INTERNALERROR>*") def test_import_plugin_unicode_name(testdir): diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 5673b811b..e2f06a0a2 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -399,7 +399,7 @@ def test_match_raises_error(testdir): result = testdir.runpytest() assert result.ret != 0 result.stdout.fnmatch_lines(["*AssertionError*Pattern*[123]*not found*"]) - assert "__tracebackhide__ = True" not in result.stdout.str() + result.stdout.no_fnmatch_line("*__tracebackhide__ = True*") result = testdir.runpytest("--fulltrace") assert result.ret != 0 @@ -1343,7 +1343,8 @@ def test_cwd_deleted(testdir): ) result = testdir.runpytest() result.stdout.fnmatch_lines(["* 1 failed in *"]) - assert "INTERNALERROR" not in result.stdout.str() + result.stderr.str() + result.stdout.no_fnmatch_line("*INTERNALERROR*") + result.stderr.no_fnmatch_line("*INTERNALERROR*") @pytest.mark.usefixtures("limited_recursion_depth") diff --git a/testing/logging/test_fixture.py b/testing/logging/test_fixture.py index 5d2ff4654..c68866bef 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -46,7 +46,7 @@ def test_change_level_undo(testdir): ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*log from test1*", "*2 failed in *"]) - assert "log from test2" not in result.stdout.str() + result.stdout.no_fnmatch_line("*log from test2*") def test_with_statement(caplog): diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 1ae0bd783..5b24ef963 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -109,7 +109,7 @@ def test_log_cli_level_log_level_interaction(testdir): "=* 1 failed in *=", ] ) - assert "DEBUG" not in result.stdout.str() + result.stdout.no_re_match_line("DEBUG") def test_setup_logging(testdir): @@ -282,7 +282,7 @@ def test_log_cli_default_level(testdir): "WARNING*test_log_cli_default_level.py* message will be shown*", ] ) - assert "INFO message won't be shown" not in result.stdout.str() + result.stdout.no_fnmatch_line("*INFO message won't be shown*") # make sure that that we get a '0' exit code for the testsuite assert result.ret == 0 @@ -566,7 +566,7 @@ def test_log_cli_level(testdir): "PASSED", # 'PASSED' on its own line because the log message prints a new line ] ) - assert "This log message won't be shown" not in result.stdout.str() + result.stdout.no_fnmatch_line("*This log message won't be shown*") # make sure that that we get a '0' exit code for the testsuite assert result.ret == 0 @@ -580,7 +580,7 @@ def test_log_cli_level(testdir): "PASSED", # 'PASSED' on its own line because the log message prints a new line ] ) - assert "This log message won't be shown" not in result.stdout.str() + result.stdout.no_fnmatch_line("*This log message won't be shown*") # make sure that that we get a '0' exit code for the testsuite assert result.ret == 0 @@ -616,7 +616,7 @@ def test_log_cli_ini_level(testdir): "PASSED", # 'PASSED' on its own line because the log message prints a new line ] ) - assert "This log message won't be shown" not in result.stdout.str() + result.stdout.no_fnmatch_line("*This log message won't be shown*") # make sure that that we get a '0' exit code for the testsuite assert result.ret == 0 @@ -942,7 +942,7 @@ def test_collection_collect_only_live_logging(testdir, verbose): ] ) elif verbose == "-q": - assert "collected 1 item*" not in result.stdout.str() + result.stdout.no_fnmatch_line("*collected 1 item**") expected_lines.extend( [ "*test_collection_collect_only_live_logging.py::test_simple*", @@ -950,7 +950,7 @@ def test_collection_collect_only_live_logging(testdir, verbose): ] ) elif verbose == "-qq": - assert "collected 1 item*" not in result.stdout.str() + result.stdout.no_fnmatch_line("*collected 1 item**") expected_lines.extend(["*test_collection_collect_only_live_logging.py: 1*"]) result.stdout.fnmatch_lines(expected_lines) @@ -983,7 +983,7 @@ def test_collection_logging_to_file(testdir): result = testdir.runpytest() - assert "--- live log collection ---" not in result.stdout.str() + result.stdout.no_fnmatch_line("*--- live log collection ---*") assert result.ret == 0 assert os.path.isfile(log_file) diff --git a/testing/python/collect.py b/testing/python/collect.py index e6dd3e870..8fc882f88 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1139,7 +1139,7 @@ def test_unorderable_types(testdir): """ ) result = testdir.runpytest() - assert "TypeError" not in result.stdout.str() + result.stdout.no_fnmatch_line("*TypeError*") assert result.ret == ExitCode.NO_TESTS_COLLECTED diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index f4dbfdf09..7bacfb6e2 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -455,7 +455,7 @@ class TestFillFixtures: "*1 error*", ] ) - assert "INTERNAL" not in result.stdout.str() + result.stdout.no_fnmatch_line("*INTERNAL*") def test_fixture_excinfo_leak(self, testdir): # on python2 sys.excinfo would leak into fixture executions @@ -2647,7 +2647,7 @@ class TestFixtureMarker: *3 passed* """ ) - assert "error" not in result.stdout.str() + result.stdout.no_fnmatch_line("*error*") def test_fixture_finalizer(self, testdir): testdir.makeconftest( @@ -3151,7 +3151,7 @@ class TestShowFixtures: *hello world* """ ) - assert "arg0" not in result.stdout.str() + result.stdout.no_fnmatch_line("*arg0*") @pytest.mark.parametrize("testmod", [True, False]) def test_show_fixtures_conftest(self, testdir, testmod): diff --git a/testing/python/setup_only.py b/testing/python/setup_only.py index 7c871a9ee..6343991ae 100644 --- a/testing/python/setup_only.py +++ b/testing/python/setup_only.py @@ -27,7 +27,7 @@ def test_show_only_active_fixtures(testdir, mode, dummy_yaml_custom_test): result.stdout.fnmatch_lines( ["*SETUP F arg1*", "*test_arg1 (fixtures used: arg1)*", "*TEARDOWN F arg1*"] ) - assert "_arg0" not in result.stdout.str() + result.stdout.no_fnmatch_line("*_arg0*") def test_show_different_scopes(testdir, mode): diff --git a/testing/python/show_fixtures_per_test.py b/testing/python/show_fixtures_per_test.py index aff8aa0e5..ef841819d 100644 --- a/testing/python/show_fixtures_per_test.py +++ b/testing/python/show_fixtures_per_test.py @@ -1,6 +1,6 @@ def test_no_items_should_not_show_output(testdir): result = testdir.runpytest("--fixtures-per-test") - assert "fixtures used by" not in result.stdout.str() + result.stdout.no_fnmatch_line("*fixtures used by*") assert result.ret == 0 @@ -30,7 +30,7 @@ def test_fixtures_in_module(testdir): " arg1 docstring", ] ) - assert "_arg0" not in result.stdout.str() + result.stdout.no_fnmatch_line("*_arg0*") def test_fixtures_in_conftest(testdir): diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 8fce5e279..56729d28a 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1034,7 +1034,7 @@ def test_assertion_options(testdir): result = testdir.runpytest() assert "3 == 4" in result.stdout.str() result = testdir.runpytest_subprocess("--assert=plain") - assert "3 == 4" not in result.stdout.str() + result.stdout.no_fnmatch_line("*3 == 4*") def test_triple_quoted_string_issue113(testdir): @@ -1046,7 +1046,7 @@ def test_triple_quoted_string_issue113(testdir): ) result = testdir.runpytest("--fulltrace") result.stdout.fnmatch_lines(["*1 failed*"]) - assert "SyntaxError" not in result.stdout.str() + result.stdout.no_fnmatch_line("*SyntaxError*") def test_traceback_failure(testdir): diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 89b23a72c..470c54145 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -914,7 +914,7 @@ def test_rewritten(): testdir.chdir() result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(["*= 1 passed in *=*"]) - assert "pytest-warning summary" not in result.stdout.str() + result.stdout.no_fnmatch_line("*pytest-warning summary*") def test_rewrite_warning_using_pytest_plugins_env_var(self, testdir, monkeypatch): monkeypatch.setenv("PYTEST_PLUGINS", "plugin") @@ -932,7 +932,7 @@ def test_rewritten(): testdir.chdir() result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(["*= 1 passed in *=*"]) - assert "pytest-warning summary" not in result.stdout.str() + result.stdout.no_fnmatch_line("*pytest-warning summary*") class TestAssertionRewriteHookDetails: @@ -1124,7 +1124,7 @@ def test_issue731(testdir): """ ) result = testdir.runpytest() - assert "unbalanced braces" not in result.stdout.str() + result.stdout.no_fnmatch_line("*unbalanced braces*") class TestIssue925: diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index cbba27e5f..e2fd5a4ca 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -327,7 +327,7 @@ class TestLastFailed: result = testdir.runpytest("--lf", "--ff") # Test order will be failing tests firs result.stdout.fnmatch_lines(["test_b.py*"]) - assert "test_a.py" not in result.stdout.str() + result.stdout.no_fnmatch_line("*test_a.py*") def test_lastfailed_difference_invocations(self, testdir, monkeypatch): monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") @@ -660,11 +660,11 @@ class TestLastFailed: if quiet: args.append("-q") result = testdir.runpytest(*args) - assert "run all" not in result.stdout.str() + result.stdout.no_fnmatch_line("*run all*") result = testdir.runpytest(*args) if quiet: - assert "run all" not in result.stdout.str() + result.stdout.no_fnmatch_line("*run all*") else: assert "rerun previous" in result.stdout.str() diff --git a/testing/test_capture.py b/testing/test_capture.py index f5b193597..0f7db4b8e 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -609,12 +609,12 @@ class TestCaptureFixture: *while capture is disabled* """ ) - assert "captured before" not in result.stdout.str() - assert "captured after" not in result.stdout.str() + result.stdout.no_fnmatch_line("*captured before*") + result.stdout.no_fnmatch_line("*captured after*") if no_capture: assert "test_normal executed" in result.stdout.str() else: - assert "test_normal executed" not in result.stdout.str() + result.stdout.no_fnmatch_line("*test_normal executed*") @pytest.mark.parametrize("fixture", ["capsys", "capfd"]) def test_fixture_use_by_other_fixtures(self, testdir, fixture): @@ -650,8 +650,8 @@ class TestCaptureFixture: ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 passed*"]) - assert "stdout contents begin" not in result.stdout.str() - assert "stderr contents begin" not in result.stdout.str() + result.stdout.no_fnmatch_line("*stdout contents begin*") + result.stdout.no_fnmatch_line("*stderr contents begin*") @pytest.mark.parametrize("cap", ["capsys", "capfd"]) def test_fixture_use_by_other_fixtures_teardown(self, testdir, cap): @@ -721,7 +721,7 @@ def test_capture_conftest_runtest_setup(testdir): testdir.makepyfile("def test_func(): pass") result = testdir.runpytest() assert result.ret == 0 - assert "hello19" not in result.stdout.str() + result.stdout.no_fnmatch_line("*hello19*") def test_capture_badoutput_issue412(testdir): @@ -1388,7 +1388,7 @@ def test_crash_on_closing_tmpfile_py27(testdir): result = testdir.runpytest_subprocess(str(p)) assert result.ret == 0 assert result.stderr.str() == "" - assert "IOError" not in result.stdout.str() + result.stdout.no_fnmatch_line("*IOError*") def test_pickling_and_unpickling_encoded_file(): diff --git a/testing/test_collection.py b/testing/test_collection.py index dee07d5c7..7a5cf795b 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -139,7 +139,7 @@ class TestCollectFS: # by default, ignore tests inside a virtualenv result = testdir.runpytest() - assert "test_invenv" not in result.stdout.str() + result.stdout.no_fnmatch_line("*test_invenv*") # allow test collection if user insists result = testdir.runpytest("--collect-in-virtualenv") assert "test_invenv" in result.stdout.str() @@ -165,7 +165,7 @@ class TestCollectFS: testfile = testdir.tmpdir.ensure(".virtual", "test_invenv.py") testfile.write("def test_hello(): pass") result = testdir.runpytest("--collect-in-virtualenv") - assert "test_invenv" not in result.stdout.str() + result.stdout.no_fnmatch_line("*test_invenv*") # ...unless the virtualenv is explicitly given on the CLI result = testdir.runpytest("--collect-in-virtualenv", ".virtual") assert "test_invenv" in result.stdout.str() @@ -364,7 +364,7 @@ class TestCustomConftests: testdir.makepyfile(test_world="def test_hello(): pass") result = testdir.runpytest() assert result.ret == ExitCode.NO_TESTS_COLLECTED - assert "passed" not in result.stdout.str() + result.stdout.no_fnmatch_line("*passed*") result = testdir.runpytest("--XX") assert result.ret == 0 assert "passed" in result.stdout.str() @@ -857,7 +857,7 @@ def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir): ["*ERROR collecting test_02_import_error.py*", "*No module named *asdfa*"] ) - assert "test_03" not in res.stdout.str() + res.stdout.no_fnmatch_line("*test_03*") def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir): @@ -996,12 +996,12 @@ def test_collect_init_tests(testdir): result.stdout.fnmatch_lines( ["", " ", " "] ) - assert "test_init" not in result.stdout.str() + result.stdout.no_fnmatch_line("*test_init*") result = testdir.runpytest("./tests/__init__.py", "--collect-only") result.stdout.fnmatch_lines( ["", " ", " "] ) - assert "test_foo" not in result.stdout.str() + result.stdout.no_fnmatch_line("*test_foo*") def test_collect_invalid_signature_message(testdir): diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 3f08ee381..0374db0b3 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -187,7 +187,7 @@ def test_conftest_confcutdir(testdir): ) result = testdir.runpytest("-h", "--confcutdir=%s" % x, x) result.stdout.fnmatch_lines(["*--xyz*"]) - assert "warning: could not load initial" not in result.stdout.str() + result.stdout.no_fnmatch_line("*warning: could not load initial*") @pytest.mark.skipif( @@ -648,5 +648,5 @@ def test_required_option_help(testdir): ) ) result = testdir.runpytest("-h", x) - assert "argument --xyz is required" not in result.stdout.str() + result.stdout.no_fnmatch_line("*argument --xyz is required*") assert "general:" in result.stdout.str() diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 4aac5432d..755f26286 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -239,8 +239,8 @@ class TestDoctests: ] ) # lines below should be trimmed out - assert "text-line-2" not in result.stdout.str() - assert "text-line-after" not in result.stdout.str() + result.stdout.no_fnmatch_line("*text-line-2*") + result.stdout.no_fnmatch_line("*text-line-after*") def test_docstring_full_context_around_error(self, testdir): """Test that we show the whole context before the actual line of a failing @@ -1177,7 +1177,7 @@ class TestDoctestAutoUseFixtures: """ ) result = testdir.runpytest("--doctest-modules") - assert "FAILURES" not in str(result.stdout.str()) + result.stdout.no_fnmatch_line("*FAILURES*") result.stdout.fnmatch_lines(["*=== 1 passed in *"]) @pytest.mark.parametrize("scope", SCOPES) @@ -1209,7 +1209,7 @@ class TestDoctestAutoUseFixtures: """ ) result = testdir.runpytest("--doctest-modules") - assert "FAILURES" not in str(result.stdout.str()) + str(result.stdout.no_fnmatch_line("*FAILURES*")) result.stdout.fnmatch_lines(["*=== 1 passed in *"]) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index d4a1f6cc3..06a033489 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1216,7 +1216,7 @@ def test_runs_twice(testdir, run_and_parse): ) result, dom = run_and_parse(f, f) - assert "INTERNALERROR" not in result.stdout.str() + result.stdout.no_fnmatch_line("*INTERNALERROR*") first, second = [x["classname"] for x in dom.find_by_tag("testcase")] assert first == second @@ -1231,7 +1231,7 @@ def test_runs_twice_xdist(testdir, run_and_parse): ) result, dom = run_and_parse(f, "--dist", "each", "--tx", "2*popen") - assert "INTERNALERROR" not in result.stdout.str() + result.stdout.no_fnmatch_line("*INTERNALERROR*") first, second = [x["classname"] for x in dom.find_by_tag("testcase")] assert first == second @@ -1271,7 +1271,7 @@ def test_fancy_items_regression(testdir, run_and_parse): result, dom = run_and_parse() - assert "INTERNALERROR" not in result.stdout.str() + result.stdout.no_fnmatch_line("*INTERNALERROR*") items = sorted("%(classname)s %(name)s" % x for x in dom.find_by_tag("testcase")) import pprint diff --git a/testing/test_runner.py b/testing/test_runner.py index 82e413518..9920d2b3e 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -615,7 +615,7 @@ def test_pytest_fail_notrace_runtest(testdir): ) result = testdir.runpytest() result.stdout.fnmatch_lines(["world", "hello"]) - assert "def teardown_function" not in result.stdout.str() + result.stdout.no_fnmatch_line("*def teardown_function*") def test_pytest_fail_notrace_collection(testdir): @@ -630,7 +630,7 @@ def test_pytest_fail_notrace_collection(testdir): ) result = testdir.runpytest() result.stdout.fnmatch_lines(["hello"]) - assert "def some_internal_function()" not in result.stdout.str() + result.stdout.no_fnmatch_line("*def some_internal_function()*") def test_pytest_fail_notrace_non_ascii(testdir): @@ -648,7 +648,7 @@ def test_pytest_fail_notrace_non_ascii(testdir): ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*test_hello*", "oh oh: ☺"]) - assert "def test_hello" not in result.stdout.str() + result.stdout.no_fnmatch_line("*def test_hello*") def test_pytest_no_tests_collected_exit_status(testdir): @@ -813,7 +813,7 @@ def test_failure_in_setup(testdir): """ ) result = testdir.runpytest("--tb=line") - assert "def setup_module" not in result.stdout.str() + result.stdout.no_fnmatch_line("*def setup_module*") def test_makereport_getsource(testdir): @@ -825,7 +825,7 @@ def test_makereport_getsource(testdir): """ ) result = testdir.runpytest() - assert "INTERNALERROR" not in result.stdout.str() + result.stdout.no_fnmatch_line("*INTERNALERROR*") result.stdout.fnmatch_lines(["*else: assert False*"]) @@ -856,7 +856,7 @@ def test_makereport_getsource_dynamic_code(testdir, monkeypatch): """ ) result = testdir.runpytest("-vv") - assert "INTERNALERROR" not in result.stdout.str() + result.stdout.no_fnmatch_line("*INTERNALERROR*") result.stdout.fnmatch_lines(["*test_fix*", "*fixture*'missing'*not found*"]) diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index 34a086551..1e63bbf49 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -237,7 +237,7 @@ def test_setup_funcarg_setup_when_outer_scope_fails(testdir): "*2 error*", ] ) - assert "xyz43" not in result.stdout.str() + result.stdout.no_fnmatch_line("*xyz43*") @pytest.mark.parametrize("arg", ["", "arg"]) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 8bba479f1..51b1bbdd6 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -949,7 +949,7 @@ def test_xfail_test_setup_exception(testdir): result = testdir.runpytest(p) assert result.ret == 0 assert "xfailed" in result.stdout.str() - assert "xpassed" not in result.stdout.str() + result.stdout.no_fnmatch_line("*xpassed*") def test_imperativeskip_on_xfail_test(testdir): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 88f96f894..3bdabc5de 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -204,7 +204,7 @@ class TestTerminal: result = testdir.runpytest("-vv") assert result.ret == 0 result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"]) - assert " <- " not in result.stdout.str() + result.stdout.no_fnmatch_line("* <- *") def test_keyboard_interrupt(self, testdir, option): testdir.makepyfile( @@ -559,7 +559,7 @@ class TestTerminalFunctional: "*= 2 passed, 1 deselected in * =*", ] ) - assert "= 1 deselected =" not in result.stdout.str() + result.stdout.no_fnmatch_line("*= 1 deselected =*") assert result.ret == 0 def test_no_skip_summary_if_failure(self, testdir): @@ -759,7 +759,7 @@ def test_fail_extra_reporting(testdir, monkeypatch): monkeypatch.setenv("COLUMNS", "80") testdir.makepyfile("def test_this(): assert 0, 'this_failed' * 100") result = testdir.runpytest() - assert "short test summary" not in result.stdout.str() + result.stdout.no_fnmatch_line("*short test summary*") result = testdir.runpytest("-rf") result.stdout.fnmatch_lines( [ @@ -772,13 +772,13 @@ def test_fail_extra_reporting(testdir, monkeypatch): def test_fail_reporting_on_pass(testdir): testdir.makepyfile("def test_this(): assert 1") result = testdir.runpytest("-rf") - assert "short test summary" not in result.stdout.str() + result.stdout.no_fnmatch_line("*short test summary*") def test_pass_extra_reporting(testdir): testdir.makepyfile("def test_this(): assert 1") result = testdir.runpytest() - assert "short test summary" not in result.stdout.str() + result.stdout.no_fnmatch_line("*short test summary*") result = testdir.runpytest("-rp") result.stdout.fnmatch_lines(["*test summary*", "PASS*test_pass_extra_reporting*"]) @@ -786,7 +786,7 @@ def test_pass_extra_reporting(testdir): def test_pass_reporting_on_fail(testdir): testdir.makepyfile("def test_this(): assert 0") result = testdir.runpytest("-rp") - assert "short test summary" not in result.stdout.str() + result.stdout.no_fnmatch_line("*short test summary*") def test_pass_output_reporting(testdir): @@ -829,7 +829,7 @@ def test_color_no(testdir): testdir.makepyfile("def test_this(): assert 1") result = testdir.runpytest("--color=no") assert "test session starts" in result.stdout.str() - assert "\x1b[1m" not in result.stdout.str() + result.stdout.no_fnmatch_line("*\x1b[1m*") @pytest.mark.parametrize("verbose", [True, False]) @@ -851,7 +851,7 @@ def test_color_yes_collection_on_non_atty(testdir, verbose): result = testdir.runpytest(*args) assert "test session starts" in result.stdout.str() assert "\x1b[1m" in result.stdout.str() - assert "collecting 10 items" not in result.stdout.str() + result.stdout.no_fnmatch_line("*collecting 10 items*") if verbose: assert "collecting ..." in result.stdout.str() assert "collected 10 items" in result.stdout.str() @@ -1214,7 +1214,7 @@ def test_terminal_summary_warnings_are_displayed(testdir): "*== 1 failed, 2 warnings in *", ] ) - assert "None" not in result.stdout.str() + result.stdout.no_fnmatch_line("*None*") stdout = result.stdout.str() assert stdout.count("warning_from_test") == 1 assert stdout.count("=== warnings summary ") == 2 @@ -1239,7 +1239,7 @@ def test_terminal_summary_warnings_header_once(testdir): "*== 1 failed, 1 warnings in *", ] ) - assert "None" not in result.stdout.str() + result.stdout.no_fnmatch_line("*None*") stdout = result.stdout.str() assert stdout.count("warning_from_test") == 1 assert stdout.count("=== warnings summary ") == 1 @@ -1402,7 +1402,7 @@ class TestProgressOutputStyle: """ ) output = testdir.runpytest() - assert "ZeroDivisionError" not in output.stdout.str() + output.stdout.no_fnmatch_line("*ZeroDivisionError*") output.stdout.fnmatch_lines(["=* 2 passed in *="]) def test_normal(self, many_tests_files, testdir): @@ -1494,7 +1494,7 @@ class TestProgressOutputStyle: ) output = testdir.runpytest("--capture=no") - assert "%]" not in output.stdout.str() + output.stdout.no_fnmatch_line("*%]*") class TestProgressWithTeardown: diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 9b1b688ff..281c85281 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -270,7 +270,7 @@ def test_setup_failure_is_shown(testdir): result = testdir.runpytest("-s") assert result.ret == 1 result.stdout.fnmatch_lines(["*setUp*", "*assert 0*down1*", "*1 failed*"]) - assert "never42" not in result.stdout.str() + result.stdout.no_fnmatch_line("*never42*") def test_setup_setUpClass(testdir): @@ -342,7 +342,7 @@ def test_testcase_adderrorandfailure_defers(testdir, type): % (type, type) ) result = testdir.runpytest() - assert "should not raise" not in result.stdout.str() + result.stdout.no_fnmatch_line("*should not raise*") @pytest.mark.parametrize("type", ["Error", "Failure"]) @@ -684,7 +684,7 @@ def test_unittest_not_shown_in_traceback(testdir): """ ) res = testdir.runpytest() - assert "failUnlessEqual" not in res.stdout.str() + res.stdout.no_fnmatch_line("*failUnlessEqual*") def test_unorderable_types(testdir): @@ -703,7 +703,7 @@ def test_unorderable_types(testdir): """ ) result = testdir.runpytest() - assert "TypeError" not in result.stdout.str() + result.stdout.no_fnmatch_line("*TypeError*") assert result.ret == ExitCode.NO_TESTS_COLLECTED @@ -1020,7 +1020,7 @@ def test_testcase_handles_init_exceptions(testdir): ) result = testdir.runpytest() assert "should raise this exception" in result.stdout.str() - assert "ERROR at teardown of MyTestCase.test_hello" not in result.stdout.str() + result.stdout.no_fnmatch_line("*ERROR at teardown of MyTestCase.test_hello*") def test_error_message_with_parametrized_fixtures(testdir): From 946434c610430d42cd2434d62bedd1f8dce45256 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 7 Oct 2019 02:25:18 +0200 Subject: [PATCH 005/153] Improve full diff output for lists Massage text input for difflib when comparing pformat output of different line lengths. Also do not strip ndiff output on the left, which currently already removes indenting for lines with no differences. Before: E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] E Right contains 3 more items, first extra item: ' ' E Full diff: E - ['version', 'version_info', 'sys.version', 'sys.version_info'] E + ['version', E + 'version_info', E + 'sys.version', E + 'sys.version_info', E + ' ', E + 'sys.version', E + 'sys.version_info'] After: E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] E Right contains 3 more items, first extra item: ' ' E Full diff: E [ E 'version', E 'version_info', E 'sys.version', E 'sys.version_info', E + ' ', E + 'sys.version', E + 'sys.version_info', E ] --- changelog/5924.feature.rst | 34 ++++++++++++++++++++++++ src/_pytest/assertion/util.py | 32 ++++++++++++++++++++++- testing/test_assertion.py | 49 +++++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+), 1 deletion(-) create mode 100644 changelog/5924.feature.rst diff --git a/changelog/5924.feature.rst b/changelog/5924.feature.rst new file mode 100644 index 000000000..a03eb4704 --- /dev/null +++ b/changelog/5924.feature.rst @@ -0,0 +1,34 @@ +Improve verbose diff output with sequences. + +Before: + +.. code-block:: + + E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] + E Right contains 3 more items, first extra item: ' ' + E Full diff: + E - ['version', 'version_info', 'sys.version', 'sys.version_info'] + E + ['version', + E + 'version_info', + E + 'sys.version', + E + 'sys.version_info', + E + ' ', + E + 'sys.version', + E + 'sys.version_info'] + +After: + +.. code-block:: + + E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] + E Right contains 3 more items, first extra item: ' ' + E Full diff: + E [ + E 'version', + E 'version_info', + E 'sys.version', + E 'sys.version_info', + E + ' ', + E + 'sys.version', + E + 'sys.version_info', + E ] diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index c2a4e446f..0350b0b07 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -246,6 +246,18 @@ def _compare_eq_verbose(left, right): return explanation +def _surrounding_parens_on_own_lines(lines): # type: (List) -> None + """Move opening/closing parenthesis/bracket to own lines.""" + opening = lines[0][:1] + if opening in ["(", "[", "{"]: + lines[0] = " " + lines[0][1:] + lines[:] = [opening] + lines + closing = lines[-1][-1:] + if closing in [")", "]", "}"]: + lines[-1] = lines[-1][:-1] + "," + lines[:] = lines + [closing] + + def _compare_eq_iterable(left, right, verbose=0): if not verbose: return ["Use -v to get the full diff"] @@ -254,9 +266,27 @@ def _compare_eq_iterable(left, right, verbose=0): left_formatting = pprint.pformat(left).splitlines() right_formatting = pprint.pformat(right).splitlines() + + # Re-format for different output lengths. + lines_left = len(left_formatting) + lines_right = len(right_formatting) + if lines_left != lines_right: + if lines_left > lines_right: + max_width = min(len(x) for x in left_formatting) + right_formatting = pprint.pformat(right, width=max_width).splitlines() + lines_right = len(right_formatting) + else: + max_width = min(len(x) for x in right_formatting) + left_formatting = pprint.pformat(left, width=max_width).splitlines() + lines_left = len(left_formatting) + + if lines_left > 1 or lines_right > 1: + _surrounding_parens_on_own_lines(left_formatting) + _surrounding_parens_on_own_lines(right_formatting) + explanation = ["Full diff:"] explanation.extend( - line.strip() for line in difflib.ndiff(left_formatting, right_formatting) + line.rstrip() for line in difflib.ndiff(left_formatting, right_formatting) ) return explanation diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 56729d28a..999f64a0e 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -413,6 +413,55 @@ class TestAssert_reprcompare: expl = callequal([0, 1, 2], [0, 1]) assert len(expl) > 1 + def test_list_wrap_for_multiple_lines(self): + long_d = "d" * 80 + l1 = ["a", "b", "c"] + l2 = ["a", "b", "c", long_d] + diff = callequal(l1, l2, verbose=True) + assert diff == [ + "['a', 'b', 'c'] == ['a', 'b', 'c...dddddddddddd']", + "Right contains one more item: '" + long_d + "'", + "Full diff:", + " [", + " 'a',", + " 'b',", + " 'c',", + "+ '" + long_d + "',", + " ]", + ] + + diff = callequal(l2, l1, verbose=True) + assert diff == [ + "['a', 'b', 'c...dddddddddddd'] == ['a', 'b', 'c']", + "Left contains one more item: '" + long_d + "'", + "Full diff:", + " [", + " 'a',", + " 'b',", + " 'c',", + "- '" + long_d + "',", + " ]", + ] + + def test_list_wrap_for_width_rewrap_same_length(self): + long_a = "a" * 30 + long_b = "b" * 30 + long_c = "c" * 30 + l1 = [long_a, long_b, long_c] + l2 = [long_b, long_c, long_a] + diff = callequal(l1, l2, verbose=True) + assert diff == [ + "['aaaaaaaaaaa...cccccccccccc'] == ['bbbbbbbbbbb...aaaaaaaaaaaa']", + "At index 0 diff: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' != 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'", + "Full diff:", + " [", + "- 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',", + " 'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbb',", + " 'cccccccccccccccccccccccccccccc',", + "+ 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',", + " ]", + ] + def test_dict(self): expl = callequal({"a": 0}, {"a": 1}) assert len(expl) > 1 From 2a2fe7d3db12b03b9a62392bccaceacd256c063e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 9 Oct 2019 05:16:27 +0200 Subject: [PATCH 006/153] Improve ExceptionInfo.__repr__ --- doc/5934.feature.rst | 1 + src/_pytest/_code/code.py | 4 +++- testing/code/test_excinfo.py | 15 +++++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 doc/5934.feature.rst diff --git a/doc/5934.feature.rst b/doc/5934.feature.rst new file mode 100644 index 000000000..17c0b1737 --- /dev/null +++ b/doc/5934.feature.rst @@ -0,0 +1 @@ +``repr`` of ``ExceptionInfo`` objects has been improved to honor the ``__repr__`` method of the underlying exception. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 534bfe2a8..1d26d94ab 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -507,7 +507,9 @@ class ExceptionInfo(Generic[_E]): def __repr__(self) -> str: if self._excinfo is None: return "" - return "" % (self.typename, len(self.traceback)) + return "<{} {} tblen={}>".format( + self.__class__.__name__, saferepr(self._excinfo[1]), len(self.traceback) + ) def exconly(self, tryshort: bool = False) -> str: """ return the exception as a string diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index e2f06a0a2..3f205b131 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -316,8 +316,19 @@ def test_excinfo_exconly(): def test_excinfo_repr_str(): excinfo = pytest.raises(ValueError, h) - assert repr(excinfo) == "" - assert str(excinfo) == "" + assert repr(excinfo) == "" + assert str(excinfo) == "" + + class CustomException(Exception): + def __repr__(self): + return "custom_repr" + + def raises(): + raise CustomException() + + excinfo = pytest.raises(CustomException, raises) + assert repr(excinfo) == "" + assert str(excinfo) == "" def test_excinfo_for_later(): From fb90259460a30b31f9d44803294f785d5aaa6d14 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 11 Oct 2019 04:18:19 +0200 Subject: [PATCH 007/153] test_assertion: improve mock_config --- testing/test_assertion.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 999f64a0e..7b99a65b4 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -12,13 +12,11 @@ from _pytest.assertion import util from _pytest.compat import ATTRS_EQ_FIELD -def mock_config(): +def mock_config(verbose=0): class Config: - verbose = False - def getoption(self, name): if name == "verbose": - return self.verbose + return verbose raise KeyError("Not mocked out: %s" % name) return Config() @@ -296,9 +294,8 @@ class TestBinReprIntegration: result.stdout.fnmatch_lines(["*test_hello*FAIL*", "*test_check*PASS*"]) -def callequal(left, right, verbose=False): - config = mock_config() - config.verbose = verbose +def callequal(left, right, verbose=0): + config = mock_config(verbose=verbose) return plugin.pytest_assertrepr_compare(config, "==", left, right) @@ -322,7 +319,7 @@ class TestAssert_reprcompare: assert "a" * 50 not in line def test_text_skipping_verbose(self): - lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs", verbose=True) + lines = callequal("a" * 50 + "spam", "a" * 50 + "eggs", verbose=1) assert "- " + "a" * 50 + "spam" in lines assert "+ " + "a" * 50 + "eggs" in lines @@ -345,7 +342,7 @@ class TestAssert_reprcompare: def test_bytes_diff_verbose(self): """Check special handling for bytes diff (#5260)""" - diff = callequal(b"spam", b"eggs", verbose=True) + diff = callequal(b"spam", b"eggs", verbose=1) assert diff == [ "b'spam' == b'eggs'", "At index 0 diff: b's' != b'e'", @@ -402,9 +399,9 @@ class TestAssert_reprcompare: When verbose is False, then just a -v notice to get the diff is rendered, when verbose is True, then ndiff of the pprint is returned. """ - expl = callequal(left, right, verbose=False) + expl = callequal(left, right, verbose=0) assert expl[-1] == "Use -v to get the full diff" - expl = "\n".join(callequal(left, right, verbose=True)) + expl = "\n".join(callequal(left, right, verbose=1)) assert expl.endswith(textwrap.dedent(expected).strip()) def test_list_different_lengths(self): From 361f0e6ba7f1949d7daefa882be4b7c975375e1c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 14 Oct 2019 22:44:29 +0200 Subject: [PATCH 008/153] minor: test_failure_function: use vars --- testing/test_junitxml.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 06a033489..885d25941 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -477,22 +477,25 @@ class TestPython: assert "ValueError" in fnode.toxml() systemout = fnode.next_sibling assert systemout.tag == "system-out" - assert "hello-stdout" in systemout.toxml() - assert "info msg" not in systemout.toxml() + systemout_xml = systemout.toxml() + assert "hello-stdout" in systemout_xml + assert "info msg" not in systemout_xml systemerr = systemout.next_sibling assert systemerr.tag == "system-err" - assert "hello-stderr" in systemerr.toxml() - assert "info msg" not in systemerr.toxml() + systemerr_xml = systemerr.toxml() + assert "hello-stderr" in systemerr_xml + assert "info msg" not in systemerr_xml if junit_logging == "system-out": - assert "warning msg" in systemout.toxml() - assert "warning msg" not in systemerr.toxml() + assert "warning msg" in systemout_xml + assert "warning msg" not in systemerr_xml elif junit_logging == "system-err": - assert "warning msg" not in systemout.toxml() - assert "warning msg" in systemerr.toxml() - elif junit_logging == "no": - assert "warning msg" not in systemout.toxml() - assert "warning msg" not in systemerr.toxml() + assert "warning msg" not in systemout_xml + assert "warning msg" in systemerr_xml + else: + assert junit_logging == "no" + assert "warning msg" not in systemout_xml + assert "warning msg" not in systemerr_xml @parametrize_families def test_failure_verbose_message(self, testdir, run_and_parse, xunit_family): From c2ae0e0dc631cbcbc8921b79bcdc7740dad33aaf Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 15 Oct 2019 00:40:38 +0200 Subject: [PATCH 009/153] tests: move tests for setuponly/setupplan Forgotten in 032ce8baf. --- testing/{python/setup_only.py => test_setuponly.py} | 0 testing/{python/setup_plan.py => test_setupplan.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename testing/{python/setup_only.py => test_setuponly.py} (100%) rename testing/{python/setup_plan.py => test_setupplan.py} (100%) diff --git a/testing/python/setup_only.py b/testing/test_setuponly.py similarity index 100% rename from testing/python/setup_only.py rename to testing/test_setuponly.py diff --git a/testing/python/setup_plan.py b/testing/test_setupplan.py similarity index 100% rename from testing/python/setup_plan.py rename to testing/test_setupplan.py From 9da73541b7813690b514bab0cb695672ff2578bf Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 18 Oct 2019 22:10:30 +0200 Subject: [PATCH 010/153] tox: pass TERM Ref: https://github.com/tox-dev/tox/issues/1441 --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index b03941657..edc9a5667 100644 --- a/tox.ini +++ b/tox.ini @@ -21,7 +21,7 @@ commands = {env:_PYTEST_TOX_COVERAGE_RUN:} pytest {posargs:{env:_PYTEST_TOX_DEFAULT_POSARGS:}} coverage: coverage combine coverage: coverage report -m -passenv = USER USERNAME COVERAGE_* TRAVIS PYTEST_ADDOPTS +passenv = USER USERNAME COVERAGE_* TRAVIS PYTEST_ADDOPTS TERM setenv = _PYTEST_TOX_DEFAULT_POSARGS={env:_PYTEST_TOX_POSARGS_LSOF:} {env:_PYTEST_TOX_POSARGS_XDIST:} From ab245ccdc3f1d7aa99a695dd43ceb9d94ccc93bb Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 18 Oct 2019 22:06:13 +0200 Subject: [PATCH 011/153] help: display default verbosity --- src/_pytest/terminal.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 0d9794159..2fd81ea2b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -66,7 +66,11 @@ def pytest_addoption(parser): help="decrease verbosity.", ), group._addoption( - "--verbosity", dest="verbose", type=int, default=0, help="set verbosity" + "--verbosity", + dest="verbose", + type=int, + default=0, + help="set verbosity. Default is 0.", ) group._addoption( "-r", From cd753aa4ab599d2cccbefb7c837681efae406a29 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 19 Oct 2019 03:01:10 +0200 Subject: [PATCH 012/153] ExceptionInfo.from_current: pass through exprinfo This was lost in 11f1f79222. --- src/_pytest/_code/code.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 1d26d94ab..694f45fb6 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -448,7 +448,7 @@ class ExceptionInfo(Generic[_E]): assert tup[1] is not None, "no current exception" assert tup[2] is not None, "no current exception" exc_info = (tup[0], tup[1], tup[2]) - return cls.from_exc_info(exc_info) + return cls.from_exc_info(exc_info, exprinfo) @classmethod def for_later(cls) -> "ExceptionInfo[_E]": From 5b88612e5bebc3e4d067484436cfc591f41a3eac Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 19 Oct 2019 09:03:26 +0200 Subject: [PATCH 013/153] tests: harden/fix test_trial_error --- testing/test_unittest.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 281c85281..f56284d85 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -233,7 +233,7 @@ def test_unittest_skip_issue148(testdir): def test_method_and_teardown_failing_reporting(testdir): testdir.makepyfile( """ - import unittest, pytest + import unittest class TC(unittest.TestCase): def tearDown(self): assert 0, "down1" @@ -530,19 +530,31 @@ class TestTrialUnittest: # will crash both at test time and at teardown """ ) - result = testdir.runpytest() + # Ignore DeprecationWarning (for `cmp`) from attrs through twisted, + # for stable test results. + result = testdir.runpytest( + "-vv", "-oconsole_output_style=classic", "-W", "ignore::DeprecationWarning" + ) result.stdout.fnmatch_lines( [ + "test_trial_error.py::TC::test_four FAILED", + "test_trial_error.py::TC::test_four ERROR", + "test_trial_error.py::TC::test_one FAILED", + "test_trial_error.py::TC::test_three FAILED", + "test_trial_error.py::TC::test_two FAILED", "*ERRORS*", + "*_ ERROR at teardown of TC.test_four _*", "*DelayedCalls*", - "*test_four*", + "*= FAILURES =*", + "*_ TC.test_four _*", "*NameError*crash*", - "*test_one*", + "*_ TC.test_one _*", "*NameError*crash*", - "*test_three*", + "*_ TC.test_three _*", "*DelayedCalls*", - "*test_two*", - "*crash*", + "*_ TC.test_two _*", + "*NameError*crash*", + "*= 4 failed, 1 error in *", ] ) From 15f956869479a60b1c39f72309e9a0e7b75f1689 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 19 Oct 2019 11:05:12 +0200 Subject: [PATCH 014/153] Improve/revisit CallInfo.__repr__ --- src/_pytest/runner.py | 13 +++---------- testing/test_runner.py | 13 +++++++++++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index fce4c1e3f..29f9658ee 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -236,16 +236,9 @@ class CallInfo: return cls(start=start, stop=stop, when=when, result=result, excinfo=excinfo) def __repr__(self): - if self.excinfo is not None: - status = "exception" - value = self.excinfo.value - else: - # TODO: investigate unification - value = repr(self._result) - status = "result" - return "".format( - when=self.when, value=value, status=status - ) + if self.excinfo is None: + return "".format(self.when, self._result) + return "".format(self.when, self.excinfo) def pytest_runtest_makereport(item, call): diff --git a/testing/test_runner.py b/testing/test_runner.py index 2d6b8476c..d2ce5d0bc 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -483,13 +483,22 @@ def test_callinfo(): assert ci.result == 0 assert "result" in repr(ci) assert repr(ci) == "" + assert str(ci) == "" ci = runner.CallInfo.from_call(lambda: 0 / 0, "123") assert ci.when == "123" assert not hasattr(ci, "result") - assert repr(ci) == "" + assert repr(ci) == "".format(ci.excinfo) + assert str(ci) == repr(ci) assert ci.excinfo - assert "exc" in repr(ci) + + # Newlines are escaped. + def raise_assertion(): + assert 0, "assert_msg" + + ci = runner.CallInfo.from_call(raise_assertion, "call") + assert repr(ci) == "".format(ci.excinfo) + assert "\n" not in repr(ci) # design question: do we want general hooks in python files? From d12cdd3127d5222335c699fd8e52c7ed8257b9e4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 19 Oct 2019 15:54:54 -0300 Subject: [PATCH 015/153] Make InvocationParams.args a tuple This avoids mutating the original list to reflect on InvocationParams, which is supposed to be an immutable snapshot of the state of pytest.main() at the moment of invocation (see pytest-dev/pytest-xdist#478). --- changelog/6008.improvement.rst | 2 ++ src/_pytest/config/__init__.py | 11 +++++------ testing/test_config.py | 9 +++++++-- 3 files changed, 14 insertions(+), 8 deletions(-) create mode 100644 changelog/6008.improvement.rst diff --git a/changelog/6008.improvement.rst b/changelog/6008.improvement.rst new file mode 100644 index 000000000..22ef35cc8 --- /dev/null +++ b/changelog/6008.improvement.rst @@ -0,0 +1,2 @@ +``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be +immutable and avoid accidental modifications. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 39c8d2cdf..cd23281fa 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -169,7 +169,7 @@ def get_config(args=None, plugins=None): config = Config( pluginmanager, invocation_params=Config.InvocationParams( - args=args, plugins=plugins, dir=Path().resolve() + args=args or (), plugins=plugins, dir=Path().resolve() ), ) @@ -654,7 +654,7 @@ class Config: Contains the following read-only attributes: - * ``args``: list of command-line arguments as passed to ``pytest.main()``. + * ``args``: tuple of command-line arguments as passed to ``pytest.main()``. * ``plugins``: list of extra plugins, might be None. * ``dir``: directory where ``pytest.main()`` was invoked from. """ @@ -667,13 +667,13 @@ class Config: .. note:: - Currently the environment variable PYTEST_ADDOPTS is also handled by - pytest implicitly, not being part of the invocation. + Note that the environment variable ``PYTEST_ADDOPTS`` and the ``addopts`` + ini option are handled by pytest, not being included in the ``args`` attribute. Plugins accessing ``InvocationParams`` must be aware of that. """ - args = attr.ib() + args = attr.ib(converter=tuple) plugins = attr.ib() dir = attr.ib(type=Path) @@ -938,7 +938,6 @@ class Config: assert not hasattr( self, "args" ), "can only parse cmdline args at most once per Config object" - assert self.invocation_params.args == args self.hook.pytest_addhooks.call_historic( kwargs=dict(pluginmanager=self.pluginmanager) ) diff --git a/testing/test_config.py b/testing/test_config.py index 71dae5c4c..0264b029d 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -7,6 +7,7 @@ import _pytest._code import pytest from _pytest.compat import importlib_metadata from _pytest.config import _iter_rewritable_modules +from _pytest.config import Config from _pytest.config.exceptions import UsageError from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor @@ -456,7 +457,7 @@ class TestConfigFromdictargs: config = Config.fromdictargs(option_dict, args) assert config.args == ["a", "b"] - assert config.invocation_params.args == args + assert config.invocation_params.args == tuple(args) assert config.option.verbose == 4 assert config.option.capture == "no" @@ -1235,7 +1236,7 @@ def test_invocation_args(testdir): call = calls[0] config = call.item.config - assert config.invocation_params.args == [p, "-v"] + assert config.invocation_params.args == (p, "-v") assert config.invocation_params.dir == Path(str(testdir.tmpdir)) plugins = config.invocation_params.plugins @@ -1243,6 +1244,10 @@ def test_invocation_args(testdir): assert plugins[0] is plugin assert type(plugins[1]).__name__ == "Collect" # installed by testdir.inline_run() + # args cannot be None + with pytest.raises(TypeError): + Config.InvocationParams(args=None, plugins=None, dir=Path()) + @pytest.mark.parametrize( "plugin", From 83351a33682df31fca12e6baf67370ab21af185b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 20 Jul 2019 06:08:22 +0200 Subject: [PATCH 016/153] doc: improve help for filterwarnings --- src/_pytest/warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 63d22477c..d817a5cfa 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -42,7 +42,7 @@ def pytest_addoption(parser): type="linelist", help="Each line specifies a pattern for " "warnings.filterwarnings. " - "Processed after -W and --pythonwarnings.", + "Processed after -W/--pythonwarnings.", ) From a6152db84adfb7e9b64c16826aad73d5be91cb28 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 20 Oct 2019 17:57:25 +0200 Subject: [PATCH 017/153] setuponly: pytest_fixture_setup: use option directly --- src/_pytest/setuponly.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index 70d6ed12f..13c2886bb 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -22,8 +22,7 @@ def pytest_addoption(parser): @pytest.hookimpl(hookwrapper=True) def pytest_fixture_setup(fixturedef, request): yield - config = request.config - if config.option.setupshow: + if request.config.option.setupshow: if hasattr(request, "param"): # Save the fixture parameter so ._show_fixture_action() can # display it now and during the teardown (in .finish()). From d91ff0af8a1d7fde73b88017ee7fd3c8ad846f3b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 10 Oct 2019 01:12:32 +0200 Subject: [PATCH 018/153] assertrepr_compare: use safeformat with -vv --- changelog/5936.feature.rst | 1 + src/_pytest/assertion/util.py | 17 +++++++++++++---- testing/test_assertrewrite.py | 21 +++++++++++++++------ 3 files changed, 29 insertions(+), 10 deletions(-) create mode 100644 changelog/5936.feature.rst diff --git a/changelog/5936.feature.rst b/changelog/5936.feature.rst new file mode 100644 index 000000000..c5cd924bb --- /dev/null +++ b/changelog/5936.feature.rst @@ -0,0 +1 @@ +Display untruncated assertion message with ``-vv``. diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 0350b0b07..ce29553d5 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -7,6 +7,7 @@ from typing import Optional import _pytest._code from _pytest import outcomes +from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr from _pytest.compat import ATTRS_EQ_FIELD @@ -123,13 +124,21 @@ def isiterable(obj): def assertrepr_compare(config, op, left, right): """Return specialised explanations for some operators/operands""" - maxsize = (80 - 15 - len(op) - 2) // 2 # 15 chars indentation, 1 space around op - left_repr = saferepr(left, maxsize=maxsize) - right_repr = saferepr(right, maxsize=maxsize) + verbose = config.getoption("verbose") + if verbose > 1: + left_repr = safeformat(left) + right_repr = safeformat(right) + else: + # XXX: "15 chars indentation" is wrong + # ("E AssertionError: assert "); should use term width. + maxsize = ( + 80 - 15 - len(op) - 2 + ) // 2 # 15 chars indentation, 1 space around op + left_repr = saferepr(left, maxsize=maxsize) + right_repr = saferepr(right, maxsize=maxsize) summary = "{} {} {}".format(left_repr, op, right_repr) - verbose = config.getoption("verbose") explanation = None try: if op == "==": diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 470c54145..3555d8252 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -190,11 +190,12 @@ class TestAssertionRewrite: pass msg = getmsg(f, {"cls": X}).splitlines() - if verbose > 0: - + if verbose > 1: + assert msg == ["assert {!r} == 42".format(X), " -{!r}".format(X), " +42"] + elif verbose > 0: assert msg == [ "assert .X'> == 42", - " -.X'>", + " -{!r}".format(X), " +42", ] else: @@ -206,9 +207,17 @@ class TestAssertionRewrite: def f(): assert "1234567890" * 5 + "A" == "1234567890" * 5 + "B" - assert getmsg(f).splitlines()[0] == ( - "assert '123456789012...901234567890A' == '123456789012...901234567890B'" - ) + msg = getmsg(f).splitlines()[0] + if request.config.getoption("verbose") > 1: + assert msg == ( + "assert '12345678901234567890123456789012345678901234567890A' " + "== '12345678901234567890123456789012345678901234567890B'" + ) + else: + assert msg == ( + "assert '123456789012...901234567890A' " + "== '123456789012...901234567890B'" + ) def test_dont_rewrite_if_hasattr_fails(self, request): class Y: From 46fbf22524058a36b7fd94a5dcd021858079189a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 17 Oct 2019 00:01:54 +0200 Subject: [PATCH 019/153] ci: Travis: cover verbose=1 --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b77c3f595..92d28cafc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,10 +23,13 @@ install: jobs: include: # OSX tests - first (in test stage), since they are the slower ones. + # Coverage for: + # - osx + # - verbose=1 - os: osx osx_image: xcode10.1 language: generic - env: TOXENV=py37-xdist PYTEST_COVERAGE=1 + env: TOXENV=py37-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS=-v before_install: - which python3 - python3 -V @@ -52,7 +55,7 @@ jobs: # - TestArgComplete (linux only) # - numpy # - old attrs - # Empty PYTEST_ADDOPTS to run this non-verbose. + # - verbose=0 - env: TOXENV=py37-lsof-oldattrs-numpy-twisted-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS= # Specialized factors for py37. From b47f57a08a9db7b3ec80b8197153a86d1eef117d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 21 Oct 2019 01:13:29 +0200 Subject: [PATCH 020/153] pytester: parseconfigure: remove duplicate config._ensure_unconfigure This gets done in `parseconfig` already. --- src/_pytest/pytester.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a050dad09..2ce3701c0 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -927,11 +927,9 @@ class Testdir: This returns a new :py:class:`_pytest.config.Config` instance like :py:meth:`parseconfig`, but also calls the pytest_configure hook. - """ config = self.parseconfig(*args) config._do_configure() - self.request.addfinalizer(config._ensure_unconfigure) return config def getitem(self, source, funcname="test_func"): From 995990c61b1eaa5830d01a1a14608ec981eaf8b9 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 21 Oct 2019 02:24:59 +0200 Subject: [PATCH 021/153] Remove (rejected) comment from DontReadFromInput Ref: https://github.com/pytest-dev/pytest/pull/4996#issuecomment-479686487 --- src/_pytest/capture.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index e4e58b32c..c4099e6b0 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -693,13 +693,6 @@ class SysCaptureBinary(SysCapture): class DontReadFromInput: - """Temporary stub class. Ideally when stdin is accessed, the - capturing should be turned off, with possibly all data captured - so far sent to the screen. This should be configurable, though, - because in automated test runs it is better to crash than - hang indefinitely. - """ - encoding = None def read(self, *args): From 554dba391c2302c29590d7991414bfe5d01ab81e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 6 Apr 2019 17:09:48 +0200 Subject: [PATCH 022/153] Multiple colors with terminal summary_stats Ref: https://github.com/pytest-dev/pytest/issues/5060 --- changelog/5061.feature.rst | 1 + src/_pytest/terminal.py | 83 ++++++++++++++++------- testing/test_pdb.py | 10 +-- testing/test_terminal.py | 131 ++++++++++++++++++++++++++++++------- 4 files changed, 172 insertions(+), 53 deletions(-) create mode 100644 changelog/5061.feature.rst diff --git a/changelog/5061.feature.rst b/changelog/5061.feature.rst new file mode 100644 index 000000000..9eb0c1cd3 --- /dev/null +++ b/changelog/5061.feature.rst @@ -0,0 +1 @@ +Use multiple colors with terminal summary statistics. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2fd81ea2b..fd30d8572 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -864,15 +864,41 @@ class TerminalReporter: self._tw.line(content) def summary_stats(self): - session_duration = time.time() - self._sessionstarttime - (line, color) = build_summary_stats_line(self.stats) - msg = "{} in {}".format(line, format_session_duration(session_duration)) - markup = {color: True, "bold": True} + if self.verbosity < -1: + return - if self.verbosity >= 0: - self.write_sep("=", msg, **markup) - if self.verbosity == -1: - self.write_line(msg, **markup) + session_duration = time.time() - self._sessionstarttime + (parts, main_color) = build_summary_stats_line(self.stats) + line_parts = [] + + display_sep = self.verbosity >= 0 + if display_sep: + fullwidth = self._tw.fullwidth + for text, markup in parts: + with_markup = self._tw.markup(text, **markup) + if display_sep: + fullwidth += len(with_markup) - len(text) + line_parts.append(with_markup) + msg = ", ".join(line_parts) + + main_markup = {main_color: True} + duration = " in {}".format(format_session_duration(session_duration)) + duration_with_markup = self._tw.markup(duration, **main_markup) + if display_sep: + fullwidth += len(duration_with_markup) - len(duration) + msg += duration_with_markup + + if display_sep: + markup_for_end_sep = self._tw.markup("", **main_markup) + if markup_for_end_sep.endswith("\x1b[0m"): + markup_for_end_sep = markup_for_end_sep[:-4] + fullwidth += len(markup_for_end_sep) + msg += markup_for_end_sep + + if display_sep: + self.write_sep("=", msg, fullwidth=fullwidth, **main_markup) + else: + self.write_line(msg, **main_markup) def short_test_summary(self): if not self.reportchars: @@ -1011,6 +1037,15 @@ def _folded_skips(skipped): return values +_color_for_type = { + "failed": "red", + "error": "red", + "warnings": "yellow", + "passed": "green", +} +_color_for_type_default = "yellow" + + def build_summary_stats_line(stats): known_types = ( "failed passed skipped deselected xfailed xpassed warnings error".split() @@ -1021,6 +1056,17 @@ def build_summary_stats_line(stats): if found_type: # setup/teardown reports have an empty key, ignore them known_types.append(found_type) unknown_type_seen = True + + # main color + if "failed" in stats or "error" in stats: + main_color = "red" + elif "warnings" in stats or unknown_type_seen: + main_color = "yellow" + elif "passed" in stats: + main_color = "green" + else: + main_color = "yellow" + parts = [] for key in known_types: reports = stats.get(key, None) @@ -1028,23 +1074,14 @@ def build_summary_stats_line(stats): count = sum( 1 for rep in reports if getattr(rep, "count_towards_summary", True) ) - parts.append("%d %s" % (count, key)) + color = _color_for_type.get(key, _color_for_type_default) + markup = {color: True, "bold": color == main_color} + parts.append(("%d %s" % (count, key), markup)) - if parts: - line = ", ".join(parts) - else: - line = "no tests ran" + if not parts: + parts = [("no tests ran", {_color_for_type_default: True})] - if "failed" in stats or "error" in stats: - color = "red" - elif "warnings" in stats or unknown_type_seen: - color = "yellow" - elif "passed" in stats: - color = "green" - else: - color = "yellow" - - return line, color + return parts, main_color def _plugin_nameversions(plugininfo): diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 924c2f4af..d4c4e8014 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -193,7 +193,7 @@ class TestPDB: ) child = testdir.spawn_pytest("-rs --pdb %s" % p1) child.expect("Skipping also with pdb active") - child.expect("1 skipped in") + child.expect_exact("= \x1b[33m\x1b[1m1 skipped\x1b[0m\x1b[33m in") child.sendeof() self.flush(child) @@ -221,7 +221,7 @@ class TestPDB: child.sendeof() rest = child.read().decode("utf8") assert "Exit: Quitting debugger" in rest - assert "= 1 failed in" in rest + assert "= \x1b[31m\x1b[1m1 failed\x1b[0m\x1b[31m in" in rest assert "def test_1" not in rest assert "get rekt" not in rest self.flush(child) @@ -703,7 +703,7 @@ class TestPDB: assert "> PDB continue (IO-capturing resumed) >" in rest else: assert "> PDB continue >" in rest - assert "1 passed in" in rest + assert "= \x1b[32m\x1b[1m1 passed\x1b[0m\x1b[32m in" in rest def test_pdb_used_outside_test(self, testdir): p1 = testdir.makepyfile( @@ -1019,7 +1019,7 @@ class TestTraceOption: child.sendline("q") child.expect_exact("Exit: Quitting debugger") rest = child.read().decode("utf8") - assert "2 passed in" in rest + assert "= \x1b[32m\x1b[1m2 passed\x1b[0m\x1b[32m in" in rest assert "reading from stdin while output" not in rest # Only printed once - not on stderr. assert "Exit: Quitting debugger" not in child.before.decode("utf8") @@ -1130,7 +1130,7 @@ def test_pdb_suspends_fixture_capturing(testdir, fixture): TestPDB.flush(child) assert child.exitstatus == 0 - assert "= 1 passed in " in rest + assert "= \x1b[32m\x1b[1m1 passed\x1b[0m\x1b[32m in" in rest assert "> PDB continue (IO-capturing resumed for fixture %s) >" % (fixture) in rest diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 3bdabc5de..a624be3b4 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -164,7 +164,7 @@ class TestTerminal: child.expect(r"collecting 2 items") child.expect(r"collected 2 items") rest = child.read().decode("utf8") - assert "2 passed in" in rest + assert "= \x1b[32m\x1b[1m2 passed\x1b[0m\x1b[32m in" in rest def test_itemreport_subclasses_show_subclassed_file(self, testdir): testdir.makepyfile( @@ -1252,42 +1252,123 @@ def test_terminal_summary_warnings_header_once(testdir): # dict value, not the actual contents, so tuples of anything # suffice # Important statuses -- the highest priority of these always wins - ("red", "1 failed", {"failed": (1,)}), - ("red", "1 failed, 1 passed", {"failed": (1,), "passed": (1,)}), - ("red", "1 error", {"error": (1,)}), - ("red", "1 passed, 1 error", {"error": (1,), "passed": (1,)}), + ("red", [("1 failed", {"bold": True, "red": True})], {"failed": (1,)}), + ( + "red", + [ + ("1 failed", {"bold": True, "red": True}), + ("1 passed", {"bold": False, "green": True}), + ], + {"failed": (1,), "passed": (1,)}, + ), + ("red", [("1 error", {"bold": True, "red": True})], {"error": (1,)}), + ( + "red", + [ + ("1 passed", {"bold": False, "green": True}), + ("1 error", {"bold": True, "red": True}), + ], + {"error": (1,), "passed": (1,)}, + ), # (a status that's not known to the code) - ("yellow", "1 weird", {"weird": (1,)}), - ("yellow", "1 passed, 1 weird", {"weird": (1,), "passed": (1,)}), - ("yellow", "1 warnings", {"warnings": (1,)}), - ("yellow", "1 passed, 1 warnings", {"warnings": (1,), "passed": (1,)}), - ("green", "5 passed", {"passed": (1, 2, 3, 4, 5)}), + ("yellow", [("1 weird", {"bold": True, "yellow": True})], {"weird": (1,)}), + ( + "yellow", + [ + ("1 passed", {"bold": False, "green": True}), + ("1 weird", {"bold": True, "yellow": True}), + ], + {"weird": (1,), "passed": (1,)}, + ), + ( + "yellow", + [("1 warnings", {"bold": True, "yellow": True})], + {"warnings": (1,)}, + ), + ( + "yellow", + [ + ("1 passed", {"bold": False, "green": True}), + ("1 warnings", {"bold": True, "yellow": True}), + ], + {"warnings": (1,), "passed": (1,)}, + ), + ( + "green", + [("5 passed", {"bold": True, "green": True})], + {"passed": (1, 2, 3, 4, 5)}, + ), # "Boring" statuses. These have no effect on the color of the summary # line. Thus, if *every* test has a boring status, the summary line stays # at its default color, i.e. yellow, to warn the user that the test run # produced no useful information - ("yellow", "1 skipped", {"skipped": (1,)}), - ("green", "1 passed, 1 skipped", {"skipped": (1,), "passed": (1,)}), - ("yellow", "1 deselected", {"deselected": (1,)}), - ("green", "1 passed, 1 deselected", {"deselected": (1,), "passed": (1,)}), - ("yellow", "1 xfailed", {"xfailed": (1,)}), - ("green", "1 passed, 1 xfailed", {"xfailed": (1,), "passed": (1,)}), - ("yellow", "1 xpassed", {"xpassed": (1,)}), - ("green", "1 passed, 1 xpassed", {"xpassed": (1,), "passed": (1,)}), + ("yellow", [("1 skipped", {"bold": True, "yellow": True})], {"skipped": (1,)}), + ( + "green", + [ + ("1 passed", {"bold": True, "green": True}), + ("1 skipped", {"bold": False, "yellow": True}), + ], + {"skipped": (1,), "passed": (1,)}, + ), + ( + "yellow", + [("1 deselected", {"bold": True, "yellow": True})], + {"deselected": (1,)}, + ), + ( + "green", + [ + ("1 passed", {"bold": True, "green": True}), + ("1 deselected", {"bold": False, "yellow": True}), + ], + {"deselected": (1,), "passed": (1,)}, + ), + ("yellow", [("1 xfailed", {"bold": True, "yellow": True})], {"xfailed": (1,)}), + ( + "green", + [ + ("1 passed", {"bold": True, "green": True}), + ("1 xfailed", {"bold": False, "yellow": True}), + ], + {"xfailed": (1,), "passed": (1,)}, + ), + ("yellow", [("1 xpassed", {"bold": True, "yellow": True})], {"xpassed": (1,)}), + ( + "green", + [ + ("1 passed", {"bold": True, "green": True}), + ("1 xpassed", {"bold": False, "yellow": True}), + ], + {"xpassed": (1,), "passed": (1,)}, + ), # Likewise if no tests were found at all - ("yellow", "no tests ran", {}), + ("yellow", [("no tests ran", {"yellow": True})], {}), # Test the empty-key special case - ("yellow", "no tests ran", {"": (1,)}), - ("green", "1 passed", {"": (1,), "passed": (1,)}), + ("yellow", [("no tests ran", {"yellow": True})], {"": (1,)}), + ( + "green", + [("1 passed", {"bold": True, "green": True})], + {"": (1,), "passed": (1,)}, + ), # A couple more complex combinations ( "red", - "1 failed, 2 passed, 3 xfailed", + [ + ("1 failed", {"bold": True, "red": True}), + ("2 passed", {"bold": False, "green": True}), + ("3 xfailed", {"bold": False, "yellow": True}), + ], {"passed": (1, 2), "failed": (1,), "xfailed": (1, 2, 3)}, ), ( "green", - "1 passed, 2 skipped, 3 deselected, 2 xfailed", + [ + ("1 passed", {"bold": True, "green": True}), + ("2 skipped", {"bold": False, "yellow": True}), + ("3 deselected", {"bold": False, "yellow": True}), + ("2 xfailed", {"bold": False, "yellow": True}), + ], { "passed": (1,), "skipped": (1, 2), @@ -1313,11 +1394,11 @@ def test_skip_counting_towards_summary(): r1 = DummyReport() r2 = DummyReport() res = build_summary_stats_line({"failed": (r1, r2)}) - assert res == ("2 failed", "red") + assert res == ([("2 failed", {"bold": True, "red": True})], "red") r1.count_towards_summary = False res = build_summary_stats_line({"failed": (r1, r2)}) - assert res == ("1 failed", "red") + assert res == ([("1 failed", {"bold": True, "red": True})], "red") class TestClassicOutputStyle: From 3c14dd7f55dbc14dabdff4ecf5246ba0f4501e8f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 22 Oct 2019 02:01:50 +0200 Subject: [PATCH 023/153] capture: improve message with DontReadFromInput's IOError Ref: https://github.com/pytest-dev/pytest/pull/4996#issuecomment-479686487 --- src/_pytest/capture.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index e4e58b32c..2877f2215 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -703,7 +703,9 @@ class DontReadFromInput: encoding = None def read(self, *args): - raise IOError("reading from stdin while output is captured") + raise IOError( + "pytest: reading from stdin while output is captured! Consider using `-s`." + ) readline = read readlines = read From 56cec5fa79106c0e8c02eb34bd8e5768ec52044d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 22 Oct 2019 05:46:10 +0200 Subject: [PATCH 024/153] ci: use tox -vv This will display durations, and is useful in logs in general. --- .travis.yml | 2 +- azure-pipelines.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b77c3f595..be30aa44e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -113,7 +113,7 @@ before_script: export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess fi -script: tox +script: tox -vv after_success: - | diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f18ce0887..2ee1604a7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -57,7 +57,7 @@ jobs: export COVERAGE_FILE="$PWD/.coverage" export COVERAGE_PROCESS_START="$PWD/.coveragerc" fi - python -m tox -e $(tox.env) + python -m tox -e $(tox.env) -vv displayName: 'Run tests' - task: PublishTestResults@2 From 851fc0280f155becb1c0a4307e23be7b857432ea Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 22 Oct 2019 08:23:39 +0200 Subject: [PATCH 025/153] ci: Travis: configure/restrict branch builds [ci skip] --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index b77c3f595..5542fa2ac 100644 --- a/.travis.yml +++ b/.travis.yml @@ -130,3 +130,10 @@ notifications: skip_join: true email: - pytest-commit@python.org + +branches: + only: + - master + - features + - 4.6-maintenance + - /^\d+(\.\d+)+$/ From 0dd68ba0b6a923bd57a4ba50772540020b974386 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 22 Oct 2019 23:44:16 +0200 Subject: [PATCH 026/153] tests: mark test_meta as slow This moves it to the end of tests during collection. Takes ~7s for me. --- testing/test_meta.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_meta.py b/testing/test_meta.py index 7aa100e6e..296aa42aa 100644 --- a/testing/test_meta.py +++ b/testing/test_meta.py @@ -15,6 +15,7 @@ def _modules(): ) +@pytest.mark.slow @pytest.mark.parametrize("module", _modules()) def test_no_warnings(module): # fmt: off From a51bb3eedb43c2951ce8ee2dd3fab849c4d6ee7c Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 22 Oct 2019 19:43:42 -0300 Subject: [PATCH 027/153] Add CHANGELOG for #5630 --- changelog/5630.improvement.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5630.improvement.rst diff --git a/changelog/5630.improvement.rst b/changelog/5630.improvement.rst new file mode 100644 index 000000000..45d49bdae --- /dev/null +++ b/changelog/5630.improvement.rst @@ -0,0 +1 @@ +Quitting from debuggers is now properly handled in ``doctest`` items. From 5e7b2ae704da84db5e9d4a786ebe1efe1997f9ff Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 23 Oct 2019 00:21:29 +0200 Subject: [PATCH 028/153] doctest: pytest_unconfigure: reset RUNNER_CLASS This is important when used with ``pytester``'s ``runpytest_inprocess``. Since 07f20ccab `pytest testing/acceptance_test.py -k test_doctest_id` would fail, since the second run would not consider the exception to be an instance of `doctest.DocTestFailure` anymore, since the module was re-imported, and use another failure message then in the short test summary info (and in the report itself): > FAILED test_doctest_id.txt::test_doctest_id.txt - doctest.DocTestFailure: FAILED test_doctest_id.txt::test_doctest_id.txt --- changelog/6039.bugfix.rst | 3 +++ src/_pytest/doctest.py | 6 ++++++ testing/acceptance_test.py | 25 +++++++++++++++---------- 3 files changed, 24 insertions(+), 10 deletions(-) create mode 100644 changelog/6039.bugfix.rst diff --git a/changelog/6039.bugfix.rst b/changelog/6039.bugfix.rst new file mode 100644 index 000000000..b13a677c8 --- /dev/null +++ b/changelog/6039.bugfix.rst @@ -0,0 +1,3 @@ +The ``PytestDoctestRunner`` is properly invalidated when unconfiguring the doctest plugin. + +This is important when used with ``pytester``'s ``runpytest_inprocess``. diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 1bd2642ae..4c17e0358 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -86,6 +86,12 @@ def pytest_addoption(parser): ) +def pytest_unconfigure(): + global RUNNER_CLASS + + RUNNER_CLASS = None + + def pytest_collect_file(path, parent): config = parent.config if path.ext == ".py": diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index a5187644d..2bf56cb80 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -859,16 +859,21 @@ class TestInvocationVariants: 4 """, ) - result = testdir.runpytest("-rf") - lines = result.stdout.str().splitlines() - for line in lines: - if line.startswith(("FAIL ", "FAILED ")): - _fail, _sep, testid = line.partition(" ") - break - result = testdir.runpytest(testid, "-rf") - result.stdout.fnmatch_lines( - ["FAILED test_doctest_id.txt::test_doctest_id.txt", "*1 failed*"] - ) + testid = "test_doctest_id.txt::test_doctest_id.txt" + expected_lines = [ + "*= FAILURES =*", + "*_ ?doctest? test_doctest_id.txt _*", + "FAILED test_doctest_id.txt::test_doctest_id.txt", + "*= 1 failed in*", + ] + result = testdir.runpytest(testid, "-rf", "--tb=short") + result.stdout.fnmatch_lines(expected_lines) + + # Ensure that re-running it will still handle it as + # doctest.DocTestFailure, which was not the case before when + # re-importing doctest, but not creating a new RUNNER_CLASS. + result = testdir.runpytest(testid, "-rf", "--tb=short") + result.stdout.fnmatch_lines(expected_lines) def test_core_backward_compatibility(self): """Test backward compatibility for get_plugin_manager function. See #787.""" From b079dc2dbeb5f117de8c18e41a25cf44531f6bb2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 23 Oct 2019 04:13:37 +0200 Subject: [PATCH 029/153] Fix test_doctest_set_trace_quit on features --- testing/test_pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 53475078e..fbf344807 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -506,7 +506,7 @@ class TestPDB: rest = child.read().decode("utf8") assert "! _pytest.outcomes.Exit: Quitting debugger !" in rest - assert "= no tests ran in" in rest + assert "= \x1b[33mno tests ran\x1b[0m\x1b[33m in" in rest assert "BdbQuit" not in rest assert "UNEXPECTED EXCEPTION" not in rest From 046aa0b6e93db59dbb67f6909415215d038fd02b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 21 Oct 2019 23:45:57 +0200 Subject: [PATCH 030/153] pytest.main: return ExitCode --- changelog/6023.improvement.rst | 1 + src/_pytest/config/__init__.py | 13 ++++++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 changelog/6023.improvement.rst diff --git a/changelog/6023.improvement.rst b/changelog/6023.improvement.rst new file mode 100644 index 000000000..6cf81002e --- /dev/null +++ b/changelog/6023.improvement.rst @@ -0,0 +1 @@ +``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index ea709a26a..4746fd6c7 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -18,6 +18,7 @@ from typing import Optional from typing import Sequence from typing import Set from typing import Tuple +from typing import Union import attr import py @@ -56,7 +57,7 @@ class ConftestImportFailure(Exception): self.excinfo = excinfo # type: Tuple[Type[Exception], Exception, TracebackType] -def main(args=None, plugins=None): +def main(args=None, plugins=None) -> "Union[int, _pytest.main.ExitCode]": """ return exit code, after performing an in-process test run. :arg args: list of command line arguments. @@ -84,10 +85,16 @@ def main(args=None, plugins=None): formatted_tb = str(exc_repr) for line in formatted_tb.splitlines(): tw.line(line.rstrip(), red=True) - return 4 + return ExitCode.USAGE_ERROR else: try: - return config.hook.pytest_cmdline_main(config=config) + ret = config.hook.pytest_cmdline_main( + config=config + ) # type: Union[ExitCode, int] + try: + return ExitCode(ret) + except ValueError: + return ret finally: config._ensure_unconfigure() except UsageError as e: From 2f589a9769e12e71253624372e7eeb7076b7549b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 23 Oct 2019 00:00:15 +0200 Subject: [PATCH 031/153] pytester: runpytest_inprocess: use splitlines() This avoids having a trailing empty lines always. --- src/_pytest/pytester.py | 4 +++- testing/test_config.py | 2 +- testing/test_runner.py | 5 ++--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 2ce3701c0..2a9ef12b8 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -878,7 +878,9 @@ class Testdir: sys.stdout.write(out) sys.stderr.write(err) - res = RunResult(reprec.ret, out.split("\n"), err.split("\n"), time.time() - now) + res = RunResult( + reprec.ret, out.splitlines(), err.splitlines(), time.time() - now + ) res.reprec = reprec return res diff --git a/testing/test_config.py b/testing/test_config.py index 0264b029d..d4d624348 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1291,7 +1291,7 @@ def test_config_blocked_default_plugins(testdir, plugin): if plugin != "terminal": result.stdout.fnmatch_lines(["* 1 failed in *"]) else: - assert result.stdout.lines == [""] + assert result.stdout.lines == [] class TestSetupCfg: diff --git a/testing/test_runner.py b/testing/test_runner.py index d2ce5d0bc..86e9bddff 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -597,7 +597,7 @@ def test_pytest_exit_returncode(testdir): result = testdir.runpytest() result.stdout.fnmatch_lines(["*! *Exit: some exit msg !*"]) - assert _strip_resource_warnings(result.stderr.lines) == [""] + assert _strip_resource_warnings(result.stderr.lines) == [] assert result.ret == 99 # It prints to stderr also in case of exit during pytest_sessionstart. @@ -612,8 +612,7 @@ def test_pytest_exit_returncode(testdir): result = testdir.runpytest() result.stdout.fnmatch_lines(["*! *Exit: during_sessionstart !*"]) assert _strip_resource_warnings(result.stderr.lines) == [ - "Exit: during_sessionstart", - "", + "Exit: during_sessionstart" ] assert result.ret == 98 From 52b85f6f1ae44bb35c0292817696daf092858b98 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 23 Oct 2019 10:24:37 +0300 Subject: [PATCH 032/153] Update mypy 0.720 -> 0.740 Changelogs: http://mypy-lang.blogspot.com/2019/09/mypy-730-released.html http://mypy-lang.blogspot.com/2019/10/mypy-0740-released.html New errors: src/_pytest/recwarn.py:77: error: Missing return statement src/_pytest/recwarn.py:185: error: "bool" is invalid as return type for "__exit__" that always returns False src/_pytest/recwarn.py:185: note: Use "typing_extensions.Literal[False]" as the return type or change it to "None" src/_pytest/recwarn.py:185: note: If return type of "__exit__" implies that it may return True, the context manager may swallow exceptions src/_pytest/recwarn.py:185: error: Return type "bool" of "__exit__" incompatible with return type "None" in supertype "catch_warnings" src/_pytest/recwarn.py:230: error: "bool" is invalid as return type for "__exit__" that always returns False src/_pytest/recwarn.py:230: note: Use "typing_extensions.Literal[False]" as the return type or change it to "None" src/_pytest/recwarn.py:230: note: If return type of "__exit__" implies that it may return True, the context manager may swallow exceptions src/_pytest/recwarn.py:230: error: Return type "bool" of "__exit__" incompatible with return type "None" in supertype "catch_warnings" The errors are due to this new error: https://mypy.readthedocs.io/en/latest/error_code_list.html#check-the-return-type-of-exit-exit-return --- .pre-commit-config.yaml | 2 +- src/_pytest/recwarn.py | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e9a970ca7..8481848f7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -42,7 +42,7 @@ repos: hooks: - id: rst-backticks - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.720 + rev: v0.740 hooks: - id: mypy files: ^(src/|testing/) diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 58076d66b..4967106d9 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -187,7 +187,7 @@ class WarningsRecorder(warnings.catch_warnings): exc_type: Optional["Type[BaseException]"], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], - ) -> bool: + ) -> None: if not self._entered: __tracebackhide__ = True raise RuntimeError("Cannot exit %r without entering first" % self) @@ -198,8 +198,6 @@ class WarningsRecorder(warnings.catch_warnings): # manually here for this context manager to become reusable. self._entered = False - return False - class WarningsChecker(WarningsRecorder): def __init__( @@ -232,7 +230,7 @@ class WarningsChecker(WarningsRecorder): exc_type: Optional["Type[BaseException]"], exc_val: Optional[BaseException], exc_tb: Optional[TracebackType], - ) -> bool: + ) -> None: super().__exit__(exc_type, exc_val, exc_tb) __tracebackhide__ = True @@ -263,4 +261,3 @@ class WarningsChecker(WarningsRecorder): [each.message for each in self], ) ) - return False From 1371b01f78afcfb836ef21a81cdb9b6516c1119e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 23 Oct 2019 03:28:47 +0200 Subject: [PATCH 033/153] typing for ReprFailDoctest --- src/_pytest/doctest.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 1bd2642ae..fd65a3cc7 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -6,6 +6,8 @@ import sys import traceback import warnings from contextlib import contextmanager +from typing import Sequence +from typing import Tuple import pytest from _pytest import outcomes @@ -113,11 +115,12 @@ def _is_doctest(config, path, parent): class ReprFailDoctest(TerminalRepr): - def __init__(self, reprlocation_lines): - # List of (reprlocation, lines) tuples + def __init__( + self, reprlocation_lines: Sequence[Tuple[ReprFileLocation, Sequence[str]]] + ): self.reprlocation_lines = reprlocation_lines - def toterminal(self, tw): + def toterminal(self, tw) -> None: for reprlocation, lines in self.reprlocation_lines: for line in lines: tw.line(line) From 1984c10427adc264d7cdd1bedbb315792181c14b Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 30 Aug 2019 10:35:08 +0300 Subject: [PATCH 034/153] Fix check_untyped_defs errors in doctest In order to make the LiteralOutputChecker lazy initialization more amenable to type checking, I changed it to match the scheme already used in this file to lazy-initialize PytestDoctestRunner. --- src/_pytest/doctest.py | 95 ++++++++++++++++++++++++++--------------- testing/test_doctest.py | 7 +-- 2 files changed, 65 insertions(+), 37 deletions(-) diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index db1de1986..7449a56c8 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -6,8 +6,12 @@ import sys import traceback import warnings from contextlib import contextmanager +from typing import Dict +from typing import List +from typing import Optional from typing import Sequence from typing import Tuple +from typing import Union import pytest from _pytest import outcomes @@ -20,6 +24,10 @@ from _pytest.outcomes import Skipped from _pytest.python_api import approx from _pytest.warning_types import PytestWarning +if False: # TYPE_CHECKING + import doctest + from typing import Type + DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" DOCTEST_REPORT_CHOICE_NDIFF = "ndiff" @@ -36,6 +44,8 @@ DOCTEST_REPORT_CHOICES = ( # Lazy definition of runner class RUNNER_CLASS = None +# Lazy definition of output checker class +CHECKER_CLASS = None # type: Optional[Type[doctest.OutputChecker]] def pytest_addoption(parser): @@ -139,7 +149,7 @@ class MultipleDoctestFailures(Exception): self.failures = failures -def _init_runner_class(): +def _init_runner_class() -> "Type[doctest.DocTestRunner]": import doctest class PytestDoctestRunner(doctest.DebugRunner): @@ -177,12 +187,19 @@ def _init_runner_class(): return PytestDoctestRunner -def _get_runner(checker=None, verbose=None, optionflags=0, continue_on_failure=True): +def _get_runner( + checker: Optional["doctest.OutputChecker"] = None, + verbose: Optional[bool] = None, + optionflags: int = 0, + continue_on_failure: bool = True, +) -> "doctest.DocTestRunner": # We need this in order to do a lazy import on doctest global RUNNER_CLASS if RUNNER_CLASS is None: RUNNER_CLASS = _init_runner_class() - return RUNNER_CLASS( + # Type ignored because the continue_on_failure argument is only defined on + # PytestDoctestRunner, which is lazily defined so can't be used as a type. + return RUNNER_CLASS( # type: ignore checker=checker, verbose=verbose, optionflags=optionflags, @@ -211,7 +228,7 @@ class DoctestItem(pytest.Item): def runtest(self): _check_all_skipped(self.dtest) self._disable_output_capturing_for_darwin() - failures = [] + failures = [] # type: List[doctest.DocTestFailure] self.runner.run(self.dtest, out=failures) if failures: raise MultipleDoctestFailures(failures) @@ -232,7 +249,9 @@ class DoctestItem(pytest.Item): def repr_failure(self, excinfo): import doctest - failures = None + failures = ( + None + ) # type: Optional[List[Union[doctest.DocTestFailure, doctest.UnexpectedException]]] if excinfo.errisinstance((doctest.DocTestFailure, doctest.UnexpectedException)): failures = [excinfo.value] elif excinfo.errisinstance(MultipleDoctestFailures): @@ -255,8 +274,10 @@ class DoctestItem(pytest.Item): self.config.getoption("doctestreport") ) if lineno is not None: + assert failure.test.docstring is not None lines = failure.test.docstring.splitlines(False) # add line numbers to the left of the error message + assert test.lineno is not None lines = [ "%03d %s" % (i + test.lineno + 1, x) for (i, x) in enumerate(lines) @@ -288,7 +309,7 @@ class DoctestItem(pytest.Item): return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name -def _get_flag_lookup(): +def _get_flag_lookup() -> Dict[str, int]: import doctest return dict( @@ -340,14 +361,16 @@ class DoctestTextfile(pytest.Module): optionflags = get_optionflags(self) runner = _get_runner( - verbose=0, + verbose=False, optionflags=optionflags, checker=_get_checker(), continue_on_failure=_get_continue_on_failure(self.config), ) parser = doctest.DocTestParser() - test = parser.get_doctest(text, globs, name, filename, 0) + # Remove ignore once this reaches mypy: + # https://github.com/python/typeshed/commit/3e4a251b2b6da6bb43137acf5abf81ecfa7ba8ee + test = parser.get_doctest(text, globs, name, filename, 0) # type: ignore if test.examples: yield DoctestItem(test.name, self, runner, test) @@ -419,7 +442,8 @@ class DoctestModule(pytest.Module): return with _patch_unwrap_mock_aware(): - doctest.DocTestFinder._find( + # Type ignored because this is a private function. + doctest.DocTestFinder._find( # type: ignore self, tests, obj, name, module, source_lines, globs, seen ) @@ -437,7 +461,7 @@ class DoctestModule(pytest.Module): finder = MockAwareDocTestFinder() optionflags = get_optionflags(self) runner = _get_runner( - verbose=0, + verbose=False, optionflags=optionflags, checker=_get_checker(), continue_on_failure=_get_continue_on_failure(self.config), @@ -466,24 +490,7 @@ def _setup_fixtures(doctest_item): return fixture_request -def _get_checker(): - """ - Returns a doctest.OutputChecker subclass that supports some - additional options: - - * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b'' - prefixes (respectively) in string literals. Useful when the same - doctest should run in Python 2 and Python 3. - - * NUMBER to ignore floating-point differences smaller than the - precision of the literal number in the doctest. - - An inner class is used to avoid importing "doctest" at the module - level. - """ - if hasattr(_get_checker, "LiteralsOutputChecker"): - return _get_checker.LiteralsOutputChecker() - +def _init_checker_class() -> "Type[doctest.OutputChecker]": import doctest import re @@ -573,11 +580,31 @@ def _get_checker(): offset += w.end() - w.start() - (g.end() - g.start()) return got - _get_checker.LiteralsOutputChecker = LiteralsOutputChecker - return _get_checker.LiteralsOutputChecker() + return LiteralsOutputChecker -def _get_allow_unicode_flag(): +def _get_checker() -> "doctest.OutputChecker": + """ + Returns a doctest.OutputChecker subclass that supports some + additional options: + + * ALLOW_UNICODE and ALLOW_BYTES options to ignore u'' and b'' + prefixes (respectively) in string literals. Useful when the same + doctest should run in Python 2 and Python 3. + + * NUMBER to ignore floating-point differences smaller than the + precision of the literal number in the doctest. + + An inner class is used to avoid importing "doctest" at the module + level. + """ + global CHECKER_CLASS + if CHECKER_CLASS is None: + CHECKER_CLASS = _init_checker_class() + return CHECKER_CLASS() + + +def _get_allow_unicode_flag() -> int: """ Registers and returns the ALLOW_UNICODE flag. """ @@ -586,7 +613,7 @@ def _get_allow_unicode_flag(): return doctest.register_optionflag("ALLOW_UNICODE") -def _get_allow_bytes_flag(): +def _get_allow_bytes_flag() -> int: """ Registers and returns the ALLOW_BYTES flag. """ @@ -595,7 +622,7 @@ def _get_allow_bytes_flag(): return doctest.register_optionflag("ALLOW_BYTES") -def _get_number_flag(): +def _get_number_flag() -> int: """ Registers and returns the NUMBER flag. """ @@ -604,7 +631,7 @@ def _get_number_flag(): return doctest.register_optionflag("NUMBER") -def _get_report_choice(key): +def _get_report_choice(key: str) -> int: """ This function returns the actual `doctest` module flag value, we want to do it as late as possible to avoid importing `doctest` and all its dependencies when parsing options, as it adds overhead and breaks tests. diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 755f26286..37b3988f7 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -839,7 +839,8 @@ class TestLiterals: reprec = testdir.inline_run() reprec.assertoutcome(failed=1) - def test_number_re(self): + def test_number_re(self) -> None: + _number_re = _get_checker()._number_re # type: ignore for s in [ "1.", "+1.", @@ -861,12 +862,12 @@ class TestLiterals: "-1.2e-3", ]: print(s) - m = _get_checker()._number_re.match(s) + m = _number_re.match(s) assert m is not None assert float(m.group()) == pytest.approx(float(s)) for s in ["1", "abc"]: print(s) - assert _get_checker()._number_re.match(s) is None + assert _number_re.match(s) is None @pytest.mark.parametrize("config_mode", ["ini", "comment"]) def test_number_precision(self, testdir, config_mode): From 583c2a2f9babe67c8f2413b391416874efe91d36 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 13 Sep 2019 22:35:49 +0300 Subject: [PATCH 035/153] Fix check_untyped_defs errors in logging --- src/_pytest/logging.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 054bfc866..c72f76118 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -2,6 +2,10 @@ import logging import re from contextlib import contextmanager +from typing import AbstractSet +from typing import Dict +from typing import List +from typing import Mapping import py @@ -32,14 +36,15 @@ class ColoredLevelFormatter(logging.Formatter): logging.INFO: {"green"}, logging.DEBUG: {"purple"}, logging.NOTSET: set(), - } + } # type: Mapping[int, AbstractSet[str]] LEVELNAME_FMT_REGEX = re.compile(r"%\(levelname\)([+-.]?\d*s)") - def __init__(self, terminalwriter, *args, **kwargs): + def __init__(self, terminalwriter, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._original_fmt = self._style._fmt - self._level_to_fmt_mapping = {} + self._level_to_fmt_mapping = {} # type: Dict[int, str] + assert self._fmt is not None levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt) if not levelname_fmt_match: return @@ -216,17 +221,17 @@ def catching_logs(handler, formatter=None, level=None): class LogCaptureHandler(logging.StreamHandler): """A logging handler that stores log records and the log text.""" - def __init__(self): + def __init__(self) -> None: """Creates a new log handler.""" logging.StreamHandler.__init__(self, py.io.TextIO()) - self.records = [] + self.records = [] # type: List[logging.LogRecord] - def emit(self, record): + def emit(self, record: logging.LogRecord) -> None: """Keep the log records in a list in addition to the log text.""" self.records.append(record) logging.StreamHandler.emit(self, record) - def reset(self): + def reset(self) -> None: self.records = [] self.stream = py.io.TextIO() @@ -234,13 +239,13 @@ class LogCaptureHandler(logging.StreamHandler): class LogCaptureFixture: """Provides access and control of log capturing.""" - def __init__(self, item): + def __init__(self, item) -> None: """Creates a new funcarg.""" self._item = item # dict of log name -> log level - self._initial_log_levels = {} # Dict[str, int] + self._initial_log_levels = {} # type: Dict[str, int] - def _finalize(self): + def _finalize(self) -> None: """Finalizes the fixture. This restores the log levels changed by :meth:`set_level`. @@ -453,7 +458,7 @@ class LoggingPlugin: ): formatter = ColoredLevelFormatter( create_terminal_writer(self._config), log_format, log_date_format - ) + ) # type: logging.Formatter else: formatter = logging.Formatter(log_format, log_date_format) From 93c8822f26c90e3b440021b26da62e6aa6d49158 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 13 Sep 2019 22:45:45 +0300 Subject: [PATCH 036/153] Fix check_untyped_defs errors in warnings --- src/_pytest/warnings.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index d817a5cfa..8fdb61c2b 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -66,6 +66,8 @@ def catch_warnings_for_item(config, ihook, when, item): cmdline_filters = config.getoption("pythonwarnings") or [] inifilters = config.getini("filterwarnings") with warnings.catch_warnings(record=True) as log: + # mypy can't infer that record=True means log is not None; help it. + assert log is not None if not sys.warnoptions: # if user is not explicitly configuring warning filters, show deprecation warnings by default (#2908) @@ -145,6 +147,8 @@ def _issue_warning_captured(warning, hook, stacklevel): with warnings.catch_warnings(record=True) as records: warnings.simplefilter("always", type(warning)) warnings.warn(warning, stacklevel=stacklevel) + # Mypy can't infer that record=True means records is not None; help it. + assert records is not None hook.pytest_warning_captured.call_historic( kwargs=dict(warning_message=records[0], when="config", item=None) ) From 5dca7a2f4fa5e5c16935bc79c2f1da9d28886609 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 13 Sep 2019 22:49:47 +0300 Subject: [PATCH 037/153] Fix check_untyped_defs errors in cacheprovider --- src/_pytest/cacheprovider.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 7a5deaa39..dad76f13f 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -7,6 +7,7 @@ ignores the external pytest-cache import json import os from collections import OrderedDict +from typing import List import attr import py @@ -15,6 +16,9 @@ import pytest from .pathlib import Path from .pathlib import resolve_from_str from .pathlib import rm_rf +from _pytest import nodes +from _pytest.config import Config +from _pytest.main import Session README_CONTENT = """\ # pytest cache directory # @@ -263,10 +267,12 @@ class NFPlugin: self.active = config.option.newfirst self.cached_nodeids = config.cache.get("cache/nodeids", []) - def pytest_collection_modifyitems(self, session, config, items): + def pytest_collection_modifyitems( + self, session: Session, config: Config, items: List[nodes.Item] + ) -> None: if self.active: - new_items = OrderedDict() - other_items = OrderedDict() + new_items = OrderedDict() # type: OrderedDict[str, nodes.Item] + other_items = OrderedDict() # type: OrderedDict[str, nodes.Item] for item in items: if item.nodeid not in self.cached_nodeids: new_items[item.nodeid] = item From 0267b25c66875617ed69132445820a6f82e6e2fa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 13 Sep 2019 23:09:08 +0300 Subject: [PATCH 038/153] Fix some check_untyped_defs mypy errors in terminal --- src/_pytest/terminal.py | 42 +++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index fd30d8572..35f6d324b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -9,6 +9,12 @@ import platform import sys import time from functools import partial +from typing import Callable +from typing import Dict +from typing import List +from typing import Mapping +from typing import Optional +from typing import Set import attr import pluggy @@ -195,8 +201,8 @@ class WarningReport: file system location of the source of the warning (see ``get_location``). """ - message = attr.ib() - nodeid = attr.ib(default=None) + message = attr.ib(type=str) + nodeid = attr.ib(type=Optional[str], default=None) fslocation = attr.ib(default=None) count_towards_summary = True @@ -240,7 +246,7 @@ class TerminalReporter: self.reportchars = getreportopt(config) self.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() - self._progress_nodeids_reported = set() + self._progress_nodeids_reported = set() # type: Set[str] self._show_progress_info = self._determine_show_progress_info() self._collect_report_last_write = None @@ -619,7 +625,7 @@ class TerminalReporter: # because later versions are going to get rid of them anyway if self.config.option.verbose < 0: if self.config.option.verbose < -1: - counts = {} + counts = {} # type: Dict[str, int] for item in items: name = item.nodeid.split("::", 1)[0] counts[name] = counts.get(name, 0) + 1 @@ -750,7 +756,9 @@ class TerminalReporter: def summary_warnings(self): if self.hasopt("w"): - all_warnings = self.stats.get("warnings") + all_warnings = self.stats.get( + "warnings" + ) # type: Optional[List[WarningReport]] if not all_warnings: return @@ -763,7 +771,9 @@ class TerminalReporter: if not warning_reports: return - reports_grouped_by_message = collections.OrderedDict() + reports_grouped_by_message = ( + collections.OrderedDict() + ) # type: collections.OrderedDict[str, List[WarningReport]] for wr in warning_reports: reports_grouped_by_message.setdefault(wr.message, []).append(wr) @@ -900,11 +910,11 @@ class TerminalReporter: else: self.write_line(msg, **main_markup) - def short_test_summary(self): + def short_test_summary(self) -> None: if not self.reportchars: return - def show_simple(stat, lines): + def show_simple(stat, lines: List[str]) -> None: failed = self.stats.get(stat, []) if not failed: return @@ -914,7 +924,7 @@ class TerminalReporter: line = _get_line_with_reprcrash_message(config, rep, termwidth) lines.append(line) - def show_xfailed(lines): + def show_xfailed(lines: List[str]) -> None: xfailed = self.stats.get("xfailed", []) for rep in xfailed: verbose_word = rep._get_verbose_word(self.config) @@ -924,7 +934,7 @@ class TerminalReporter: if reason: lines.append(" " + str(reason)) - def show_xpassed(lines): + def show_xpassed(lines: List[str]) -> None: xpassed = self.stats.get("xpassed", []) for rep in xpassed: verbose_word = rep._get_verbose_word(self.config) @@ -932,7 +942,7 @@ class TerminalReporter: reason = rep.wasxfail lines.append("{} {} {}".format(verbose_word, pos, reason)) - def show_skipped(lines): + def show_skipped(lines: List[str]) -> None: skipped = self.stats.get("skipped", []) fskips = _folded_skips(skipped) if skipped else [] if not fskips: @@ -958,9 +968,9 @@ class TerminalReporter: "S": show_skipped, "p": partial(show_simple, "passed"), "E": partial(show_simple, "error"), - } + } # type: Mapping[str, Callable[[List[str]], None]] - lines = [] + lines = [] # type: List[str] for char in self.reportchars: action = REPORTCHAR_ACTIONS.get(char) if action: # skipping e.g. "P" (passed with output) here. @@ -1084,8 +1094,8 @@ def build_summary_stats_line(stats): return parts, main_color -def _plugin_nameversions(plugininfo): - values = [] +def _plugin_nameversions(plugininfo) -> List[str]: + values = [] # type: List[str] for plugin, dist in plugininfo: # gets us name and version! name = "{dist.project_name}-{dist.version}".format(dist=dist) @@ -1099,7 +1109,7 @@ def _plugin_nameversions(plugininfo): return values -def format_session_duration(seconds): +def format_session_duration(seconds: float) -> str: """Format the given seconds in a human readable manner to show in the final summary""" if seconds < 60: return "{:.2f}s".format(seconds) From 1787bffda016f07b25c6dc7a2fb27e39ab920ec4 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 14 Sep 2019 00:20:24 +0300 Subject: [PATCH 039/153] Fix check_untyped_defs errors in capture --- src/_pytest/capture.py | 10 +++++----- testing/test_capture.py | 6 ++++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/_pytest/capture.py b/src/_pytest/capture.py index c4099e6b0..56707822d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -12,6 +12,7 @@ from tempfile import TemporaryFile import pytest from _pytest.compat import CaptureIO +from _pytest.fixtures import FixtureRequest patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} @@ -241,13 +242,12 @@ class CaptureManager: capture_fixtures = {"capfd", "capfdbinary", "capsys", "capsysbinary"} -def _ensure_only_one_capture_fixture(request, name): - fixtures = set(request.fixturenames) & capture_fixtures - {name} +def _ensure_only_one_capture_fixture(request: FixtureRequest, name): + fixtures = sorted(set(request.fixturenames) & capture_fixtures - {name}) if fixtures: - fixtures = sorted(fixtures) - fixtures = fixtures[0] if len(fixtures) == 1 else fixtures + arg = fixtures[0] if len(fixtures) == 1 else fixtures raise request.raiseerror( - "cannot use {} and {} at the same time".format(fixtures, name) + "cannot use {} and {} at the same time".format(arg, name) ) diff --git a/testing/test_capture.py b/testing/test_capture.py index 0f7db4b8e..180637db6 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -6,6 +6,8 @@ import subprocess import sys import textwrap from io import UnsupportedOperation +from typing import List +from typing import TextIO import py @@ -857,8 +859,8 @@ def tmpfile(testdir): @needsosdup -def test_dupfile(tmpfile): - flist = [] +def test_dupfile(tmpfile) -> None: + flist = [] # type: List[TextIO] for i in range(5): nf = capture.safe_text_dupfile(tmpfile, "wb") assert nf != tmpfile From 1cc1ac51838f45d819fcdbfd71df79aa851dc677 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 23 Oct 2019 14:47:56 +0300 Subject: [PATCH 040/153] Remove some type: ignores fixed in typeshed --- src/_pytest/assertion/rewrite.py | 6 +++--- src/_pytest/doctest.py | 4 +--- src/_pytest/nodes.py | 3 +-- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index c225eff5f..4e7db8369 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -78,7 +78,8 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder): # there's nothing to rewrite there # python3.5 - python3.6: `namespace` # python3.7+: `None` - or spec.origin in {None, "namespace"} + or spec.origin == "namespace" + or spec.origin is None # we can only rewrite source files or not isinstance(spec.loader, importlib.machinery.SourceFileLoader) # if the file doesn't exist, we can't rewrite it @@ -743,8 +744,7 @@ class AssertionRewriter(ast.NodeVisitor): from _pytest.warning_types import PytestAssertRewriteWarning import warnings - # Ignore type: typeshed bug https://github.com/python/typeshed/pull/3121 - warnings.warn_explicit( # type: ignore + warnings.warn_explicit( PytestAssertRewriteWarning( "assertion is always true, perhaps remove parentheses?" ), diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 7449a56c8..48c934e3a 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -368,9 +368,7 @@ class DoctestTextfile(pytest.Module): ) parser = doctest.DocTestParser() - # Remove ignore once this reaches mypy: - # https://github.com/python/typeshed/commit/3e4a251b2b6da6bb43137acf5abf81ecfa7ba8ee - test = parser.get_doctest(text, globs, name, filename, 0) # type: ignore + test = parser.get_doctest(text, globs, name, filename, 0) if test.examples: yield DoctestItem(test.name, self, runner, test) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index e6dee1547..71036dc7e 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -139,8 +139,7 @@ class Node: ) ) path, lineno = get_fslocation_from_item(self) - # Type ignored: https://github.com/python/typeshed/pull/3121 - warnings.warn_explicit( # type: ignore + warnings.warn_explicit( warning, category=None, filename=str(path), From 7beb520555b7908c647c5cfa35bbfc3e48b28638 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 22 Oct 2019 19:33:54 -0300 Subject: [PATCH 041/153] Show the mnemonic of pytest.ExitCode in RunResult's repr Fix #4901 --- changelog/4901.trivial.rst | 2 ++ src/_pytest/pytester.py | 7 +++++-- testing/test_pytester.py | 30 +++++++++++++++++++----------- 3 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 changelog/4901.trivial.rst diff --git a/changelog/4901.trivial.rst b/changelog/4901.trivial.rst new file mode 100644 index 000000000..f6609ddf1 --- /dev/null +++ b/changelog/4901.trivial.rst @@ -0,0 +1,2 @@ +``RunResult`` from ``pytester`` now displays the mnemonic of the ``ret`` attribute when it is a +valid ``pytest.ExitCode`` value. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a050dad09..875828fd6 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -362,7 +362,10 @@ class RunResult: """ def __init__(self, ret, outlines, errlines, duration): - self.ret = ret + try: + self.ret = pytest.ExitCode(ret) + except ValueError: + self.ret = ret self.outlines = outlines self.errlines = errlines self.stdout = LineMatcher(outlines) @@ -371,7 +374,7 @@ class RunResult: def __repr__(self): return ( - "" + "" % (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration) ) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index f8b0896c5..1a068c1d0 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -121,17 +121,6 @@ def test_runresult_assertion_on_xpassed(testdir): assert result.ret == 0 -def test_runresult_repr(): - from _pytest.pytester import RunResult - - assert ( - repr( - RunResult(ret="ret", outlines=[""], errlines=["some", "errors"], duration=1) - ) - == "" - ) - - def test_xpassed_with_strict_is_considered_a_failure(testdir): testdir.makepyfile( """ @@ -616,3 +605,22 @@ def test_spawn_uses_tmphome(testdir): child = testdir.spawn_pytest(str(p1)) out = child.read() assert child.wait() == 0, out.decode("utf8") + + +def test_run_result_repr(): + outlines = ["some", "normal", "output"] + errlines = ["some", "nasty", "errors", "happened"] + + # known exit code + r = pytester.RunResult(1, outlines, errlines, duration=0.5) + assert ( + repr(r) == "" + ) + + # unknown exit code: just the number + r = pytester.RunResult(99, outlines, errlines, duration=0.5) + assert ( + repr(r) == "" + ) From 92418b8d5d61a7728dba895dcbd76b1d2ffc77a3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 23 Oct 2019 21:27:07 -0300 Subject: [PATCH 042/153] Change #5061 changelog to 'improvement' --- changelog/{5061.feature.rst => 5061.improvement.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog/{5061.feature.rst => 5061.improvement.rst} (100%) diff --git a/changelog/5061.feature.rst b/changelog/5061.improvement.rst similarity index 100% rename from changelog/5061.feature.rst rename to changelog/5061.improvement.rst From 8ef4287bf0b2a0a6ff7e612ed48ba3e781f570d4 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 22 Oct 2019 01:33:43 +0200 Subject: [PATCH 043/153] pytester: align prefixes This is important for using another match_nickname, e.g. "re.match". TODO: - [ ] changelog - [ ] test --- changelog/6026.improvement.rst | 1 + src/_pytest/pytester.py | 25 ++++++++------ testing/test_pytester.py | 59 +++++++++++++++++++++++++--------- 3 files changed, 61 insertions(+), 24 deletions(-) create mode 100644 changelog/6026.improvement.rst diff --git a/changelog/6026.improvement.rst b/changelog/6026.improvement.rst new file mode 100644 index 000000000..34dfb278d --- /dev/null +++ b/changelog/6026.improvement.rst @@ -0,0 +1 @@ +Align prefixes in output of pytester's ``LineMatcher``. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index a050dad09..2974420f5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1344,7 +1344,6 @@ class LineMatcher: pattern :param str match_nickname: the nickname for the match function that will be logged to stdout when a match occurs - """ assert isinstance(lines2, Sequence) lines2 = self._getlines(lines2) @@ -1352,6 +1351,7 @@ class LineMatcher: nextline = None extralines = [] __tracebackhide__ = True + wnick = len(match_nickname) + 1 for line in lines2: nomatchprinted = False while lines1: @@ -1361,17 +1361,21 @@ class LineMatcher: break elif match_func(nextline, line): self._log("%s:" % match_nickname, repr(line)) - self._log(" with:", repr(nextline)) + self._log( + "{:>{width}}".format("with:", width=wnick), repr(nextline) + ) break else: if not nomatchprinted: - self._log("nomatch:", repr(line)) + self._log( + "{:>{width}}".format("nomatch:", width=wnick), repr(line) + ) nomatchprinted = True - self._log(" and:", repr(nextline)) + self._log("{:>{width}}".format("and:", width=wnick), repr(nextline)) extralines.append(nextline) else: self._log("remains unmatched: {!r}".format(line)) - pytest.fail(self._log_text) + pytest.fail(self._log_text.lstrip()) def no_fnmatch_line(self, pat): """Ensure captured lines do not match the given pattern, using ``fnmatch.fnmatch``. @@ -1396,16 +1400,19 @@ class LineMatcher: """ __tracebackhide__ = True nomatch_printed = False + wnick = len(match_nickname) + 1 try: for line in self.lines: if match_func(line, pat): self._log("%s:" % match_nickname, repr(pat)) - self._log(" with:", repr(line)) - pytest.fail(self._log_text) + self._log("{:>{width}}".format("with:", width=wnick), repr(line)) + pytest.fail(self._log_text.lstrip()) else: if not nomatch_printed: - self._log("nomatch:", repr(pat)) + self._log( + "{:>{width}}".format("nomatch:", width=wnick), repr(pat) + ) nomatch_printed = True - self._log(" and:", repr(line)) + self._log("{:>{width}}".format("and:", width=wnick), repr(line)) finally: self._log_output = [] diff --git a/testing/test_pytester.py b/testing/test_pytester.py index f8b0896c5..63710143a 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -457,16 +457,39 @@ def test_linematcher_with_nonlist(): assert lm._getlines(set()) == set() +def test_linematcher_match_failure(): + lm = LineMatcher(["foo", "foo", "bar"]) + with pytest.raises(pytest.fail.Exception) as e: + lm.fnmatch_lines(["foo", "f*", "baz"]) + assert e.value.msg.splitlines() == [ + "exact match: 'foo'", + "fnmatch: 'f*'", + " with: 'foo'", + "nomatch: 'baz'", + " and: 'bar'", + "remains unmatched: 'baz'", + ] + + lm = LineMatcher(["foo", "foo", "bar"]) + with pytest.raises(pytest.fail.Exception) as e: + lm.re_match_lines(["foo", "^f.*", "baz"]) + assert e.value.msg.splitlines() == [ + "exact match: 'foo'", + "re.match: '^f.*'", + " with: 'foo'", + " nomatch: 'baz'", + " and: 'bar'", + "remains unmatched: 'baz'", + ] + + @pytest.mark.parametrize("function", ["no_fnmatch_line", "no_re_match_line"]) def test_no_matching(function): - """""" if function == "no_fnmatch_line": - match_func_name = "fnmatch" good_pattern = "*.py OK*" bad_pattern = "*X.py OK*" else: assert function == "no_re_match_line" - match_func_name = "re.match" good_pattern = r".*py OK" bad_pattern = r".*Xpy OK" @@ -480,24 +503,30 @@ def test_no_matching(function): ] ) - def check_failure_lines(lines): - expected = [ - "nomatch: '{}'".format(good_pattern), - " and: 'cachedir: .pytest_cache'", - " and: 'collecting ... collected 1 item'", - " and: ''", - "{}: '{}'".format(match_func_name, good_pattern), - " with: 'show_fixtures_per_test.py OK'", - ] - assert lines == expected - # check the function twice to ensure we don't accumulate the internal buffer for i in range(2): with pytest.raises(pytest.fail.Exception) as e: func = getattr(lm, function) func(good_pattern) obtained = str(e.value).splitlines() - check_failure_lines(obtained) + if function == "no_fnmatch_line": + assert obtained == [ + "nomatch: '{}'".format(good_pattern), + " and: 'cachedir: .pytest_cache'", + " and: 'collecting ... collected 1 item'", + " and: ''", + "fnmatch: '{}'".format(good_pattern), + " with: 'show_fixtures_per_test.py OK'", + ] + else: + assert obtained == [ + "nomatch: '{}'".format(good_pattern), + " and: 'cachedir: .pytest_cache'", + " and: 'collecting ... collected 1 item'", + " and: ''", + "re.match: '{}'".format(good_pattern), + " with: 'show_fixtures_per_test.py OK'", + ] func = getattr(lm, function) func(bad_pattern) # bad pattern does not match any line: passes From ed9fda84d30850f71b7d7d5c831c9ab1aaf3b2c8 Mon Sep 17 00:00:00 2001 From: AnjoMan Date: Thu, 24 Oct 2019 21:07:36 -0400 Subject: [PATCH 044/153] Add tolerance to complex numbers --- changelog/6057.feature.rst | 3 +++ src/_pytest/python_api.py | 12 +++++------- testing/python/approx.py | 16 +++++++++++++++- 3 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 changelog/6057.feature.rst diff --git a/changelog/6057.feature.rst b/changelog/6057.feature.rst new file mode 100644 index 000000000..b7334e7fe --- /dev/null +++ b/changelog/6057.feature.rst @@ -0,0 +1,3 @@ +Add tolerances to complex values when printing ``pytest.approx``. + +For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index f03d45ab7..025a46076 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -223,26 +223,24 @@ class ApproxScalar(ApproxBase): def __repr__(self): """ Return a string communicating both the expected value and the tolerance - for the comparison being made, e.g. '1.0 +- 1e-6'. Use the unicode - plus/minus symbol if this is python3 (it's too hard to get right for - python2). + for the comparison being made, e.g. '1.0 ± 1e-6', '(3+4j) ± 5e-6 ∠ ±180°'. """ - if isinstance(self.expected, complex): - return str(self.expected) # Infinities aren't compared using tolerances, so don't show a # tolerance. - if math.isinf(self.expected): + if math.isinf(abs(self.expected)): return str(self.expected) # If a sensible tolerance can't be calculated, self.tolerance will # raise a ValueError. In this case, display '???'. try: vetted_tolerance = "{:.1e}".format(self.tolerance) + if isinstance(self.expected, complex) and not math.isinf(self.tolerance): + vetted_tolerance += " ∠ ±180°" except ValueError: vetted_tolerance = "???" - return "{} \u00b1 {}".format(self.expected, vetted_tolerance) + return "{} ± {}".format(self.expected, vetted_tolerance) def __eq__(self, actual): """ diff --git a/testing/python/approx.py b/testing/python/approx.py index 0575557ae..5900dee28 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -46,7 +46,6 @@ class TestApprox: assert repr(approx(1.0, rel=inf)) == "1.0 {pm} {infr}".format( pm=plus_minus, infr=infr ) - assert repr(approx(1.0j, rel=inf)) == "1j" # Dictionaries aren't ordered, so we need to check both orders. assert repr(approx({"a": 1.0, "b": 2.0})) in ( @@ -58,6 +57,21 @@ class TestApprox: ), ) + def test_repr_complex_numbers(self): + assert repr(approx(inf + 1j)) == "(inf+1j)" + assert repr(approx(1.0j, rel=inf)) == "1j ± inf" + + # can't compute a sensible tolerance + assert repr(approx(nan + 1j)) == "(nan+1j) ± ???" + + assert repr(approx(1.0j)) == "1j ± 1.0e-06 ∠ ±180°" + + # relative tolerance is scaled to |3+4j| = 5 + assert repr(approx(3 + 4 * 1j)) == "(3+4j) ± 5.0e-06 ∠ ±180°" + + # absolute tolerance is not scaled + assert repr(approx(3.3 + 4.4 * 1j, abs=0.02)) == "(3.3+4.4j) ± 2.0e-02 ∠ ±180°" + @pytest.mark.parametrize( "value, repr_string", [ From 34a02121adc2af5641b663c25804def9adb151e3 Mon Sep 17 00:00:00 2001 From: AnjoMan Date: Thu, 24 Oct 2019 21:20:27 -0400 Subject: [PATCH 045/153] Drop python 2 unicode tests for approx repr --- testing/python/approx.py | 51 +++++++++++++--------------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index 5900dee28..60fde151a 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -24,37 +24,18 @@ class MyDocTestRunner(doctest.DocTestRunner): class TestApprox: - @pytest.fixture - def plus_minus(self): - return "\u00b1" - - def test_repr_string(self, plus_minus): - tol1, tol2, infr = "1.0e-06", "2.0e-06", "inf" - assert repr(approx(1.0)) == "1.0 {pm} {tol1}".format(pm=plus_minus, tol1=tol1) - assert repr( - approx([1.0, 2.0]) - ) == "approx([1.0 {pm} {tol1}, 2.0 {pm} {tol2}])".format( - pm=plus_minus, tol1=tol1, tol2=tol2 - ) - assert repr( - approx((1.0, 2.0)) - ) == "approx((1.0 {pm} {tol1}, 2.0 {pm} {tol2}))".format( - pm=plus_minus, tol1=tol1, tol2=tol2 - ) + def test_repr_string(self): + assert repr(approx(1.0)) == "1.0 ± 1.0e-06" + assert repr(approx([1.0, 2.0])) == "approx([1.0 ± 1.0e-06, 2.0 ± 2.0e-06])" + assert repr(approx((1.0, 2.0))) == "approx((1.0 ± 1.0e-06, 2.0 ± 2.0e-06))" assert repr(approx(inf)) == "inf" - assert repr(approx(1.0, rel=nan)) == "1.0 {pm} ???".format(pm=plus_minus) - assert repr(approx(1.0, rel=inf)) == "1.0 {pm} {infr}".format( - pm=plus_minus, infr=infr - ) + assert repr(approx(1.0, rel=nan)) == "1.0 ± ???" + assert repr(approx(1.0, rel=inf)) == "1.0 ± inf" # Dictionaries aren't ordered, so we need to check both orders. assert repr(approx({"a": 1.0, "b": 2.0})) in ( - "approx({{'a': 1.0 {pm} {tol1}, 'b': 2.0 {pm} {tol2}}})".format( - pm=plus_minus, tol1=tol1, tol2=tol2 - ), - "approx({{'b': 2.0 {pm} {tol2}, 'a': 1.0 {pm} {tol1}}})".format( - pm=plus_minus, tol1=tol1, tol2=tol2 - ), + "approx({'a': 1.0 ± 1.0e-06, 'b': 2.0 ± 2.0e-06})", + "approx({'b': 2.0 ± 2.0e-06, 'a': 1.0 ± 1.0e-06})", ) def test_repr_complex_numbers(self): @@ -73,20 +54,20 @@ class TestApprox: assert repr(approx(3.3 + 4.4 * 1j, abs=0.02)) == "(3.3+4.4j) ± 2.0e-02 ∠ ±180°" @pytest.mark.parametrize( - "value, repr_string", + "value, expected_repr_string", [ - (5.0, "approx(5.0 {pm} 5.0e-06)"), - ([5.0], "approx([5.0 {pm} 5.0e-06])"), - ([[5.0]], "approx([[5.0 {pm} 5.0e-06]])"), - ([[5.0, 6.0]], "approx([[5.0 {pm} 5.0e-06, 6.0 {pm} 6.0e-06]])"), - ([[5.0], [6.0]], "approx([[5.0 {pm} 5.0e-06], [6.0 {pm} 6.0e-06]])"), + (5.0, "approx(5.0 ± 5.0e-06)"), + ([5.0], "approx([5.0 ± 5.0e-06])"), + ([[5.0]], "approx([[5.0 ± 5.0e-06]])"), + ([[5.0, 6.0]], "approx([[5.0 ± 5.0e-06, 6.0 ± 6.0e-06]])"), + ([[5.0], [6.0]], "approx([[5.0 ± 5.0e-06], [6.0 ± 6.0e-06]])"), ], ) - def test_repr_nd_array(self, plus_minus, value, repr_string): + def test_repr_nd_array(self, value, expected_repr_string): """Make sure that arrays of all different dimensions are repr'd correctly.""" np = pytest.importorskip("numpy") np_array = np.array(value) - assert repr(approx(np_array)) == repr_string.format(pm=plus_minus) + assert repr(approx(np_array)) == expected_repr_string def test_operator_overloading(self): assert 1 == approx(1, rel=1e-6, abs=1e-12) From 82753bec50fae4718fc6f38bdab3cf214f2be00f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 25 Oct 2019 05:59:10 +0200 Subject: [PATCH 046/153] terminal: report collection errors as "ERROR" in short summary --- changelog/6059.improvement.rst | 1 + src/_pytest/terminal.py | 8 +++++++- testing/test_terminal.py | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 changelog/6059.improvement.rst diff --git a/changelog/6059.improvement.rst b/changelog/6059.improvement.rst new file mode 100644 index 000000000..39ffff99b --- /dev/null +++ b/changelog/6059.improvement.rst @@ -0,0 +1 @@ +Collection errors are reported as errors (and not failures like before) in the terminal's short test summary. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 35f6d324b..e9d44f2a8 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -187,7 +187,13 @@ def pytest_report_teststatus(report): letter = "F" if report.when != "call": letter = "f" - return report.outcome, letter, report.outcome.upper() + + # Report failed CollectReports as "error" (in line with pytest_collectreport). + outcome = report.outcome + if report.when == "collect" and outcome == "failed": + outcome = "error" + + return outcome, letter, outcome.upper() @attr.s diff --git a/testing/test_terminal.py b/testing/test_terminal.py index a624be3b4..fdd53d3c6 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1776,3 +1776,20 @@ def test_format_session_duration(seconds, expected): from _pytest.terminal import format_session_duration assert format_session_duration(seconds) == expected + + +def test_collecterror(testdir): + p1 = testdir.makepyfile("raise SyntaxError()") + result = testdir.runpytest("-ra", str(p1)) + result.stdout.fnmatch_lines( + [ + "collected 0 items / 1 errors", + "*= ERRORS =*", + "*_ ERROR collecting test_collecterror.py _*", + "E SyntaxError: *", + "*= short test summary info =*", + "ERROR test_collecterror.py", + "*! Interrupted: 1 errors during collection !*", + "*= 1 error in *", + ] + ) From 1f5b454355d27bccda2ce1b3b145cfbde020fb76 Mon Sep 17 00:00:00 2001 From: Michael Krebs Date: Mon, 7 Oct 2019 17:56:28 -0400 Subject: [PATCH 047/153] Add log-auto-indent option to control multiline formatting --- AUTHORS | 1 + changelog/5515.feature.rst | 11 ++++ doc/en/reference.rst | 23 +++++++ src/_pytest/logging.py | 99 +++++++++++++++++++++++++++---- testing/logging/test_formatter.py | 68 ++++++++++++++++++++- 5 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 changelog/5515.feature.rst diff --git a/AUTHORS b/AUTHORS index 9f6ee048d..d96e6e713 100644 --- a/AUTHORS +++ b/AUTHORS @@ -177,6 +177,7 @@ Michael Aquilina Michael Birtwell Michael Droettboom Michael Goerz +Michael Krebs Michael Seifert Michal Wajszczuk Mihai Capotă diff --git a/changelog/5515.feature.rst b/changelog/5515.feature.rst new file mode 100644 index 000000000..b53097c43 --- /dev/null +++ b/changelog/5515.feature.rst @@ -0,0 +1,11 @@ +Allow selective auto-indentation of multiline log messages. + +Adds command line option ``--log-auto-indent``, config option +``log_auto_indent`` and support for per-entry configuration of +indentation behavior on calls to ``logging.log()``. + +Alters the default for auto-indention from ``on`` to ``off``. This +restores the older behavior that existed prior to v4.6.0. This +reversion to earlier behavior was done because it is better to +activate new features that may lead to broken tests explicitly +rather than implicitly. diff --git a/doc/en/reference.rst b/doc/en/reference.rst index 9c3a4c731..f90efc3a5 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1192,6 +1192,29 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] junit_suite_name = my_suite +.. confval:: log_auto_indent + + Allow selective auto-indentation of multiline log messages. + + Supports command line option ``--log-auto-indent [value]`` + and config option ``log_auto_indent = [value]`` to set the + auto-indentation behavior for all logging. + + ``[value]`` can be: + * True or "On" - Dynamically auto-indent multiline log messages + * False or "Off" or 0 - Do not auto-indent multiline log messages (the default behavior) + * [positive integer] - auto-indent multiline log messages by [value] spaces + + .. code-block:: ini + + [pytest] + log_auto_indent = False + + Supports passing kwarg ``extra={"auto_indent": [value]}`` to + calls to ``logging.log()`` to specify auto-indentation behavior for + a specific entry in the log. ``extra`` kwarg overrides the value specified + on the command line or in the config. + .. confval:: log_cli_date_format diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 2861baefd..f12ac13d8 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -7,6 +7,7 @@ import py import pytest from _pytest.compat import nullcontext +from _pytest.config import _strtobool from _pytest.config import create_terminal_writer from _pytest.pathlib import Path @@ -72,24 +73,87 @@ class PercentStyleMultiline(logging.PercentStyle): formats the message as if each line were logged separately. """ + def __init__(self, fmt, auto_indent): + super().__init__(fmt) + self._auto_indent = self._get_auto_indent(auto_indent) + @staticmethod def _update_message(record_dict, message): tmp = record_dict.copy() tmp["message"] = message return tmp + @staticmethod + def _get_auto_indent(auto_indent_option) -> int: + """Determines the current auto indentation setting + + Specify auto indent behavior (on/off/fixed) by passing in + extra={"auto_indent": [value]} to the call to logging.log() or + using a --log-auto-indent [value] command line or the + log_auto_indent [value] config option. + + Default behavior is auto-indent off. + + Using the string "True" or "on" or the boolean True as the value + turns auto indent on, using the string "False" or "off" or the + boolean False or the int 0 turns it off, and specifying a + positive integer fixes the indentation position to the value + specified. + + Any other values for the option are invalid, and will silently be + converted to the default. + + :param any auto_indent_option: User specified option for indentation + from command line, config or extra kwarg. Accepts int, bool or str. + str option accepts the same range of values as boolean config options, + as well as positive integers represented in str form. + + :returns: indentation value, which can be + -1 (automatically determine indentation) or + 0 (auto-indent turned off) or + >0 (explicitly set indentation position). + """ + + if type(auto_indent_option) is int: + return int(auto_indent_option) + elif type(auto_indent_option) is str: + try: + return int(auto_indent_option) + except ValueError: + pass + try: + if _strtobool(auto_indent_option): + return -1 + except ValueError: + return 0 + elif type(auto_indent_option) is bool: + if auto_indent_option: + return -1 + + return 0 + def format(self, record): if "\n" in record.message: - lines = record.message.splitlines() - formatted = self._fmt % self._update_message(record.__dict__, lines[0]) - # TODO optimize this by introducing an option that tells the - # logging framework that the indentation doesn't - # change. This allows to compute the indentation only once. - indentation = _remove_ansi_escape_sequences(formatted).find(lines[0]) - lines[0] = formatted - return ("\n" + " " * indentation).join(lines) - else: - return self._fmt % record.__dict__ + if hasattr(record, "auto_indent"): + # passed in from the "extra={}" kwarg on the call to logging.log() + auto_indent = self._get_auto_indent(record.auto_indent) + else: + auto_indent = self._auto_indent + + if auto_indent: + lines = record.message.splitlines() + formatted = self._fmt % self._update_message(record.__dict__, lines[0]) + + if auto_indent < 0: + indentation = _remove_ansi_escape_sequences(formatted).find( + lines[0] + ) + else: + # optimizes logging by allowing a fixed indentation + indentation = auto_indent + lines[0] = formatted + return ("\n" + " " * indentation).join(lines) + return self._fmt % record.__dict__ def get_option_ini(config, *names): @@ -183,6 +247,12 @@ def pytest_addoption(parser): default=DEFAULT_LOG_DATE_FORMAT, help="log date format as used by the logging module.", ) + add_option_ini( + "--log-auto-indent", + dest="log_auto_indent", + default=None, + help="Auto-indent multiline messages passed to the logging module. Accepts true|on, false|off or an integer.", + ) @contextmanager @@ -413,6 +483,7 @@ class LoggingPlugin: self.formatter = self._create_formatter( get_option_ini(config, "log_format"), get_option_ini(config, "log_date_format"), + get_option_ini(config, "log_auto_indent"), ) self.log_level = get_actual_log_level(config, "log_level") @@ -444,7 +515,7 @@ class LoggingPlugin: if self._log_cli_enabled(): self._setup_cli_logging() - def _create_formatter(self, log_format, log_date_format): + def _create_formatter(self, log_format, log_date_format, auto_indent): # color option doesn't exist if terminal plugin is disabled color = getattr(self._config.option, "color", "no") if color != "no" and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search( @@ -456,7 +527,10 @@ class LoggingPlugin: else: formatter = logging.Formatter(log_format, log_date_format) - formatter._style = PercentStyleMultiline(formatter._style._fmt) + formatter._style = PercentStyleMultiline( + formatter._style._fmt, auto_indent=auto_indent + ) + return formatter def _setup_cli_logging(self): @@ -473,6 +547,7 @@ class LoggingPlugin: log_cli_formatter = self._create_formatter( get_option_ini(config, "log_cli_format", "log_format"), get_option_ini(config, "log_cli_date_format", "log_date_format"), + get_option_ini(config, "log_auto_indent"), ) log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level") diff --git a/testing/logging/test_formatter.py b/testing/logging/test_formatter.py index 6850a83cd..b363e8b03 100644 --- a/testing/logging/test_formatter.py +++ b/testing/logging/test_formatter.py @@ -53,13 +53,77 @@ def test_multiline_message(): # this is called by logging.Formatter.format record.message = record.getMessage() - style = PercentStyleMultiline(logfmt) - output = style.format(record) + ai_on_style = PercentStyleMultiline(logfmt, True) + output = ai_on_style.format(record) assert output == ( "dummypath 10 INFO Test Message line1\n" " line2" ) + ai_off_style = PercentStyleMultiline(logfmt, False) + output = ai_off_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\nline2" + ) + + ai_none_style = PercentStyleMultiline(logfmt, None) + output = ai_none_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\nline2" + ) + + record.auto_indent = False + output = ai_on_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\nline2" + ) + + record.auto_indent = True + output = ai_off_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\n" + " line2" + ) + + record.auto_indent = "False" + output = ai_on_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\nline2" + ) + + record.auto_indent = "True" + output = ai_off_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\n" + " line2" + ) + + # bad string values default to False + record.auto_indent = "junk" + output = ai_off_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\nline2" + ) + + # anything other than string or int will default to False + record.auto_indent = dict() + output = ai_off_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\nline2" + ) + + record.auto_indent = "5" + output = ai_off_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\n line2" + ) + + record.auto_indent = 5 + output = ai_off_style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\n line2" + ) + def test_colored_short_level(): logfmt = "%(levelname).1s %(message)s" From 3c7fbe2d8b4e3a0f3d9dcd7fbdf6ac12de181ef8 Mon Sep 17 00:00:00 2001 From: Anton Lodder Date: Fri, 25 Oct 2019 12:03:03 -0400 Subject: [PATCH 048/153] Document evaluating complex number for infinity --- src/_pytest/python_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 025a46076..52a91a905 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -227,7 +227,7 @@ class ApproxScalar(ApproxBase): """ # Infinities aren't compared using tolerances, so don't show a - # tolerance. + # tolerance. Need to call abs to handle complex numbers, e.g. (inf + 1j) if math.isinf(abs(self.expected)): return str(self.expected) From f93f284356c73e35ef3f50076496538bbdce4b6b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 18 Sep 2019 20:29:24 -0300 Subject: [PATCH 049/153] Support sys.pycache_prefix on py38 Fix #4730 --- changelog/4730.feature.rst | 3 ++ src/_pytest/assertion/rewrite.py | 46 ++++++++++++++++-------- testing/test_assertrewrite.py | 61 +++++++++++++++++++++++++++++++- 3 files changed, 94 insertions(+), 16 deletions(-) create mode 100644 changelog/4730.feature.rst diff --git a/changelog/4730.feature.rst b/changelog/4730.feature.rst new file mode 100644 index 000000000..80d1c4a38 --- /dev/null +++ b/changelog/4730.feature.rst @@ -0,0 +1,3 @@ +When ``sys.pycache_prefix`` (Python 3.8+) is set, it will be used by pytest to cache test files changed by the assertion rewriting mechanism. + +This makes it easier to benefit of cached ``.pyc`` files even on file systems without permissions. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 4e7db8369..6a4b24da1 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -13,6 +13,7 @@ import struct import sys import tokenize import types +from pathlib import Path from typing import Dict from typing import List from typing import Optional @@ -30,7 +31,7 @@ from _pytest.assertion.util import ( # noqa: F401 from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import PurePath -# pytest caches rewritten pycs in __pycache__. +# pytest caches rewritten pycs in pycache dirs PYTEST_TAG = "{}-pytest-{}".format(sys.implementation.cache_tag, version) PYC_EXT = ".py" + (__debug__ and "c" or "o") PYC_TAIL = "." + PYTEST_TAG + PYC_EXT @@ -103,7 +104,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder): return None # default behaviour is fine def exec_module(self, module): - fn = module.__spec__.origin + fn = Path(module.__spec__.origin) state = self.config._assertstate self._rewritten_names.add(module.__name__) @@ -117,15 +118,15 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder): # cached pyc is always a complete, valid pyc. Operations on it must be # atomic. POSIX's atomic rename comes in handy. write = not sys.dont_write_bytecode - cache_dir = os.path.join(os.path.dirname(fn), "__pycache__") + cache_dir = get_cache_dir(fn) if write: ok = try_mkdir(cache_dir) if not ok: write = False - state.trace("read only directory: {}".format(os.path.dirname(fn))) + state.trace("read only directory: {}".format(cache_dir)) - cache_name = os.path.basename(fn)[:-3] + PYC_TAIL - pyc = os.path.join(cache_dir, cache_name) + cache_name = fn.name[:-3] + PYC_TAIL + pyc = cache_dir / cache_name # Notice that even if we're in a read-only directory, I'm going # to check for a cached pyc. This may not be optimal... co = _read_pyc(fn, pyc, state.trace) @@ -139,7 +140,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder): finally: self._writing_pyc = False else: - state.trace("found cached rewritten pyc for {!r}".format(fn)) + state.trace("found cached rewritten pyc for {}".format(fn)) exec(co, module.__dict__) def _early_rewrite_bailout(self, name, state): @@ -258,7 +259,7 @@ def _write_pyc(state, co, source_stat, pyc): # (C)Python, since these "pycs" should never be seen by builtin # import. However, there's little reason deviate. try: - with atomicwrites.atomic_write(pyc, mode="wb", overwrite=True) as fp: + with atomicwrites.atomic_write(str(pyc), mode="wb", overwrite=True) as fp: fp.write(importlib.util.MAGIC_NUMBER) # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) mtime = int(source_stat.st_mtime) & 0xFFFFFFFF @@ -269,7 +270,7 @@ def _write_pyc(state, co, source_stat, pyc): except EnvironmentError as e: state.trace("error writing pyc file at {}: errno={}".format(pyc, e.errno)) # we ignore any failure to write the cache file - # there are many reasons, permission-denied, __pycache__ being a + # there are many reasons, permission-denied, pycache dir being a # file etc. return False return True @@ -277,6 +278,7 @@ def _write_pyc(state, co, source_stat, pyc): def _rewrite_test(fn, config): """read and rewrite *fn* and return the code object.""" + fn = str(fn) stat = os.stat(fn) with open(fn, "rb") as f: source = f.read() @@ -292,12 +294,12 @@ def _read_pyc(source, pyc, trace=lambda x: None): Return rewritten code if successful or None if not. """ try: - fp = open(pyc, "rb") + fp = open(str(pyc), "rb") except IOError: return None with fp: try: - stat_result = os.stat(source) + stat_result = os.stat(str(source)) mtime = int(stat_result.st_mtime) size = stat_result.st_size data = fp.read(12) @@ -749,7 +751,7 @@ class AssertionRewriter(ast.NodeVisitor): "assertion is always true, perhaps remove parentheses?" ), category=None, - filename=self.module_path, + filename=str(self.module_path), lineno=assert_.lineno, ) @@ -872,7 +874,7 @@ warn_explicit( lineno={lineno}, ) """.format( - filename=module_path, lineno=lineno + filename=str(module_path), lineno=lineno ) ).body return ast.If(val_is_none, send_warning, []) @@ -1021,9 +1023,9 @@ warn_explicit( def try_mkdir(cache_dir): """Attempts to create the given directory, returns True if successful""" try: - os.mkdir(cache_dir) + os.makedirs(str(cache_dir)) except FileExistsError: - # Either the __pycache__ directory already exists (the + # Either the pycache directory already exists (the # common case) or it's blocked by a non-dir node. In the # latter case, we'll ignore it in _write_pyc. return True @@ -1039,3 +1041,17 @@ def try_mkdir(cache_dir): return False raise return True + + +def get_cache_dir(file_path: Path) -> Path: + """Returns the cache directory to write .pyc files for the given .py file path""" + if sys.version_info >= (3, 8) and sys.pycache_prefix: + # given: + # prefix = '/tmp/pycs' + # path = '/home/user/proj/test_app.py' + # we want: + # '/tmp/pycs/home/user/proj' + return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) + else: + # classic pycache directory + return file_path.parent / "__pycache__" diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 3555d8252..f00f25ff0 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -9,6 +9,7 @@ import sys import textwrap import zipfile from functools import partial +from pathlib import Path import py @@ -17,6 +18,8 @@ import pytest from _pytest.assertion import util from _pytest.assertion.rewrite import _get_assertion_exprs from _pytest.assertion.rewrite import AssertionRewritingHook +from _pytest.assertion.rewrite import get_cache_dir +from _pytest.assertion.rewrite import PYC_TAIL from _pytest.assertion.rewrite import PYTEST_TAG from _pytest.assertion.rewrite import rewrite_asserts from _pytest.main import ExitCode @@ -1564,7 +1567,7 @@ def test_try_mkdir(monkeypatch, tmp_path): assert try_mkdir(str(p)) # monkeypatch to simulate all error situations - def fake_mkdir(p, *, exc): + def fake_mkdir(p, mode, *, exc): assert isinstance(p, str) raise exc @@ -1589,3 +1592,59 @@ def test_try_mkdir(monkeypatch, tmp_path): with pytest.raises(OSError) as exc_info: try_mkdir(str(p)) assert exc_info.value.errno == errno.ECHILD + + +class TestPyCacheDir: + @pytest.mark.parametrize( + "prefix, source, expected", + [ + ("c:/tmp/pycs", "d:/projects/src/foo.py", "c:/tmp/pycs/projects/src"), + (None, "d:/projects/src/foo.py", "d:/projects/src/__pycache__"), + ("/tmp/pycs", "/home/projects/src/foo.py", "/tmp/pycs/home/projects/src"), + (None, "/home/projects/src/foo.py", "/home/projects/src/__pycache__"), + ], + ) + def test_get_cache_dir(self, monkeypatch, prefix, source, expected): + if prefix: + if sys.version_info < (3, 8): + pytest.skip("pycache_prefix not available in py<38") + monkeypatch.setattr(sys, "pycache_prefix", prefix) + + assert get_cache_dir(Path(source)) == Path(expected) + + @pytest.mark.skipif( + sys.version_info < (3, 8), reason="pycache_prefix not available in py<38" + ) + def test_sys_pycache_prefix_integration(self, tmp_path, monkeypatch, testdir): + """Integration test for sys.pycache_prefix (#4730).""" + pycache_prefix = tmp_path / "my/pycs" + monkeypatch.setattr(sys, "pycache_prefix", str(pycache_prefix)) + monkeypatch.setattr(sys, "dont_write_bytecode", False) + + testdir.makepyfile( + **{ + "src/test_foo.py": """ + import bar + def test_foo(): + pass + """, + "src/bar/__init__.py": "", + } + ) + result = testdir.runpytest() + assert result.ret == 0 + + test_foo = Path(testdir.tmpdir) / "src/test_foo.py" + bar_init = Path(testdir.tmpdir) / "src/bar/__init__.py" + assert test_foo.is_file() + assert bar_init.is_file() + + # test file: rewritten, custom pytest cache tag + test_foo_pyc = get_cache_dir(test_foo) / ("test_foo" + PYC_TAIL) + assert test_foo_pyc.is_file() + + # normal file: not touched by pytest, normal cache tag + bar_init_pyc = get_cache_dir(bar_init) / "__init__.{cache_tag}.pyc".format( + cache_tag=sys.implementation.cache_tag + ) + assert bar_init_pyc.is_file() From 6f20b4b014f5e01a64f811372625284e2e8b08fe Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 21 Sep 2019 14:38:39 -0300 Subject: [PATCH 050/153] Introduce compat.fspath --- src/_pytest/assertion/rewrite.py | 34 +++++++++++++++----------------- src/_pytest/compat.py | 14 +++++++++++++ testing/test_assertrewrite.py | 30 ++++++++++++++-------------- 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 6a4b24da1..9c9d6135b 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -28,6 +28,7 @@ from _pytest.assertion import util from _pytest.assertion.util import ( # noqa: F401 format_explanation as _format_explanation, ) +from _pytest.compat import fspath from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import PurePath @@ -120,7 +121,7 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder): write = not sys.dont_write_bytecode cache_dir = get_cache_dir(fn) if write: - ok = try_mkdir(cache_dir) + ok = try_makedirs(cache_dir) if not ok: write = False state.trace("read only directory: {}".format(cache_dir)) @@ -259,7 +260,7 @@ def _write_pyc(state, co, source_stat, pyc): # (C)Python, since these "pycs" should never be seen by builtin # import. However, there's little reason deviate. try: - with atomicwrites.atomic_write(str(pyc), mode="wb", overwrite=True) as fp: + with atomicwrites.atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp: fp.write(importlib.util.MAGIC_NUMBER) # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) mtime = int(source_stat.st_mtime) & 0xFFFFFFFF @@ -278,7 +279,7 @@ def _write_pyc(state, co, source_stat, pyc): def _rewrite_test(fn, config): """read and rewrite *fn* and return the code object.""" - fn = str(fn) + fn = fspath(fn) stat = os.stat(fn) with open(fn, "rb") as f: source = f.read() @@ -294,12 +295,12 @@ def _read_pyc(source, pyc, trace=lambda x: None): Return rewritten code if successful or None if not. """ try: - fp = open(str(pyc), "rb") + fp = open(fspath(pyc), "rb") except IOError: return None with fp: try: - stat_result = os.stat(str(source)) + stat_result = os.stat(fspath(source)) mtime = int(stat_result.st_mtime) size = stat_result.st_size data = fp.read(12) @@ -751,7 +752,7 @@ class AssertionRewriter(ast.NodeVisitor): "assertion is always true, perhaps remove parentheses?" ), category=None, - filename=str(self.module_path), + filename=fspath(self.module_path), lineno=assert_.lineno, ) @@ -874,7 +875,7 @@ warn_explicit( lineno={lineno}, ) """.format( - filename=str(module_path), lineno=lineno + filename=fspath(module_path), lineno=lineno ) ).body return ast.If(val_is_none, send_warning, []) @@ -1020,18 +1021,15 @@ warn_explicit( return res, self.explanation_param(self.pop_format_context(expl_call)) -def try_mkdir(cache_dir): - """Attempts to create the given directory, returns True if successful""" +def try_makedirs(cache_dir) -> bool: + """Attempts to create the given directory and sub-directories exist, returns True if + successful or it already exists""" try: - os.makedirs(str(cache_dir)) - except FileExistsError: - # Either the pycache directory already exists (the - # common case) or it's blocked by a non-dir node. In the - # latter case, we'll ignore it in _write_pyc. - return True - except (FileNotFoundError, NotADirectoryError): - # One of the path components was not a directory, likely - # because we're in a zip file. + os.makedirs(fspath(cache_dir), exist_ok=True) + except (FileNotFoundError, NotADirectoryError, FileExistsError): + # One of the path components was not a directory: + # - we're in a zip file + # - it is a file return False except PermissionError: return False diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 3898fb252..83947d3eb 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -4,6 +4,7 @@ python version compatibility code import functools import inspect import io +import os import re import sys from contextlib import contextmanager @@ -41,6 +42,19 @@ def _format_args(func): REGEX_TYPE = type(re.compile("")) +if sys.version_info < (3, 6): + + def fspath(p): + """os.fspath replacement, useful to point out when we should replace it by the + real function once we drop py35. + """ + return str(p) + + +else: + fspath = os.fspath + + def is_generator(func): genfunc = inspect.isgeneratorfunction(func) return genfunc and not iscoroutinefunction(func) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index f00f25ff0..6c5de5c03 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1554,43 +1554,43 @@ def test_get_assertion_exprs(src, expected): assert _get_assertion_exprs(src) == expected -def test_try_mkdir(monkeypatch, tmp_path): - from _pytest.assertion.rewrite import try_mkdir +def test_try_makedirs(monkeypatch, tmp_path): + from _pytest.assertion.rewrite import try_makedirs p = tmp_path / "foo" # create - assert try_mkdir(str(p)) + assert try_makedirs(str(p)) assert p.is_dir() # already exist - assert try_mkdir(str(p)) + assert try_makedirs(str(p)) # monkeypatch to simulate all error situations - def fake_mkdir(p, mode, *, exc): + def fake_mkdir(p, exist_ok=False, *, exc): assert isinstance(p, str) raise exc - monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=FileNotFoundError())) - assert not try_mkdir(str(p)) + monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=FileNotFoundError())) + assert not try_makedirs(str(p)) - monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=NotADirectoryError())) - assert not try_mkdir(str(p)) + monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=NotADirectoryError())) + assert not try_makedirs(str(p)) - monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=PermissionError())) - assert not try_mkdir(str(p)) + monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=PermissionError())) + assert not try_makedirs(str(p)) err = OSError() err.errno = errno.EROFS - monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=err)) - assert not try_mkdir(str(p)) + monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err)) + assert not try_makedirs(str(p)) # unhandled OSError should raise err = OSError() err.errno = errno.ECHILD - monkeypatch.setattr(os, "mkdir", partial(fake_mkdir, exc=err)) + monkeypatch.setattr(os, "makedirs", partial(fake_mkdir, exc=err)) with pytest.raises(OSError) as exc_info: - try_mkdir(str(p)) + try_makedirs(str(p)) assert exc_info.value.errno == errno.ECHILD From 928587da60130889e7902c6d21e37ff82fb752bd Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 26 Oct 2019 13:01:14 -0300 Subject: [PATCH 051/153] Change 5924 and 5936 changelog entries to improvement [ci skip] --- changelog/{5924.feature.rst => 5924.improvement.rst} | 0 changelog/{5936.feature.rst => 5936.improvement.rst} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename changelog/{5924.feature.rst => 5924.improvement.rst} (100%) rename changelog/{5936.feature.rst => 5936.improvement.rst} (100%) diff --git a/changelog/5924.feature.rst b/changelog/5924.improvement.rst similarity index 100% rename from changelog/5924.feature.rst rename to changelog/5924.improvement.rst diff --git a/changelog/5936.feature.rst b/changelog/5936.improvement.rst similarity index 100% rename from changelog/5936.feature.rst rename to changelog/5936.improvement.rst From 59a59f371b303e8825b07fdd6567609b698d0092 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Oct 2019 20:07:44 +0300 Subject: [PATCH 052/153] Add type annotations to _pytest.pathlib At least the ones I was sure of. --- src/_pytest/pathlib.py | 61 ++++++++++++++++++++++++++---------------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index f45b0bab7..543103fb5 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -1,7 +1,6 @@ import atexit import fnmatch import itertools -import operator import os import shutil import sys @@ -13,6 +12,11 @@ from os.path import expandvars from os.path import isabs from os.path import sep from posixpath import sep as posix_sep +from typing import Iterable +from typing import Iterator +from typing import Set +from typing import TypeVar +from typing import Union from _pytest.warning_types import PytestWarning @@ -26,10 +30,15 @@ __all__ = ["Path", "PurePath"] LOCK_TIMEOUT = 60 * 60 * 3 -get_lock_path = operator.methodcaller("joinpath", ".lock") + +_AnyPurePath = TypeVar("_AnyPurePath", bound=PurePath) -def ensure_reset_dir(path): +def get_lock_path(path: _AnyPurePath) -> _AnyPurePath: + return path.joinpath(".lock") + + +def ensure_reset_dir(path: Path) -> None: """ ensures the given path is an empty directory """ @@ -38,7 +47,7 @@ def ensure_reset_dir(path): path.mkdir() -def on_rm_rf_error(func, path: str, exc, *, start_path) -> bool: +def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: """Handles known read-only errors during rmtree. The returned value is used only by our own tests. @@ -71,7 +80,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path) -> bool: # Chmod + retry. import stat - def chmod_rw(p: str): + def chmod_rw(p: str) -> None: mode = os.stat(p).st_mode os.chmod(p, mode | stat.S_IRUSR | stat.S_IWUSR) @@ -90,7 +99,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path) -> bool: return True -def rm_rf(path: Path): +def rm_rf(path: Path) -> None: """Remove the path contents recursively, even if some elements are read-only. """ @@ -98,7 +107,7 @@ def rm_rf(path: Path): shutil.rmtree(str(path), onerror=onerror) -def find_prefixed(root, prefix): +def find_prefixed(root: Path, prefix: str) -> Iterator[Path]: """finds all elements in root that begin with the prefix, case insensitive""" l_prefix = prefix.lower() for x in root.iterdir(): @@ -106,7 +115,7 @@ def find_prefixed(root, prefix): yield x -def extract_suffixes(iter, prefix): +def extract_suffixes(iter: Iterable[PurePath], prefix: str) -> Iterator[str]: """ :param iter: iterator over path names :param prefix: expected prefix of the path names @@ -117,13 +126,13 @@ def extract_suffixes(iter, prefix): yield p.name[p_len:] -def find_suffixes(root, prefix): +def find_suffixes(root: Path, prefix: str) -> Iterator[str]: """combines find_prefixes and extract_suffixes """ return extract_suffixes(find_prefixed(root, prefix), prefix) -def parse_num(maybe_num): +def parse_num(maybe_num) -> int: """parses number path suffixes, returns -1 on error""" try: return int(maybe_num) @@ -131,7 +140,9 @@ def parse_num(maybe_num): return -1 -def _force_symlink(root, target, link_to): +def _force_symlink( + root: Path, target: Union[str, PurePath], link_to: Union[str, Path] +) -> None: """helper to create the current symlink it's full of race conditions that are reasonably ok to ignore @@ -151,7 +162,7 @@ def _force_symlink(root, target, link_to): pass -def make_numbered_dir(root, prefix): +def make_numbered_dir(root: Path, prefix: str) -> Path: """create a directory with an increased number as suffix for the given prefix""" for i in range(10): # try up to 10 times to create the folder @@ -172,7 +183,7 @@ def make_numbered_dir(root, prefix): ) -def create_cleanup_lock(p): +def create_cleanup_lock(p: Path) -> Path: """crates a lock to prevent premature folder cleanup""" lock_path = get_lock_path(p) try: @@ -189,11 +200,11 @@ def create_cleanup_lock(p): return lock_path -def register_cleanup_lock_removal(lock_path, register=atexit.register): +def register_cleanup_lock_removal(lock_path: Path, register=atexit.register): """registers a cleanup function for removing a lock, by default on atexit""" pid = os.getpid() - def cleanup_on_exit(lock_path=lock_path, original_pid=pid): + def cleanup_on_exit(lock_path: Path = lock_path, original_pid: int = pid) -> None: current_pid = os.getpid() if current_pid != original_pid: # fork @@ -206,7 +217,7 @@ def register_cleanup_lock_removal(lock_path, register=atexit.register): return register(cleanup_on_exit) -def maybe_delete_a_numbered_dir(path): +def maybe_delete_a_numbered_dir(path: Path) -> None: """removes a numbered directory if its lock can be obtained and it does not seem to be in use""" lock_path = None try: @@ -232,7 +243,7 @@ def maybe_delete_a_numbered_dir(path): pass -def ensure_deletable(path, consider_lock_dead_if_created_before): +def ensure_deletable(path: Path, consider_lock_dead_if_created_before: float) -> bool: """checks if a lock exists and breaks it if its considered dead""" if path.is_symlink(): return False @@ -251,13 +262,13 @@ def ensure_deletable(path, consider_lock_dead_if_created_before): return False -def try_cleanup(path, consider_lock_dead_if_created_before): +def try_cleanup(path: Path, consider_lock_dead_if_created_before: float) -> None: """tries to cleanup a folder if we can ensure it's deletable""" if ensure_deletable(path, consider_lock_dead_if_created_before): maybe_delete_a_numbered_dir(path) -def cleanup_candidates(root, prefix, keep): +def cleanup_candidates(root: Path, prefix: str, keep: int) -> Iterator[Path]: """lists candidates for numbered directories to be removed - follows py.path""" max_existing = max(map(parse_num, find_suffixes(root, prefix)), default=-1) max_delete = max_existing - keep @@ -269,7 +280,9 @@ def cleanup_candidates(root, prefix, keep): yield path -def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_before): +def cleanup_numbered_dir( + root: Path, prefix: str, keep: int, consider_lock_dead_if_created_before: float +) -> None: """cleanup for lock driven numbered directories""" for path in cleanup_candidates(root, prefix, keep): try_cleanup(path, consider_lock_dead_if_created_before) @@ -277,7 +290,9 @@ def cleanup_numbered_dir(root, prefix, keep, consider_lock_dead_if_created_befor try_cleanup(path, consider_lock_dead_if_created_before) -def make_numbered_dir_with_cleanup(root, prefix, keep, lock_timeout): +def make_numbered_dir_with_cleanup( + root: Path, prefix: str, keep: int, lock_timeout: float +) -> Path: """creates a numbered dir with a cleanup lock and removes old ones""" e = None for i in range(10): @@ -311,7 +326,7 @@ def resolve_from_str(input, root): return root.joinpath(input) -def fnmatch_ex(pattern, path): +def fnmatch_ex(pattern: str, path) -> bool: """FNMatcher port from py.path.common which works with PurePath() instances. The difference between this algorithm and PurePath.match() is that the latter matches "**" glob expressions @@ -346,6 +361,6 @@ def fnmatch_ex(pattern, path): return fnmatch.fnmatch(name, pattern) -def parts(s): +def parts(s: str) -> Set[str]: parts = s.split(sep) return {sep.join(parts[: i + 1]) or sep for i in range(len(parts))} From 00a278cdb4549973998b58206ca020a87c38fcee Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Oct 2019 20:26:19 +0300 Subject: [PATCH 053/153] Add type annotations to _pytest.tmpdir At least the ones I was able to. --- src/_pytest/tmpdir.py | 35 ++++++++++++++++++++--------------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index 123a583ad..bd8fb7d8a 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -2,6 +2,7 @@ import os import re import tempfile +from typing import Optional import attr import py @@ -12,6 +13,7 @@ from .pathlib import LOCK_TIMEOUT from .pathlib import make_numbered_dir from .pathlib import make_numbered_dir_with_cleanup from .pathlib import Path +from _pytest.fixtures import FixtureRequest from _pytest.monkeypatch import MonkeyPatch @@ -22,19 +24,20 @@ class TempPathFactory: The base directory can be configured using the ``--basetemp`` option.""" _given_basetemp = attr.ib( + type=Path, # using os.path.abspath() to get absolute path instead of resolve() as it # does not work the same in all platforms (see #4427) # Path.absolute() exists, but it is not public (see https://bugs.python.org/issue25012) # Ignore type because of https://github.com/python/mypy/issues/6172. converter=attr.converters.optional( lambda p: Path(os.path.abspath(str(p))) # type: ignore - ) + ), ) _trace = attr.ib() - _basetemp = attr.ib(default=None) + _basetemp = attr.ib(type=Optional[Path], default=None) @classmethod - def from_config(cls, config): + def from_config(cls, config) -> "TempPathFactory": """ :param config: a pytest configuration """ @@ -42,7 +45,7 @@ class TempPathFactory: given_basetemp=config.option.basetemp, trace=config.trace.get("tmpdir") ) - def mktemp(self, basename, numbered=True): + def mktemp(self, basename: str, numbered: bool = True) -> Path: """makes a temporary directory managed by the factory""" if not numbered: p = self.getbasetemp().joinpath(basename) @@ -52,7 +55,7 @@ class TempPathFactory: self._trace("mktemp", p) return p - def getbasetemp(self): + def getbasetemp(self) -> Path: """ return base temporary directory. """ if self._basetemp is not None: return self._basetemp @@ -85,9 +88,9 @@ class TempdirFactory: :class:``py.path.local`` for :class:``TempPathFactory`` """ - _tmppath_factory = attr.ib() + _tmppath_factory = attr.ib(type=TempPathFactory) - def mktemp(self, basename, numbered=True): + def mktemp(self, basename: str, numbered: bool = True): """Create a subdirectory of the base temporary directory and return it. If ``numbered``, ensure the directory is unique by adding a number prefix greater than any existing one. @@ -99,7 +102,7 @@ class TempdirFactory: return py.path.local(self._tmppath_factory.getbasetemp().resolve()) -def get_user(): +def get_user() -> Optional[str]: """Return the current user name, or None if getuser() does not work in the current environment (see #1010). """ @@ -111,7 +114,7 @@ def get_user(): return None -def pytest_configure(config): +def pytest_configure(config) -> None: """Create a TempdirFactory and attach it to the config object. This is to comply with existing plugins which expect the handler to be @@ -127,20 +130,22 @@ def pytest_configure(config): @pytest.fixture(scope="session") -def tmpdir_factory(request): +def tmpdir_factory(request: FixtureRequest) -> TempdirFactory: """Return a :class:`_pytest.tmpdir.TempdirFactory` instance for the test session. """ - return request.config._tmpdirhandler + # Set dynamically by pytest_configure() above. + return request.config._tmpdirhandler # type: ignore @pytest.fixture(scope="session") -def tmp_path_factory(request): +def tmp_path_factory(request: FixtureRequest) -> TempPathFactory: """Return a :class:`_pytest.tmpdir.TempPathFactory` instance for the test session. """ - return request.config._tmp_path_factory + # Set dynamically by pytest_configure() above. + return request.config._tmp_path_factory # type: ignore -def _mk_tmp(request, factory): +def _mk_tmp(request: FixtureRequest, factory: TempPathFactory) -> Path: name = request.node.name name = re.sub(r"[\W]", "_", name) MAXVAL = 30 @@ -162,7 +167,7 @@ def tmpdir(tmp_path): @pytest.fixture -def tmp_path(request, tmp_path_factory): +def tmp_path(request: FixtureRequest, tmp_path_factory: TempPathFactory) -> Path: """Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary From be514178d00082ef52e2036e5ae31eecb423881b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 00:15:33 +0200 Subject: [PATCH 054/153] tox: remove platform restriction, only used for pexpect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This would prevent TOXENV=py37-pexpect-… from running on e.g. FreeBSD. And even on Windows it is pytest's job of skipping the tests then. This was probably still from when the pexpect env was only running pexpect-based tests. --- tox.ini | 3 --- 1 file changed, 3 deletions(-) diff --git a/tox.ini b/tox.ini index edc9a5667..e3012d280 100644 --- a/tox.ini +++ b/tox.ini @@ -36,8 +36,6 @@ setenv = lsof: _PYTEST_TOX_POSARGS_LSOF=--lsof - pexpect: _PYTEST_TOX_PLATFORM=linux|darwin - xdist: _PYTEST_TOX_POSARGS_XDIST=-n auto extras = testing deps = @@ -49,7 +47,6 @@ deps = twisted: twisted xdist: pytest-xdist>=1.13 {env:_PYTEST_TOX_EXTRA_DEP:} -platform = {env:_PYTEST_TOX_PLATFORM:.*} [testenv:linting] skip_install = True From 8e8a8fa4b9b1b88c8afc9ce92e3c2a8bc5466541 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 00:41:23 +0200 Subject: [PATCH 055/153] pytester: spawn: do not skip FreeBSD Fixes https://github.com/pytest-dev/pytest/issues/6069 --- changelog/6069.improvement.rst | 1 + src/_pytest/pytester.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 changelog/6069.improvement.rst diff --git a/changelog/6069.improvement.rst b/changelog/6069.improvement.rst new file mode 100644 index 000000000..e60d154bb --- /dev/null +++ b/changelog/6069.improvement.rst @@ -0,0 +1 @@ +``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 8a218d5e5..7eb912751 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1194,8 +1194,6 @@ class Testdir: pexpect = pytest.importorskip("pexpect", "3.0") if hasattr(sys, "pypy_version_info") and "64" in platform.machine(): pytest.skip("pypy-64 bit not supported") - if sys.platform.startswith("freebsd"): - pytest.xfail("pexpect does not work reliably on freebsd") if not hasattr(pexpect, "spawn"): pytest.skip("pexpect.spawn not available") logfile = self.tmpdir.join("spawn.out").open("wb") From 81c3bc76bcd366c61da2a63047421241e33c366b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 26 Oct 2019 22:54:11 +0200 Subject: [PATCH 056/153] tests: harden test_disabled_capture_fixture --- testing/test_capture.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index eb55093bb..dd0a3b949 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -605,11 +605,7 @@ class TestCaptureFixture: ) args = ("-s",) if no_capture else () result = testdir.runpytest_subprocess(*args) - result.stdout.fnmatch_lines( - """ - *while capture is disabled* - """ - ) + result.stdout.fnmatch_lines(["*while capture is disabled*", "*= 2 passed in *"]) result.stdout.no_fnmatch_line("*captured before*") result.stdout.no_fnmatch_line("*captured after*") if no_capture: From a4faac6c942f38613bca7b88176565da173d1ab2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 05:56:13 +0100 Subject: [PATCH 057/153] mark: move pytest_cmdline_main.tryfist into decorator Avoids comments for ignored typing. --- src/_pytest/mark/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_pytest/mark/__init__.py b/src/_pytest/mark/__init__.py index e76bb7857..e21e234e7 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -8,6 +8,7 @@ from .structures import MARK_GEN from .structures import MarkDecorator from .structures import MarkGenerator from .structures import ParameterSet +from _pytest.config import hookimpl from _pytest.config import UsageError __all__ = ["Mark", "MarkDecorator", "MarkGenerator", "get_empty_parameterset_mark"] @@ -74,6 +75,7 @@ def pytest_addoption(parser): parser.addini(EMPTY_PARAMETERSET_OPTION, "default marker for empty parametersets") +@hookimpl(tryfirst=True) def pytest_cmdline_main(config): import _pytest.config @@ -91,10 +93,6 @@ def pytest_cmdline_main(config): return 0 -# Ignore type because of https://github.com/python/mypy/issues/2087. -pytest_cmdline_main.tryfirst = True # type: ignore - - def deselect_by_keyword(items, config): keywordexpr = config.option.keyword.lstrip() if not keywordexpr: From 886a3ad609e16bb69a5d0b02f285e4fb076ebcc6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 22 Oct 2019 00:05:02 +0200 Subject: [PATCH 058/153] pytester: typing --- src/_pytest/pytester.py | 52 ++++++++++++++++++++++++---------------- testing/test_pytester.py | 21 ++++++++++++++++ 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 644058f8f..37acc4953 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -11,6 +11,7 @@ import traceback from collections.abc import Sequence from fnmatch import fnmatch from io import StringIO +from typing import Union from weakref import WeakKeyDictionary import py @@ -362,9 +363,9 @@ class RunResult: :ivar duration: duration in seconds """ - def __init__(self, ret, outlines, errlines, duration): + def __init__(self, ret: Union[int, ExitCode], outlines, errlines, duration) -> None: try: - self.ret = pytest.ExitCode(ret) + self.ret = pytest.ExitCode(ret) # type: Union[int, ExitCode] except ValueError: self.ret = ret self.outlines = outlines @@ -483,11 +484,7 @@ class Testdir: self._sys_modules_snapshot = self.__take_sys_modules_snapshot() self.chdir() self.request.addfinalizer(self.finalize) - method = self.request.config.getoption("--runpytest") - if method == "inprocess": - self._runpytest_method = self.runpytest_inprocess - elif method == "subprocess": - self._runpytest_method = self.runpytest_subprocess + self._method = self.request.config.getoption("--runpytest") mp = self.monkeypatch = MonkeyPatch() mp.setenv("PYTEST_DEBUG_TEMPROOT", str(self.test_tmproot)) @@ -835,7 +832,7 @@ class Testdir: reprec = rec.pop() else: - class reprec: + class reprec: # type: ignore pass reprec.ret = ret @@ -851,7 +848,7 @@ class Testdir: for finalizer in finalizers: finalizer() - def runpytest_inprocess(self, *args, **kwargs): + def runpytest_inprocess(self, *args, **kwargs) -> RunResult: """Return result of running pytest in-process, providing a similar interface to what self.runpytest() provides. """ @@ -866,15 +863,20 @@ class Testdir: try: reprec = self.inline_run(*args, **kwargs) except SystemExit as e: + ret = e.args[0] + try: + ret = ExitCode(e.args[0]) + except ValueError: + pass - class reprec: - ret = e.args[0] + class reprec: # type: ignore + ret = ret except Exception: traceback.print_exc() - class reprec: - ret = 3 + class reprec: # type: ignore + ret = ExitCode(3) finally: out, err = capture.readouterr() @@ -885,16 +887,20 @@ class Testdir: res = RunResult( reprec.ret, out.splitlines(), err.splitlines(), time.time() - now ) - res.reprec = reprec + res.reprec = reprec # type: ignore return res - def runpytest(self, *args, **kwargs): + def runpytest(self, *args, **kwargs) -> RunResult: """Run pytest inline or in a subprocess, depending on the command line option "--runpytest" and return a :py:class:`RunResult`. """ args = self._ensure_basetemp(args) - return self._runpytest_method(*args, **kwargs) + if self._method == "inprocess": + return self.runpytest_inprocess(*args, **kwargs) + elif self._method == "subprocess": + return self.runpytest_subprocess(*args, **kwargs) + raise RuntimeError("Unrecognized runpytest option: {}".format(self._method)) def _ensure_basetemp(self, args): args = list(args) @@ -1051,7 +1057,7 @@ class Testdir: return popen - def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN): + def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN) -> RunResult: """Run a command with arguments. Run a process using subprocess.Popen saving the stdout and stderr. @@ -1069,9 +1075,9 @@ class Testdir: """ __tracebackhide__ = True - cmdargs = [ + cmdargs = tuple( str(arg) if isinstance(arg, py.path.local) else arg for arg in cmdargs - ] + ) p1 = self.tmpdir.join("stdout") p2 = self.tmpdir.join("stderr") print("running:", *cmdargs) @@ -1122,6 +1128,10 @@ class Testdir: f2.close() self._dump_lines(out, sys.stdout) self._dump_lines(err, sys.stderr) + try: + ret = ExitCode(ret) + except ValueError: + pass return RunResult(ret, out, err, time.time() - now) def _dump_lines(self, lines, fp): @@ -1134,7 +1144,7 @@ class Testdir: def _getpytestargs(self): return sys.executable, "-mpytest" - def runpython(self, script): + def runpython(self, script) -> RunResult: """Run a python script using sys.executable as interpreter. Returns a :py:class:`RunResult`. @@ -1146,7 +1156,7 @@ class Testdir: """Run python -c "command", return a :py:class:`RunResult`.""" return self.run(sys.executable, "-c", command) - def runpytest_subprocess(self, *args, timeout=None): + def runpytest_subprocess(self, *args, timeout=None) -> RunResult: """Run pytest as a subprocess with given arguments. Any plugins added to the :py:attr:`plugins` list will be added using the diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 7bf36831c..758e999dc 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -395,6 +395,27 @@ def test_testdir_subprocess(testdir): assert testdir.runpytest_subprocess(testfile).ret == 0 +def test_testdir_subprocess_via_runpytest_arg(testdir) -> None: + testfile = testdir.makepyfile( + """ + def test_testdir_subprocess(testdir): + import os + testfile = testdir.makepyfile( + \""" + import os + def test_one(): + assert {} != os.getpid() + \""".format(os.getpid()) + ) + assert testdir.runpytest(testfile).ret == 0 + """ + ) + result = testdir.runpytest_subprocess( + "-p", "pytester", "--runpytest", "subprocess", testfile + ) + assert result.ret == 0 + + def test_unicode_args(testdir): result = testdir.runpytest("-k", "💩") assert result.ret == ExitCode.NO_TESTS_COLLECTED From d863c30c743138ed29cdf786cb18c41c24983fe1 Mon Sep 17 00:00:00 2001 From: MarcoGorelli Date: Sun, 27 Oct 2019 15:02:37 +0000 Subject: [PATCH 059/153] Fix plurality mismatch for and in pytest terminal summary --- AUTHORS | 1 + changelog/5990.improvement.rst | 1 + src/_pytest/main.py | 5 ++++- src/_pytest/terminal.py | 17 +++++++++++++++-- testing/acceptance_test.py | 4 ++-- testing/python/collect.py | 2 +- testing/python/fixtures.py | 2 +- testing/test_assertrewrite.py | 2 +- testing/test_capture.py | 2 +- testing/test_collection.py | 6 +++--- testing/test_doctest.py | 2 +- testing/test_mark.py | 2 +- testing/test_runner_xunit.py | 2 +- testing/test_skipping.py | 2 +- testing/test_stepwise.py | 2 +- testing/test_terminal.py | 11 ++++------- testing/test_warnings.py | 12 ++++++------ 17 files changed, 45 insertions(+), 30 deletions(-) create mode 100644 changelog/5990.improvement.rst diff --git a/AUTHORS b/AUTHORS index e11400c1f..def641c95 100644 --- a/AUTHORS +++ b/AUTHORS @@ -160,6 +160,7 @@ Manuel Krebber Marc Schlaich Marcelo Duarte Trevisani Marcin Bachry +Marco Gorelli Mark Abramowitz Markus Unterwaditzer Martijn Faassen diff --git a/changelog/5990.improvement.rst b/changelog/5990.improvement.rst new file mode 100644 index 000000000..6f5ad648e --- /dev/null +++ b/changelog/5990.improvement.rst @@ -0,0 +1 @@ +Fix plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). diff --git a/src/_pytest/main.py b/src/_pytest/main.py index ad65ed299..7b3855e6c 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -248,7 +248,10 @@ def pytest_collection(session): def pytest_runtestloop(session): if session.testsfailed and not session.config.option.continue_on_collection_errors: - raise session.Interrupted("%d errors during collection" % session.testsfailed) + raise session.Interrupted( + "%d error%s during collection" + % (session.testsfailed, "s" if session.testsfailed != 1 else "") + ) if session.config.option.collectonly: return True diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 35f6d324b..228fc4219 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -539,7 +539,7 @@ class TerminalReporter: str(self._numcollected) + " item" + ("" if self._numcollected == 1 else "s") ) if errors: - line += " / %d errors" % errors + line += " / %d error%s" % (errors, "s" if errors != 1 else "") if deselected: line += " / %d deselected" % deselected if skipped: @@ -1056,6 +1056,19 @@ _color_for_type = { _color_for_type_default = "yellow" +def _make_plural(count, noun): + # No need to pluralize words such as `failed` or `passed`. + if noun not in ["error", "warnings"]: + return count, noun + + # The `warnings` key is plural. To avoid API breakage, we keep it that way but + # set it to singular here so we can determine plurality in the same way as we do + # for `error`. + noun = noun.replace("warnings", "warning") + + return count, noun + "s" if count != 1 else noun + + def build_summary_stats_line(stats): known_types = ( "failed passed skipped deselected xfailed xpassed warnings error".split() @@ -1086,7 +1099,7 @@ def build_summary_stats_line(stats): ) color = _color_for_type.get(key, _color_for_type_default) markup = {color: True, "bold": color == main_color} - parts.append(("%d %s" % (count, key), markup)) + parts.append(("%d %s" % _make_plural(count, key), markup)) if not parts: parts = [("no tests ran", {_color_for_type_default: True})] diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 2bf56cb80..82c727fc6 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -628,7 +628,7 @@ class TestInvocationVariants: result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True) assert result.ret != 0 - result.stdout.fnmatch_lines(["collected*0*items*/*1*errors"]) + result.stdout.fnmatch_lines(["collected*0*items*/*1*error"]) def test_pyargs_only_imported_once(self, testdir): pkg = testdir.mkpydir("foo") @@ -956,7 +956,7 @@ class TestDurations: testdir.makepyfile(test_collecterror="""xyz""") result = testdir.runpytest("--durations=2", "-k test_1") assert result.ret == 2 - result.stdout.fnmatch_lines(["*Interrupted: 1 errors during collection*"]) + result.stdout.fnmatch_lines(["*Interrupted: 1 error during collection*"]) # Collection errors abort test execution, therefore no duration is # output result.stdout.no_fnmatch_line("*duration*") diff --git a/testing/python/collect.py b/testing/python/collect.py index 537047119..30f9841b5 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1167,7 +1167,7 @@ def test_dont_collect_non_function_callable(testdir): [ "*collected 1 item*", "*test_dont_collect_non_function_callable.py:2: *cannot collect 'test_a' because it is not a function*", - "*1 passed, 1 warnings in *", + "*1 passed, 1 warning in *", ] ) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 6399863c7..6dca793e0 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3081,7 +3081,7 @@ class TestErrors: *KeyError* *ERROR*teardown*test_2* *KeyError* - *3 pass*2 error* + *3 pass*2 errors* """ ) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 6c5de5c03..3aab3ac2a 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -122,7 +122,7 @@ class TestAssertionRewrite: } testdir.makepyfile(**contents) result = testdir.runpytest_subprocess() - assert "warnings" not in "".join(result.outlines) + assert "warning" not in "".join(result.outlines) def test_rewrites_plugin_as_a_package(self, testdir): pkgdir = testdir.mkpydir("plugin") diff --git a/testing/test_capture.py b/testing/test_capture.py index eb55093bb..a51dfea30 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -453,7 +453,7 @@ class TestCaptureFixture: "E*capfd*capsys*same*time*", "*ERROR*setup*test_two*", "E*capsys*capfd*same*time*", - "*2 error*", + "*2 errors*", ] ) diff --git a/testing/test_collection.py b/testing/test_collection.py index 7a5cf795b..259868357 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -892,7 +892,7 @@ def test_continue_on_collection_errors(testdir): assert res.ret == 1 res.stdout.fnmatch_lines( - ["collected 2 items / 2 errors", "*1 failed, 1 passed, 2 error*"] + ["collected 2 items / 2 errors", "*1 failed, 1 passed, 2 errors*"] ) @@ -909,7 +909,7 @@ def test_continue_on_collection_errors_maxfail(testdir): res = testdir.runpytest("--continue-on-collection-errors", "--maxfail=3") assert res.ret == 1 - res.stdout.fnmatch_lines(["collected 2 items / 2 errors", "*1 failed, 2 error*"]) + res.stdout.fnmatch_lines(["collected 2 items / 2 errors", "*1 failed, 2 errors*"]) def test_fixture_scope_sibling_conftests(testdir): @@ -1253,7 +1253,7 @@ def test_collector_respects_tbstyle(testdir): ' File "*/test_collector_respects_tbstyle.py", line 1, in ', " assert 0", "AssertionError: assert 0", - "*! Interrupted: 1 errors during collection !*", + "*! Interrupted: 1 error during collection !*", "*= 1 error in *", ] ) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 37b3988f7..79095e3e7 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -334,7 +334,7 @@ class TestDoctests: [ "*ERROR collecting hello.py*", "*{e}: No module named *asdals*".format(e=MODULE_NOT_FOUND_ERROR), - "*Interrupted: 1 errors during collection*", + "*Interrupted: 1 error during collection*", ] ) diff --git a/testing/test_mark.py b/testing/test_mark.py index c8d5851ac..93bc77a16 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -891,7 +891,7 @@ def test_parameterset_for_fail_at_collect(testdir): result = testdir.runpytest(str(p1)) result.stdout.fnmatch_lines( [ - "collected 0 items / 1 errors", + "collected 0 items / 1 error", "* ERROR collecting test_parameterset_for_fail_at_collect.py *", "Empty parameter set in 'test' at line 3", "*= 1 error in *", diff --git a/testing/test_runner_xunit.py b/testing/test_runner_xunit.py index 1e63bbf49..0ff508d2c 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -234,7 +234,7 @@ def test_setup_funcarg_setup_when_outer_scope_fails(testdir): "*ValueError*42*", "*function2*", "*ValueError*42*", - "*2 error*", + "*2 errors*", ] ) result.stdout.no_fnmatch_line("*xyz43*") diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 51b1bbdd6..8ba77ba12 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -886,7 +886,7 @@ def test_errors_in_xfail_skip_expressions(testdir): " syntax error", markline, "SyntaxError: invalid syntax", - "*1 pass*2 error*", + "*1 pass*2 errors*", ] ) diff --git a/testing/test_stepwise.py b/testing/test_stepwise.py index f61425b6b..3e4f86f21 100644 --- a/testing/test_stepwise.py +++ b/testing/test_stepwise.py @@ -164,7 +164,7 @@ def test_stop_on_collection_errors(broken_testdir, broken_first): if broken_first: files.reverse() result = broken_testdir.runpytest("-v", "--strict-markers", "--stepwise", *files) - result.stdout.fnmatch_lines("*errors during collection*") + result.stdout.fnmatch_lines("*error during collection*") def test_xfail_handling(testdir): diff --git a/testing/test_terminal.py b/testing/test_terminal.py index c53b9f2ec..ba1844fed 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1237,7 +1237,7 @@ def test_terminal_summary_warnings_header_once(testdir): "*= warnings summary =*", "*warning_from_test*", "*= short test summary info =*", - "*== 1 failed, 1 warnings in *", + "*== 1 failed, 1 warning in *", ] ) result.stdout.no_fnmatch_line("*None*") @@ -1263,6 +1263,7 @@ def test_terminal_summary_warnings_header_once(testdir): {"failed": (1,), "passed": (1,)}, ), ("red", [("1 error", {"bold": True, "red": True})], {"error": (1,)}), + ("red", [("2 errors", {"bold": True, "red": True})], {"error": (1, 2)}), ( "red", [ @@ -1281,16 +1282,12 @@ def test_terminal_summary_warnings_header_once(testdir): ], {"weird": (1,), "passed": (1,)}, ), - ( - "yellow", - [("1 warnings", {"bold": True, "yellow": True})], - {"warnings": (1,)}, - ), + ("yellow", [("1 warning", {"bold": True, "yellow": True})], {"warnings": (1,)}), ( "yellow", [ ("1 passed", {"bold": False, "green": True}), - ("1 warnings", {"bold": True, "yellow": True}), + ("1 warning", {"bold": True, "yellow": True}), ], {"warnings": (1,), "passed": (1,)}, ), diff --git a/testing/test_warnings.py b/testing/test_warnings.py index 077636c52..bbcf87e5a 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -142,7 +142,7 @@ def test_unicode(testdir, pyfile_with_warnings): [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, "*test_unicode.py:7: UserWarning: \u6d4b\u8bd5*", - "* 1 passed, 1 warnings*", + "* 1 passed, 1 warning*", ] ) @@ -201,7 +201,7 @@ def test_filterwarnings_mark(testdir, default_config): """ ) result = testdir.runpytest("-W always" if default_config == "cmdline" else "") - result.stdout.fnmatch_lines(["*= 1 failed, 2 passed, 1 warnings in *"]) + result.stdout.fnmatch_lines(["*= 1 failed, 2 passed, 1 warning in *"]) def test_non_string_warning_argument(testdir): @@ -216,7 +216,7 @@ def test_non_string_warning_argument(testdir): """ ) result = testdir.runpytest("-W", "always") - result.stdout.fnmatch_lines(["*= 1 passed, 1 warnings in *"]) + result.stdout.fnmatch_lines(["*= 1 passed, 1 warning in *"]) def test_filterwarnings_mark_registration(testdir): @@ -302,7 +302,7 @@ def test_collection_warnings(testdir): "*== %s ==*" % WARNINGS_SUMMARY_HEADER, " *collection_warnings.py:3: UserWarning: collection warning", ' warnings.warn(UserWarning("collection warning"))', - "* 1 passed, 1 warnings*", + "* 1 passed, 1 warning*", ] ) @@ -358,7 +358,7 @@ def test_hide_pytest_internal_warnings(testdir, ignore_pytest_warnings): [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, "*test_hide_pytest_internal_warnings.py:4: PytestWarning: some internal warning", - "* 1 passed, 1 warnings *", + "* 1 passed, 1 warning *", ] ) @@ -476,7 +476,7 @@ class TestDeprecationWarningsByDefault: [ "*== %s ==*" % WARNINGS_SUMMARY_HEADER, "*test_hidden_by_mark.py:3: DeprecationWarning: collection", - "* 1 passed, 1 warnings*", + "* 1 passed, 1 warning*", ] ) From 820b747e7a14e441bb0e499eff51de65973252d0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 19:30:41 +0100 Subject: [PATCH 060/153] tests: merge/remove test_dontreadfrominput_buffer_python3 --- testing/test_capture.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index eb55093bb..91518c37b 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -826,6 +826,7 @@ def test_dontreadfrominput(): from _pytest.capture import DontReadFromInput f = DontReadFromInput() + assert f.buffer is f assert not f.isatty() pytest.raises(IOError, f.read) pytest.raises(IOError, f.readlines) @@ -835,20 +836,6 @@ def test_dontreadfrominput(): f.close() # just for completeness -def test_dontreadfrominput_buffer_python3(): - from _pytest.capture import DontReadFromInput - - f = DontReadFromInput() - fb = f.buffer - assert not fb.isatty() - pytest.raises(IOError, fb.read) - pytest.raises(IOError, fb.readlines) - iter_f = iter(f) - pytest.raises(IOError, next, iter_f) - pytest.raises(ValueError, fb.fileno) - f.close() # just for completeness - - @pytest.fixture def tmpfile(testdir): f = testdir.makepyfile("").open("wb+") From 32412532ef58fbaf7a43a766e20fbf94da21feee Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 02:34:24 +0200 Subject: [PATCH 061/153] tests: mock doctest.DocTestRunner to not use real pdb It is not used there anyway, and might cause false positives. --- testing/python/approx.py | 37 ++++++++++++++++++++++++++----------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index 60fde151a..11502c509 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -11,16 +11,32 @@ from pytest import approx inf, nan = float("inf"), float("nan") -class MyDocTestRunner(doctest.DocTestRunner): - def __init__(self): - doctest.DocTestRunner.__init__(self) +@pytest.fixture +def mocked_doctest_runner(monkeypatch): + class MockedPdb: + def __init__(self, out): + pass - def report_failure(self, out, test, example, got): - raise AssertionError( - "'{}' evaluates to '{}', not '{}'".format( - example.source.strip(), got.strip(), example.want.strip() + def set_trace(self): + pass + + def reset(self): + pass + + def set_continue(self): + pass + + monkeypatch.setattr("doctest._OutputRedirectingPdb", MockedPdb) + + class MyDocTestRunner(doctest.DocTestRunner): + def report_failure(self, out, test, example, got): + raise AssertionError( + "'{}' evaluates to '{}', not '{}'".format( + example.source.strip(), got.strip(), example.want.strip() + ) ) - ) + + return MyDocTestRunner() class TestApprox: @@ -411,13 +427,12 @@ class TestApprox: assert a12 != approx(a21) assert a21 != approx(a12) - def test_doctests(self): + def test_doctests(self, mocked_doctest_runner): parser = doctest.DocTestParser() test = parser.get_doctest( approx.__doc__, {"approx": approx}, approx.__name__, None, None ) - runner = MyDocTestRunner() - runner.run(test) + mocked_doctest_runner.run(test) def test_unicode_plus_minus(self, testdir): """ From a5bd19e3b406f45ffaadf7e1d257b90f11e5931f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 02:37:13 +0200 Subject: [PATCH 062/153] tests: lazily import doctest in approx tests --- testing/python/approx.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index 11502c509..f72045624 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -1,4 +1,3 @@ -import doctest import operator from decimal import Decimal from fractions import Fraction @@ -13,12 +12,14 @@ inf, nan = float("inf"), float("nan") @pytest.fixture def mocked_doctest_runner(monkeypatch): + import doctest + class MockedPdb: def __init__(self, out): pass def set_trace(self): - pass + raise NotImplementedError("not used") def reset(self): pass @@ -428,6 +429,8 @@ class TestApprox: assert a21 != approx(a12) def test_doctests(self, mocked_doctest_runner): + import doctest + parser = doctest.DocTestParser() test = parser.get_doctest( approx.__doc__, {"approx": approx}, approx.__name__, None, None From 023dde89e1ec561f31e8c37162984803b589e4e6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 02:54:33 +0100 Subject: [PATCH 063/153] ci: Travis: include pexpect in main py37 job This removes xdist there (not compatible with the pexpect tests), but it is better to have one job less, although slower due to not using xdist. --- .travis.yml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0a7e425b7..bad99f380 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,8 +39,13 @@ jobs: # Full run of latest supported version, without xdist. # Coverage for: + # - pytester's LsofFdLeakChecker + # - TestArgComplete (linux only) + # - numpy + # - old attrs + # - verbose=0 # - test_sys_breakpoint_interception (via pexpect). - - env: TOXENV=py37-pexpect PYTEST_COVERAGE=1 + - env: TOXENV=py37-lsof-numpy-oldattrs-pexpect-twisted PYTEST_COVERAGE=1 PYTEST_ADDOPTS= python: '3.7' # Coverage tracking is slow with pypy, skip it. @@ -50,14 +55,6 @@ jobs: - env: TOXENV=py35-xdist python: '3.5' - # Coverage for: - # - pytester's LsofFdLeakChecker - # - TestArgComplete (linux only) - # - numpy - # - old attrs - # - verbose=0 - - env: TOXENV=py37-lsof-oldattrs-numpy-twisted-xdist PYTEST_COVERAGE=1 PYTEST_ADDOPTS= - # Specialized factors for py37. - env: TOXENV=py37-pluggymaster-xdist - env: TOXENV=py37-freeze From d6e324a5e640382bcfcf6b2abeb024577edc01c3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 02:46:40 +0100 Subject: [PATCH 064/153] tests: conftest: handle tests using runpytest_subprocess as "slowest" --- testing/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testing/conftest.py b/testing/conftest.py index a03efb0cf..8b0430f69 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -39,9 +39,12 @@ def pytest_collection_modifyitems(config, items): neutral_items.append(item) else: if "testdir" in fixtures: - if spawn_names.intersection(item.function.__code__.co_names): + co_names = item.function.__code__.co_names + if spawn_names.intersection(co_names): item.add_marker(pytest.mark.uses_pexpect) slowest_items.append(item) + elif "runpytest_subprocess" in co_names: + slowest_items.append(item) else: slow_items.append(item) item.add_marker(pytest.mark.slow) From a4554e666af4d62500bec2bae393fc9e67e2d4d8 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 02:43:49 +0100 Subject: [PATCH 065/153] tests: speed up test_faulthandler.test_timeout --- testing/test_faulthandler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_faulthandler.py b/testing/test_faulthandler.py index a0cf1d8c1..e99206a4d 100644 --- a/testing/test_faulthandler.py +++ b/testing/test_faulthandler.py @@ -58,13 +58,13 @@ def test_timeout(testdir, enabled): """ import time def test_timeout(): - time.sleep(2.0) + time.sleep(0.1) """ ) testdir.makeini( """ [pytest] - faulthandler_timeout = 1 + faulthandler_timeout = 0.01 """ ) args = ["-p", "no:faulthandler"] if not enabled else [] From 60ceec6eb180ba1ac76741edf82804ed9ae8baf2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 28 Oct 2019 14:34:43 +0100 Subject: [PATCH 066/153] tests: fix testing/test_capture.py::test_typeerror_encodedfile_write MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Failed for me due to different indent (?) - not reproducible: > ??? E Failed: nomatch: 'E TypeError: write() argument must be str, not bytes' … E and: '> def mode(self):' E and: 'E TypeError: write() argument must be str, not bytes' … E remains unmatched: 'E TypeError: write() argument must be str, not bytes' --- testing/test_capture.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index eb55093bb..4320a7cae 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1503,11 +1503,9 @@ def test_typeerror_encodedfile_write(testdir): """ ) result_without_capture = testdir.runpytest("-s", str(p)) - result_with_capture = testdir.runpytest(str(p)) assert result_with_capture.ret == result_without_capture.ret - result_with_capture.stdout.fnmatch_lines( - ["E TypeError: write() argument must be str, not bytes"] + ["E * TypeError: write() argument must be str, not bytes"] ) From 6d2cabae5752d7bf8e7f5b1b1c43ee41ec1ac067 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 29 Oct 2019 12:18:07 +0100 Subject: [PATCH 067/153] terminal: fix line offset with skip reports The original fix in https://github.com/pytest-dev/pytest/pull/2548 was wrong, and was likely meant to fix the use with decorators instead, which this does now (while reverting 869eed9898). --- changelog/2548.bugfix.rst | 1 + src/_pytest/skipping.py | 4 ++-- src/_pytest/terminal.py | 2 +- testing/test_skipping.py | 20 +++++++++++++++++--- 4 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 changelog/2548.bugfix.rst diff --git a/changelog/2548.bugfix.rst b/changelog/2548.bugfix.rst new file mode 100644 index 000000000..8ee3b6462 --- /dev/null +++ b/changelog/2548.bugfix.rst @@ -0,0 +1 @@ +Fix line offset mismatch with skipped tests in terminal summary. diff --git a/src/_pytest/skipping.py b/src/_pytest/skipping.py index 53737816f..9eaa77d17 100644 --- a/src/_pytest/skipping.py +++ b/src/_pytest/skipping.py @@ -161,9 +161,9 @@ def pytest_runtest_makereport(item, call): # skipped by mark.skipif; change the location of the failure # to point to the item definition, otherwise it will display # the location of where the skip exception was raised within pytest - filename, line, reason = rep.longrepr + _, _, reason = rep.longrepr filename, line = item.location[:2] - rep.longrepr = filename, line, reason + rep.longrepr = filename, line + 1, reason # called by terminalreporter progress reporting diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 228fc4219..216ad0417 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -954,7 +954,7 @@ class TerminalReporter: if lineno is not None: lines.append( "%s [%d] %s:%d: %s" - % (verbose_word, num, fspath, lineno + 1, reason) + % (verbose_word, num, fspath, lineno, reason) ) else: lines.append("%s [%d] %s: %s" % (verbose_word, num, fspath, reason)) diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 8ba77ba12..86f328a93 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -731,23 +731,37 @@ def test_skipif_class(testdir): def test_skipped_reasons_functional(testdir): testdir.makepyfile( test_one=""" + import pytest from conftest import doskip + def setup_function(func): doskip() + def test_func(): pass + class TestClass(object): def test_method(self): doskip() - """, + + @pytest.mark.skip("via_decorator") + def test_deco(self): + assert 0 + """, conftest=""" - import pytest + import pytest, sys def doskip(): + assert sys._getframe().f_lineno == 3 pytest.skip('test') """, ) result = testdir.runpytest("-rs") - result.stdout.fnmatch_lines(["*SKIP*2*conftest.py:4: test"]) + result.stdout.fnmatch_lines_random( + [ + "SKIPPED [[]2[]] */conftest.py:4: test", + "SKIPPED [[]1[]] test_one.py:14: via_decorator", + ] + ) assert result.ret == 0 From 8c21416798af488da173b2b1c98b5435c0865660 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 29 Oct 2019 15:18:29 +0100 Subject: [PATCH 068/153] lsof_check: include exc with skip message --- testing/test_capture.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 67aa0c77e..a79b4077b 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -905,9 +905,9 @@ def lsof_check(): pid = os.getpid() try: out = subprocess.check_output(("lsof", "-p", str(pid))).decode() - except (OSError, subprocess.CalledProcessError, UnicodeDecodeError): + except (OSError, subprocess.CalledProcessError, UnicodeDecodeError) as exc: # about UnicodeDecodeError, see note on pytester - pytest.skip("could not run 'lsof'") + pytest.skip("could not run 'lsof' ({!r})".format(exc)) yield out2 = subprocess.check_output(("lsof", "-p", str(pid))).decode() len1 = len([x for x in out.split("\n") if "REG" in x]) From b99661b9d77fbbc9ecb70d48a61a2a88eb0cd055 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 16 Oct 2019 19:24:41 -0300 Subject: [PATCH 069/153] Introduce --report-log option Fix #4488 --- changelog/4488.feature.rst | 9 +++++ doc/en/contents.rst | 1 + doc/en/deprecations.rst | 13 +++--- doc/en/report_log.rst | 70 +++++++++++++++++++++++++++++++++ doc/en/usage.rst | 16 +++++--- src/_pytest/config/__init__.py | 1 + src/_pytest/report_log.py | 72 ++++++++++++++++++++++++++++++++++ src/_pytest/reports.py | 10 ++--- testing/test_report_log.py | 54 +++++++++++++++++++++++++ testing/test_reports.py | 6 +-- 10 files changed, 231 insertions(+), 21 deletions(-) create mode 100644 changelog/4488.feature.rst create mode 100644 doc/en/report_log.rst create mode 100644 src/_pytest/report_log.py create mode 100644 testing/test_report_log.py diff --git a/changelog/4488.feature.rst b/changelog/4488.feature.rst new file mode 100644 index 000000000..ddbca65d6 --- /dev/null +++ b/changelog/4488.feature.rst @@ -0,0 +1,9 @@ +New ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. + +Each line of the report log contains a self contained JSON object corresponding to a testing event, +such as a collection or a test result report. The file is guaranteed to be flushed after writing +each line, so systems can read and process events in real-time. + +This option is meant to replace ``--resultlog``, which is deprecated and meant to be removed +in a future release. If you use ``--resultlog``, please try out ``--report-log`` and +provide feedback. diff --git a/doc/en/contents.rst b/doc/en/contents.rst index c623d0602..5d7599f50 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -27,6 +27,7 @@ Full pytest documentation unittest nose xunit_setup + report_log plugins writing_plugins logging diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 9d01e5f23..5cf3b0903 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -40,15 +40,14 @@ Result log (``--result-log``) .. deprecated:: 4.0 The ``--result-log`` option produces a stream of test reports which can be -analysed at runtime. It uses a custom format which requires users to implement their own -parser, but the team believes using a line-based format that can be parsed using standard -tools would provide a suitable and better alternative. +analysed at runtime, but it uses a custom format which requires users to implement their own +parser. -The current plan is to provide an alternative in the pytest 5.0 series and remove the ``--result-log`` -option in pytest 6.0 after the new implementation proves satisfactory to all users and is deemed -stable. +The :ref:`--report-log ` option provides a more standard and extensible alternative, producing +one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback. -The actual alternative is still being discussed in issue `#4488 `__. +The plan is remove the ``--result-log`` option in pytest 6.0 after ``--result-log`` proves satisfactory +to all users and is deemed stable. Removed Features diff --git a/doc/en/report_log.rst b/doc/en/report_log.rst new file mode 100644 index 000000000..619925180 --- /dev/null +++ b/doc/en/report_log.rst @@ -0,0 +1,70 @@ +.. _report_log: + +Report files +============ + +.. versionadded:: 5.3 + +The ``--report-log=FILE`` option writes *report logs* into a file as the test session executes. + +Each line of the report log contains a self contained JSON object corresponding to a testing event, +such as a collection or a test result report. The file is guaranteed to be flushed after writing +each line, so systems can read and process events in real-time. + +Each JSON object contains a special key ``$report_type``, which contains a unique identifier for +that kind of report object. For future compatibility, consumers of the file should ignore reports +they don't recognize, as well as ignore unknown properties/keys in JSON objects that they do know, +as future pytest versions might enrich the objects with more properties/keys. + +.. note:: + This option is meant to the replace ``--resultlog``, which is deprecated and meant to be removed + in a future release. If you use ``--resultlog``, please try out ``--report-log`` and + provide feedback. + +Example +------- + +Consider this file: + +.. code-block:: python + + # content of test_report_example.py + + + def test_ok(): + assert 5 + 5 == 10 + + + def test_fail(): + assert 4 + 4 == 1 + + +.. code-block:: pytest + + $ pytest test_report_example.py -q --report-log=log.json + .F [100%] + ================================= FAILURES ================================= + ________________________________ test_fail _________________________________ + + def test_fail(): + > assert 4 + 4 == 1 + E assert (4 + 4) == 1 + + test_report_example.py:8: AssertionError + ------------------- generated report log file: log.json -------------------- + 1 failed, 1 passed in 0.12s + +The generated ``log.json`` will contain a JSON object per line: + +:: + + $ cat log.json + {"pytest_version": "5.2.3.dev90+gd1129cf96.d20191026", "$report_type": "Header"} + {"nodeid": "", "outcome": "passed", "longrepr": null, "result": null, "sections": [], "$report_type": "CollectReport"} + {"nodeid": "test_report_example.py", "outcome": "passed", "longrepr": null, "result": null, "sections": [], "$report_type": "CollectReport"} + {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "setup", "user_properties": [], "sections": [], "duration": 0.00021314620971679688, "$report_type": "TestReport"} + {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "call", "user_properties": [], "sections": [], "duration": 0.00014543533325195312, "$report_type": "TestReport"} + {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "teardown", "user_properties": [], "sections": [], "duration": 0.00016427040100097656, "$report_type": "TestReport"} + {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "passed", "longrepr": null, "when": "setup", "user_properties": [], "sections": [], "duration": 0.00013589859008789062, "$report_type": "TestReport"} + {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "failed", "longrepr": {"reprcrash": {"path": "$REGENDOC_TMPDIR/test_report_example.py", "lineno": 8, "message": "assert (4 + 4) == 1"}, "reprtraceback": {"reprentries": [{"type": "ReprEntry", "data": {"lines": [" def test_fail():", "> assert 4 + 4 == 1", "E assert (4 + 4) == 1"], "reprfuncargs": {"args": []}, "reprlocals": null, "reprfileloc": {"path": "test_report_example.py", "lineno": 8, "message": "AssertionError"}, "style": "long"}}], "extraline": null, "style": "long"}, "sections": [], "chain": [[{"reprentries": [{"type": "ReprEntry", "data": {"lines": [" def test_fail():", "> assert 4 + 4 == 1", "E assert (4 + 4) == 1"], "reprfuncargs": {"args": []}, "reprlocals": null, "reprfileloc": {"path": "test_report_example.py", "lineno": 8, "message": "AssertionError"}, "style": "long"}}], "extraline": null, "style": "long"}, {"path": "$REGENDOC_TMPDIR/test_report_example.py", "lineno": 8, "message": "assert (4 + 4) == 1"}, null]]}, "when": "call", "user_properties": [], "sections": [], "duration": 0.00027489662170410156, "$report_type": "TestReport"} + {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "passed", "longrepr": null, "when": "teardown", "user_properties": [], "sections": [], "duration": 0.00016689300537109375, "$report_type": "TestReport"} diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 167c7fa9b..a23cf764a 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -679,12 +679,6 @@ Creating resultlog format files ---------------------------------------------------- - - This option is rarely used and is scheduled for removal in 5.0. - - See `the deprecation docs `__ - for more information. - To create plain-text machine-readable result files you can issue: .. code-block:: bash @@ -694,6 +688,16 @@ To create plain-text machine-readable result files you can issue: and look at the content at the ``path`` location. Such files are used e.g. by the `PyPy-test`_ web page to show test results over several revisions. +.. warning:: + + This option is rarely used and is scheduled for removal in pytest 6.0. + + If you use this option, consider using the new :ref:`--result-log `. + + See `the deprecation docs `__ + for more information. + + .. _`PyPy-test`: http://buildbot.pypy.org/summary diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 4746fd6c7..2b0f48c07 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -154,6 +154,7 @@ default_plugins = essential_plugins + ( "assertion", "junitxml", "resultlog", + "report_log", "doctest", "cacheprovider", "freeze_support", diff --git a/src/_pytest/report_log.py b/src/_pytest/report_log.py new file mode 100644 index 000000000..b12d0a55d --- /dev/null +++ b/src/_pytest/report_log.py @@ -0,0 +1,72 @@ +import json +from pathlib import Path + +import pytest + + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting", "report-log plugin options") + group.addoption( + "--report-log", + action="store", + metavar="path", + default=None, + help="Path to line-based json objects of test session events.", + ) + + +def pytest_configure(config): + report_log = config.option.report_log + if report_log and not hasattr(config, "slaveinput"): + config._report_log_plugin = ReportLogPlugin(config, Path(report_log)) + config.pluginmanager.register(config._report_log_plugin) + + +def pytest_unconfigure(config): + report_log_plugin = getattr(config, "_report_log_plugin", None) + if report_log_plugin: + report_log_plugin.close() + del config._report_log_plugin + + +class ReportLogPlugin: + def __init__(self, config, log_path: Path): + self._config = config + self._log_path = log_path + + log_path.parent.mkdir(parents=True, exist_ok=True) + self._file = log_path.open("w", buffering=1, encoding="UTF-8") + + def close(self): + if self._file is not None: + self._file.close() + self._file = None + + def _write_json_data(self, data): + self._file.write(json.dumps(data) + "\n") + self._file.flush() + + def pytest_sessionstart(self): + data = {"pytest_version": pytest.__version__, "$report_type": "SessionStart"} + self._write_json_data(data) + + def pytest_sessionfinish(self, exitstatus): + data = {"exitstatus": exitstatus, "$report_type": "SessionFinish"} + self._write_json_data(data) + + def pytest_runtest_logreport(self, report): + data = self._config.hook.pytest_report_to_serializable( + config=self._config, report=report + ) + self._write_json_data(data) + + def pytest_collectreport(self, report): + data = self._config.hook.pytest_report_to_serializable( + config=self._config, report=report + ) + self._write_json_data(data) + + def pytest_terminal_summary(self, terminalreporter): + terminalreporter.write_sep( + "-", "generated report log file: {}".format(self._log_path) + ) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 49eec6129..b1592f817 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -329,18 +329,18 @@ class CollectErrorRepr(TerminalRepr): def pytest_report_to_serializable(report): if isinstance(report, (TestReport, CollectReport)): data = report._to_json() - data["_report_type"] = report.__class__.__name__ + data["$report_type"] = report.__class__.__name__ return data def pytest_report_from_serializable(data): - if "_report_type" in data: - if data["_report_type"] == "TestReport": + if "$report_type" in data: + if data["$report_type"] == "TestReport": return TestReport._from_json(data) - elif data["_report_type"] == "CollectReport": + elif data["$report_type"] == "CollectReport": return CollectReport._from_json(data) assert False, "Unknown report_type unserialize data: {}".format( - data["_report_type"] + data["$report_type"] ) diff --git a/testing/test_report_log.py b/testing/test_report_log.py new file mode 100644 index 000000000..cc2a431ec --- /dev/null +++ b/testing/test_report_log.py @@ -0,0 +1,54 @@ +import json + +import pytest +from _pytest.reports import BaseReport + + +def test_basics(testdir, tmp_path, pytestconfig): + """Basic testing of the report log functionality. + + We don't test the test reports extensively because they have been + tested already in ``test_reports``. + """ + testdir.makepyfile( + """ + def test_ok(): + pass + + def test_fail(): + assert 0 + """ + ) + + log_file = tmp_path / "log.json" + + result = testdir.runpytest("--report-log", str(log_file)) + assert result.ret == pytest.ExitCode.TESTS_FAILED + result.stdout.fnmatch_lines(["* generated report log file: {}*".format(log_file)]) + + json_objs = [json.loads(x) for x in log_file.read_text().splitlines()] + assert len(json_objs) == 10 + + # first line should be the session_start + session_start = json_objs[0] + assert session_start == { + "pytest_version": pytest.__version__, + "$report_type": "SessionStart", + } + + # last line should be the session_finish + session_start = json_objs[-1] + assert session_start == { + "exitstatus": pytest.ExitCode.TESTS_FAILED, + "$report_type": "SessionFinish", + } + + # rest of the json objects should be unserialized into report objects; we don't test + # the actual report object extensively because it has been tested in ``test_reports`` + # already. + pm = pytestconfig.pluginmanager + for json_obj in json_objs[1:-1]: + rep = pm.hook.pytest_report_from_serializable( + config=pytestconfig, data=json_obj + ) + assert isinstance(rep, BaseReport) diff --git a/testing/test_reports.py b/testing/test_reports.py index 9f6c56186..ff813543c 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -330,7 +330,7 @@ class TestHooks: data = pytestconfig.hook.pytest_report_to_serializable( config=pytestconfig, report=rep ) - assert data["_report_type"] == "TestReport" + assert data["$report_type"] == "TestReport" new_rep = pytestconfig.hook.pytest_report_from_serializable( config=pytestconfig, data=data ) @@ -352,7 +352,7 @@ class TestHooks: data = pytestconfig.hook.pytest_report_to_serializable( config=pytestconfig, report=rep ) - assert data["_report_type"] == "CollectReport" + assert data["$report_type"] == "CollectReport" new_rep = pytestconfig.hook.pytest_report_from_serializable( config=pytestconfig, data=data ) @@ -376,7 +376,7 @@ class TestHooks: data = pytestconfig.hook.pytest_report_to_serializable( config=pytestconfig, report=rep ) - data["_report_type"] = "Unknown" + data["$report_type"] = "Unknown" with pytest.raises(AssertionError): _ = pytestconfig.hook.pytest_report_from_serializable( config=pytestconfig, data=data From 09096f74368a45b0d23181f3120cfe78aef9363f Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Sat, 26 Oct 2019 13:09:40 -0300 Subject: [PATCH 070/153] Remove 'experimental' status from report serialization hooks --- src/_pytest/hookspec.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 10a9857d7..7a21837bd 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -381,16 +381,6 @@ def pytest_runtest_logreport(report): @hookspec(firstresult=True) def pytest_report_to_serializable(config, report): """ - .. warning:: - This hook is experimental and subject to change between pytest releases, even - bug fixes. - - The intent is for this to be used by plugins maintained by the core-devs, such - as ``pytest-xdist``, ``pytest-subtests``, and as a replacement for the internal - 'resultlog' plugin. - - In the future it might become part of the public hook API. - Serializes the given report object into a data structure suitable for sending over the wire, e.g. converted to JSON. """ @@ -399,16 +389,6 @@ def pytest_report_to_serializable(config, report): @hookspec(firstresult=True) def pytest_report_from_serializable(config, data): """ - .. warning:: - This hook is experimental and subject to change between pytest releases, even - bug fixes. - - The intent is for this to be used by plugins maintained by the core-devs, such - as ``pytest-xdist``, ``pytest-subtests``, and as a replacement for the internal - 'resultlog' plugin. - - In the future it might become part of the public hook API. - Restores a report object previously serialized with pytest_report_to_serializable(). """ From 7a96d94fd4f98f725ce04f7ac45de041b521b86d Mon Sep 17 00:00:00 2001 From: Joshua Storck Date: Wed, 30 Oct 2019 14:18:13 -0400 Subject: [PATCH 071/153] Making it possible to access the pluginmanager in the pytest_addoption hook --- changelog/6061.feature.rst | 5 ++++ doc/en/example/markers.rst | 2 +- doc/en/example/parametrize.rst | 2 +- doc/en/example/simple.rst | 4 +-- doc/en/writing_plugins.rst | 52 +++++++++++++++++++++++++++++++++- src/_pytest/config/__init__.py | 4 ++- src/_pytest/hookspec.py | 7 ++++- testing/test_pluginmanager.py | 30 ++++++++++++++++++++ 8 files changed, 99 insertions(+), 7 deletions(-) create mode 100644 changelog/6061.feature.rst diff --git a/changelog/6061.feature.rst b/changelog/6061.feature.rst new file mode 100644 index 000000000..b7804a008 --- /dev/null +++ b/changelog/6061.feature.rst @@ -0,0 +1,5 @@ +Adding the pluginmanager as an option to :py:func:`~hookspec.pytest_addoption` +so that hooks can be invoked when setting up command line options. This is +useful for having one plugin communicate things to another plugin, +such as default values or which set of command line options to add. See +:ref:`Using hooks in pytest_addoption ` for more details. diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index ccddb1f66..8f81ff4d2 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -364,7 +364,7 @@ specifies via named environments: import pytest - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): parser.addoption( "-E", action="store", diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 1220cfb4d..6f3e8031b 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -36,7 +36,7 @@ Now we add a test configuration like this: # content of conftest.py - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): parser.addoption("--all", action="store_true", help="run all combinations") diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index a7cd06d31..85000b46d 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -33,7 +33,7 @@ provide the ``cmdopt`` through a :ref:`fixture function `: import pytest - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): parser.addoption( "--cmdopt", action="store", default="type1", help="my option: type1 or type2" ) @@ -151,7 +151,7 @@ line option to control skipping of ``pytest.mark.slow`` marked tests: import pytest - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): parser.addoption( "--runslow", action="store_true", default=False, help="run slow tests" ) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 5f429c219..818974d0b 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -338,7 +338,7 @@ string value of ``Hello World!`` if we do not supply a value or ``Hello import pytest - def pytest_addoption(parser): + def pytest_addoption(parser, pluginmanager): group = parser.getgroup("helloworld") group.addoption( "--name", @@ -677,6 +677,56 @@ Example: print(config.hook) +.. _`addoptionhooks`: + + +Using hooks in pytest_addoption +------------------------------- + +Occasionally, it is necessary to change the way in which command line options +are defined by one plugin based on hooks in another plugin. For example, +a plugin may expose a command line option for which another plugin needs +to define the default value. The pluginmanager can be used to install and +use hooks to accomplish this. The plugin would define and add the hooks +and use pytest_addoption as follows: + +.. code-block:: python + + # contents of hooks.py + + # Use firstresult=True because we only want one plugin to define this + # default value + @hookspec(firstresult=True) + def pytest_config_file_default_value(): + """ Return the default value for the config file command line option. """ + + + # contents of myplugin.py + + + def pytest_addhooks(pluginmanager): + """ This example assumes the hooks are grouped in the 'hooks' module. """ + from . import hook + + pluginmanager.add_hookspecs(hook) + + + def pytest_addoption(parser, pluginmanager): + default_value = pluginmanager.hook.pytest_config_file_default_value() + parser.addoption( + "--config-file", + help="Config file to use, defaults to %(default)s", + default=default_value, + ) + +The conftest.py that is using myplugin would simply define the hook as follows: + +.. code-block:: python + + def pytest_config_file_default_value(): + return "config.yaml" + + Optionally using hooks from 3rd party plugins --------------------------------------------- diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 1bab9877c..d09f80043 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -697,7 +697,9 @@ class Config: self._cleanup = [] # type: List[Callable[[], None]] self.pluginmanager.register(self, "pytestconfig") self._configured = False - self.hook.pytest_addoption.call_historic(kwargs=dict(parser=self._parser)) + self.hook.pytest_addoption.call_historic( + kwargs=dict(parser=self._parser, pluginmanager=self.pluginmanager) + ) @property def invocation_dir(self): diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 10a9857d7..3340b2153 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -35,7 +35,7 @@ def pytest_plugin_registered(plugin, manager): @hookspec(historic=True) -def pytest_addoption(parser): +def pytest_addoption(parser, pluginmanager): """register argparse-style options and ini-style config values, called once at the beginning of a test run. @@ -50,6 +50,11 @@ def pytest_addoption(parser): To add ini-file values call :py:func:`parser.addini(...) <_pytest.config.Parser.addini>`. + :arg _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager, + which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s + and allow one plugin to call another plugin's hooks to change how + command line options are added. + Options can later be accessed through the :py:class:`config <_pytest.config.Config>` object, respectively: diff --git a/testing/test_pluginmanager.py b/testing/test_pluginmanager.py index 97f220ca5..836b458c6 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -135,6 +135,36 @@ class TestPytestPluginInteractions: ihook_b = session.gethookproxy(testdir.tmpdir.join("tests")) assert ihook_a is not ihook_b + def test_hook_with_addoption(self, testdir): + """Test that hooks can be used in a call to pytest_addoption""" + testdir.makepyfile( + newhooks=""" + import pytest + @pytest.hookspec(firstresult=True) + def pytest_default_value(): + pass + """ + ) + testdir.makepyfile( + myplugin=""" + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.add_hookspecs(newhooks) + def pytest_addoption(parser, pluginmanager): + default_value = pluginmanager.hook.pytest_default_value() + parser.addoption("--config", help="Config, defaults to %(default)s", default=default_value) + """ + ) + testdir.makeconftest( + """ + pytest_plugins=("myplugin",) + def pytest_default_value(): + return "default_value" + """ + ) + res = testdir.runpytest("--help") + res.stdout.fnmatch_lines(["*--config=CONFIG*default_value*"]) + def test_default_markers(testdir): result = testdir.runpytest("--markers") From 0027908e9e5d23f9967e5f5465e0dda1b45046e6 Mon Sep 17 00:00:00 2001 From: Joshua Storck Date: Wed, 30 Oct 2019 15:02:18 -0400 Subject: [PATCH 072/153] Removing :py:func: and :ref: from changelog as it's not supported by towncrier --- changelog/6061.feature.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/changelog/6061.feature.rst b/changelog/6061.feature.rst index b7804a008..11f548625 100644 --- a/changelog/6061.feature.rst +++ b/changelog/6061.feature.rst @@ -1,5 +1,4 @@ -Adding the pluginmanager as an option to :py:func:`~hookspec.pytest_addoption` +Adding the pluginmanager as an option ``pytest_addoption`` so that hooks can be invoked when setting up command line options. This is useful for having one plugin communicate things to another plugin, -such as default values or which set of command line options to add. See -:ref:`Using hooks in pytest_addoption ` for more details. +such as default values or which set of command line options to add. From f4008042069b64111c4ad4ab047029a5e77f01ea Mon Sep 17 00:00:00 2001 From: Joshua Storck Date: Wed, 30 Oct 2019 16:25:50 -0400 Subject: [PATCH 073/153] Removing pluginmanager as parameter in definition of pytest_addoption hook --- doc/en/example/markers.rst | 2 +- doc/en/example/parametrize.rst | 2 +- doc/en/example/simple.rst | 4 ++-- doc/en/writing_plugins.rst | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index 8f81ff4d2..ccddb1f66 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -364,7 +364,7 @@ specifies via named environments: import pytest - def pytest_addoption(parser, pluginmanager): + def pytest_addoption(parser): parser.addoption( "-E", action="store", diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 6f3e8031b..1220cfb4d 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -36,7 +36,7 @@ Now we add a test configuration like this: # content of conftest.py - def pytest_addoption(parser, pluginmanager): + def pytest_addoption(parser): parser.addoption("--all", action="store_true", help="run all combinations") diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 85000b46d..a7cd06d31 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -33,7 +33,7 @@ provide the ``cmdopt`` through a :ref:`fixture function `: import pytest - def pytest_addoption(parser, pluginmanager): + def pytest_addoption(parser): parser.addoption( "--cmdopt", action="store", default="type1", help="my option: type1 or type2" ) @@ -151,7 +151,7 @@ line option to control skipping of ``pytest.mark.slow`` marked tests: import pytest - def pytest_addoption(parser, pluginmanager): + def pytest_addoption(parser): parser.addoption( "--runslow", action="store_true", default=False, help="run slow tests" ) diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 818974d0b..8660746bd 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -338,7 +338,7 @@ string value of ``Hello World!`` if we do not supply a value or ``Hello import pytest - def pytest_addoption(parser, pluginmanager): + def pytest_addoption(parser): group = parser.getgroup("helloworld") group.addoption( "--name", From 0c7c26fe6e7a9d53e080b3214898651042c6dbee Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 2 Nov 2019 10:01:27 +0100 Subject: [PATCH 074/153] FSCollector: keep/use given fspath Via https://github.com/blueyed/pytest/pull/42. --- src/_pytest/nodes.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 71036dc7e..e1dc21f8a 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -364,8 +364,9 @@ def _check_initialpaths_for_relpath(session, fspath): class FSCollector(Collector): - def __init__(self, fspath, parent=None, config=None, session=None, nodeid=None): - fspath = py.path.local(fspath) # xxx only for test_resultlog.py? + def __init__( + self, fspath: py.path.local, parent=None, config=None, session=None, nodeid=None + ) -> None: name = fspath.basename if parent is not None: rel = fspath.relto(parent.fspath) From 9303de877aefc5304522b2e2522c314e0616040c Mon Sep 17 00:00:00 2001 From: MarcoGorelli Date: Sun, 3 Nov 2019 16:48:06 +0000 Subject: [PATCH 075/153] Fix error in newly introduced test_collecterror Via https://github.com/pytest-dev/pytest/pull/6107. (cherry picked from commit 1b9fbbfa195aa20c48574265935dc5e66b96ec16) --- testing/test_terminal.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index e77102667..bc5ddfbe9 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1781,13 +1781,13 @@ def test_collecterror(testdir): result = testdir.runpytest("-ra", str(p1)) result.stdout.fnmatch_lines( [ - "collected 0 items / 1 errors", + "collected 0 items / 1 error", "*= ERRORS =*", "*_ ERROR collecting test_collecterror.py _*", "E SyntaxError: *", "*= short test summary info =*", "ERROR test_collecterror.py", - "*! Interrupted: 1 errors during collection !*", + "*! Interrupted: 1 error during collection !*", "*= 1 error in *", ] ) From dc30d78845970afd3089d4ff79b2df0eb86af156 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 3 Nov 2019 15:23:37 +0200 Subject: [PATCH 076/153] Add type annotations to _pytest._io.saferepr --- src/_pytest/_io/saferepr.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 7704421a2..908fd2183 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -1,8 +1,9 @@ import pprint import reprlib +from typing import Any -def _format_repr_exception(exc, obj): +def _format_repr_exception(exc: Exception, obj: Any) -> str: exc_name = type(exc).__name__ try: exc_info = str(exc) @@ -13,7 +14,7 @@ def _format_repr_exception(exc, obj): ) -def _ellipsize(s, maxsize): +def _ellipsize(s: str, maxsize: int) -> str: if len(s) > maxsize: i = max(0, (maxsize - 3) // 2) j = max(0, maxsize - 3 - i) @@ -26,19 +27,19 @@ class SafeRepr(reprlib.Repr): and includes information on exceptions raised during the call. """ - def __init__(self, maxsize): + def __init__(self, maxsize: int) -> None: super().__init__() self.maxstring = maxsize self.maxsize = maxsize - def repr(self, x): + def repr(self, x: Any) -> str: try: s = super().repr(x) except Exception as exc: s = _format_repr_exception(exc, x) return _ellipsize(s, self.maxsize) - def repr_instance(self, x, level): + def repr_instance(self, x: Any, level: int) -> str: try: s = repr(x) except Exception as exc: @@ -46,7 +47,7 @@ class SafeRepr(reprlib.Repr): return _ellipsize(s, self.maxsize) -def safeformat(obj): +def safeformat(obj: Any) -> str: """return a pretty printed string for the given object. Failing __repr__ functions of user instances will be represented with a short exception info. @@ -57,7 +58,7 @@ def safeformat(obj): return _format_repr_exception(exc, obj) -def saferepr(obj, maxsize=240): +def saferepr(obj: Any, maxsize: int = 240) -> str: """return a size-limited safe repr-string for the given object. Failing __repr__ functions of user instances will be represented with a short exception info and 'saferepr' generally takes From 18d181fa7776ced81249e18805c91c53c739834c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 3 Nov 2019 17:31:07 +0200 Subject: [PATCH 077/153] Remove dead code in _pytest.assertion.util._diff_text The function handles bytes input, however that is never used. The function has two callers: 1) ``` if istext(left) and istext(right): explanation = _diff_text(left, right, verbose ``` `istext` checks `isinstance(str)`. 2) ``` def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]: ... diff = _diff_text(correct_text, text, verbose ``` and `_notin_text` is called once: ``` if istext(left) and istext(right): explanation = _notin_text(left, right, verbose ``` --- src/_pytest/assertion/util.py | 19 +------------------ 1 file changed, 1 insertion(+), 18 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index ce29553d5..1d9fffd34 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -181,32 +181,15 @@ def assertrepr_compare(config, op, left, right): def _diff_text(left, right, verbose=0): - """Return the explanation for the diff between text or bytes. + """Return the explanation for the diff between text. Unless --verbose is used this will skip leading and trailing characters which are identical to keep the diff minimal. - - If the input are bytes they will be safely converted to text. """ from difflib import ndiff explanation = [] # type: List[str] - def escape_for_readable_diff(binary_text): - """ - Ensures that the internal string is always valid unicode, converting any bytes safely to valid unicode. - This is done using repr() which then needs post-processing to fix the encompassing quotes and un-escape - newlines and carriage returns (#429). - """ - r = str(repr(binary_text)[1:-1]) - r = r.replace(r"\n", "\n") - r = r.replace(r"\r", "\r") - return r - - if isinstance(left, bytes): - left = escape_for_readable_diff(left) - if isinstance(right, bytes): - right = escape_for_readable_diff(right) if verbose < 1: i = 0 # just in case left or right has zero length for i in range(min(len(left), len(right))): From 7d3ce374d28d7eadd85c59ed1e59319556e61635 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 3 Nov 2019 16:57:14 +0200 Subject: [PATCH 078/153] Add type annotations to _pytest.assertion.util --- src/_pytest/assertion/util.py | 65 ++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 23 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 1d9fffd34..46e578188 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -1,9 +1,15 @@ """Utilities for assertion debugging""" +import collections.abc import pprint -from collections.abc import Sequence +from typing import AbstractSet +from typing import Any from typing import Callable +from typing import Iterable from typing import List +from typing import Mapping from typing import Optional +from typing import Sequence +from typing import Tuple import _pytest._code from _pytest import outcomes @@ -22,7 +28,7 @@ _reprcompare = None # type: Optional[Callable[[str, object, object], Optional[s _assertion_pass = None # type: Optional[Callable[[int, str, str], None]] -def format_explanation(explanation): +def format_explanation(explanation: str) -> str: """This formats an explanation Normally all embedded newlines are escaped, however there are @@ -38,7 +44,7 @@ def format_explanation(explanation): return "\n".join(result) -def _split_explanation(explanation): +def _split_explanation(explanation: str) -> List[str]: """Return a list of individual lines in the explanation This will return a list of lines split on '\n{', '\n}' and '\n~'. @@ -55,7 +61,7 @@ def _split_explanation(explanation): return lines -def _format_lines(lines): +def _format_lines(lines: Sequence[str]) -> List[str]: """Format the individual lines This will replace the '{', '}' and '~' characters of our mini @@ -64,7 +70,7 @@ def _format_lines(lines): Return a list of formatted lines. """ - result = lines[:1] + result = list(lines[:1]) stack = [0] stackcnt = [0] for line in lines[1:]: @@ -90,31 +96,31 @@ def _format_lines(lines): return result -def issequence(x): - return isinstance(x, Sequence) and not isinstance(x, str) +def issequence(x: Any) -> bool: + return isinstance(x, collections.abc.Sequence) and not isinstance(x, str) -def istext(x): +def istext(x: Any) -> bool: return isinstance(x, str) -def isdict(x): +def isdict(x: Any) -> bool: return isinstance(x, dict) -def isset(x): +def isset(x: Any) -> bool: return isinstance(x, (set, frozenset)) -def isdatacls(obj): +def isdatacls(obj: Any) -> bool: return getattr(obj, "__dataclass_fields__", None) is not None -def isattrs(obj): +def isattrs(obj: Any) -> bool: return getattr(obj, "__attrs_attrs__", None) is not None -def isiterable(obj): +def isiterable(obj: Any) -> bool: try: iter(obj) return not istext(obj) @@ -122,7 +128,7 @@ def isiterable(obj): return False -def assertrepr_compare(config, op, left, right): +def assertrepr_compare(config, op: str, left: Any, right: Any) -> Optional[List[str]]: """Return specialised explanations for some operators/operands""" verbose = config.getoption("verbose") if verbose > 1: @@ -180,7 +186,7 @@ def assertrepr_compare(config, op, left, right): return [summary] + explanation -def _diff_text(left, right, verbose=0): +def _diff_text(left: str, right: str, verbose: int = 0) -> List[str]: """Return the explanation for the diff between text. Unless --verbose is used this will skip leading and trailing @@ -226,7 +232,7 @@ def _diff_text(left, right, verbose=0): return explanation -def _compare_eq_verbose(left, right): +def _compare_eq_verbose(left: Any, right: Any) -> List[str]: keepends = True left_lines = repr(left).splitlines(keepends) right_lines = repr(right).splitlines(keepends) @@ -238,7 +244,7 @@ def _compare_eq_verbose(left, right): return explanation -def _surrounding_parens_on_own_lines(lines): # type: (List) -> None +def _surrounding_parens_on_own_lines(lines: List[str]) -> None: """Move opening/closing parenthesis/bracket to own lines.""" opening = lines[0][:1] if opening in ["(", "[", "{"]: @@ -250,7 +256,9 @@ def _surrounding_parens_on_own_lines(lines): # type: (List) -> None lines[:] = lines + [closing] -def _compare_eq_iterable(left, right, verbose=0): +def _compare_eq_iterable( + left: Iterable[Any], right: Iterable[Any], verbose: int = 0 +) -> List[str]: if not verbose: return ["Use -v to get the full diff"] # dynamic import to speedup pytest @@ -283,7 +291,9 @@ def _compare_eq_iterable(left, right, verbose=0): return explanation -def _compare_eq_sequence(left, right, verbose=0): +def _compare_eq_sequence( + left: Sequence[Any], right: Sequence[Any], verbose: int = 0 +) -> List[str]: comparing_bytes = isinstance(left, bytes) and isinstance(right, bytes) explanation = [] # type: List[str] len_left = len(left) @@ -337,7 +347,9 @@ def _compare_eq_sequence(left, right, verbose=0): return explanation -def _compare_eq_set(left, right, verbose=0): +def _compare_eq_set( + left: AbstractSet[Any], right: AbstractSet[Any], verbose: int = 0 +) -> List[str]: explanation = [] diff_left = left - right diff_right = right - left @@ -352,7 +364,9 @@ def _compare_eq_set(left, right, verbose=0): return explanation -def _compare_eq_dict(left, right, verbose=0): +def _compare_eq_dict( + left: Mapping[Any, Any], right: Mapping[Any, Any], verbose: int = 0 +) -> List[str]: explanation = [] # type: List[str] set_left = set(left) set_right = set(right) @@ -391,7 +405,12 @@ def _compare_eq_dict(left, right, verbose=0): return explanation -def _compare_eq_cls(left, right, verbose, type_fns): +def _compare_eq_cls( + left: Any, + right: Any, + verbose: int, + type_fns: Tuple[Callable[[Any], bool], Callable[[Any], bool]], +) -> List[str]: isdatacls, isattrs = type_fns if isdatacls(left): all_fields = left.__dataclass_fields__ @@ -425,7 +444,7 @@ def _compare_eq_cls(left, right, verbose, type_fns): return explanation -def _notin_text(term, text, verbose=0): +def _notin_text(term: str, text: str, verbose: int = 0) -> List[str]: index = text.find(term) head = text[:index] tail = text[index + len(term) :] From 68dbc24dcbb6e2bf1aa03289caff112786eb92d2 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 3 Nov 2019 21:13:53 +0100 Subject: [PATCH 079/153] test_group_warnings_by_message: ignore own PytestExperimentalApiWarning --- testing/test_warnings.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/test_warnings.py b/testing/test_warnings.py index bbcf87e5a..c4af14dac 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -605,6 +605,7 @@ def test_warnings_checker_twice(): warnings.warn("Message B", UserWarning) +@pytest.mark.filterwarnings("ignore::pytest.PytestExperimentalApiWarning") @pytest.mark.filterwarnings("always") def test_group_warnings_by_message(testdir): testdir.copy_example("warnings/test_group_warnings_by_message.py") From 9f800b2a77e60637ad8ece1c62c3c4352e70d410 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 3 Nov 2019 21:01:37 +0100 Subject: [PATCH 080/153] test_terminal: reduce number of tests (single --fulltrace param) Remove the `--fulltrace` arg from the `Option` fixture used in several tests, but not checked for. Only use it with `test_keyboard_interrupt`. (removes 8 tests, coverage not affected) --- testing/test_terminal.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index bc5ddfbe9..d2f43888a 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -24,27 +24,19 @@ DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"]) class Option: - def __init__(self, verbosity=0, fulltrace=False): + def __init__(self, verbosity=0): self.verbosity = verbosity - self.fulltrace = fulltrace @property def args(self): values = [] values.append("--verbosity=%d" % self.verbosity) - if self.fulltrace: - values.append("--fulltrace") return values @pytest.fixture( - params=[ - Option(verbosity=0), - Option(verbosity=1), - Option(verbosity=-1), - Option(fulltrace=True), - ], - ids=["default", "verbose", "quiet", "fulltrace"], + params=[Option(verbosity=0), Option(verbosity=1), Option(verbosity=-1)], + ids=["default", "verbose", "quiet"], ) def option(request): return request.param @@ -207,7 +199,8 @@ class TestTerminal: result.stdout.fnmatch_lines(["*a123/test_hello123.py*PASS*"]) result.stdout.no_fnmatch_line("* <- *") - def test_keyboard_interrupt(self, testdir, option): + @pytest.mark.parametrize("fulltrace", ("", "--fulltrace")) + def test_keyboard_interrupt(self, testdir, fulltrace): testdir.makepyfile( """ def test_foobar(): @@ -219,7 +212,7 @@ class TestTerminal: """ ) - result = testdir.runpytest(*option.args, no_reraise_ctrlc=True) + result = testdir.runpytest(fulltrace, no_reraise_ctrlc=True) result.stdout.fnmatch_lines( [ " def test_foobar():", @@ -228,7 +221,7 @@ class TestTerminal: "*_keyboard_interrupt.py:6: KeyboardInterrupt*", ] ) - if option.fulltrace: + if fulltrace: result.stdout.fnmatch_lines( ["*raise KeyboardInterrupt # simulating the user*"] ) From 0d79061432639308ecf3e3c2e838080567c84e20 Mon Sep 17 00:00:00 2001 From: MarcoGorelli Date: Wed, 30 Oct 2019 20:49:20 +0000 Subject: [PATCH 081/153] Color percentage indicator according to color of final line indicate current outcome/status with color of percentage indicator Fix type annotation, refactor _write_progress_information_filling_space Keep code in _get_main_color as similar as possible to how it was before Write test Make black-compliant Fix error in newly introduced test_collecterror Make tests more readable by using constants and f-strings Remove accidentally added monkeypatch Make Python 3.5-compatible, add changelog entry Add newline at the end of changelog file --- changelog/6097.improvement.rst | 1 + src/_pytest/terminal.py | 21 ++++++++++++---- testing/test_terminal.py | 45 ++++++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 changelog/6097.improvement.rst diff --git a/changelog/6097.improvement.rst b/changelog/6097.improvement.rst new file mode 100644 index 000000000..32eb84906 --- /dev/null +++ b/changelog/6097.improvement.rst @@ -0,0 +1 @@ +The "[XXX%]" indicator in the test summary is colored according to the final (new) multi-colored line's main color. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index c3663e695..d26df2d2c 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -15,6 +15,7 @@ from typing import List from typing import Mapping from typing import Optional from typing import Set +from typing import Tuple import attr import pluggy @@ -458,18 +459,20 @@ class TerminalReporter: else: progress_length = len(" [100%]") + main_color, _ = _get_main_color(self.stats) + self._progress_nodeids_reported.add(nodeid) is_last_item = ( len(self._progress_nodeids_reported) == self._session.testscollected ) if is_last_item: - self._write_progress_information_filling_space() + self._write_progress_information_filling_space(color=main_color) else: w = self._width_of_current_line past_edge = w + progress_length + 1 >= self._screen_width if past_edge: msg = self._get_progress_information_message() - self._tw.write(msg + "\n", cyan=True) + self._tw.write(msg + "\n", **{main_color: True}) def _get_progress_information_message(self): collected = self._session.testscollected @@ -486,11 +489,13 @@ class TerminalReporter: return " [{:3d}%]".format(progress) return " [100%]" - def _write_progress_information_filling_space(self): + def _write_progress_information_filling_space(self, color=None): + if not color: + color, _ = _get_main_color(self.stats) msg = self._get_progress_information_message() w = self._width_of_current_line fill = self._tw.fullwidth - w - 1 - self.write(msg.rjust(fill), cyan=True) + self.write(msg.rjust(fill), **{color: True}) @property def _width_of_current_line(self): @@ -1075,7 +1080,7 @@ def _make_plural(count, noun): return count, noun + "s" if count != 1 else noun -def build_summary_stats_line(stats): +def _get_main_color(stats) -> Tuple[str, List[str]]: known_types = ( "failed passed skipped deselected xfailed xpassed warnings error".split() ) @@ -1096,6 +1101,12 @@ def build_summary_stats_line(stats): else: main_color = "yellow" + return main_color, known_types + + +def build_summary_stats_line(stats): + main_color, known_types = _get_main_color(stats) + parts = [] for key in known_types: reports = stats.get(key, None) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index e77102667..02de45ff9 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -21,6 +21,10 @@ from _pytest.terminal import getreportopt from _pytest.terminal import TerminalReporter DistInfo = collections.namedtuple("DistInfo", ["project_name", "version"]) +RED = r"\x1b\[31m" +GREEN = r"\x1b\[32m" +YELLOW = r"\x1b\[33m" +RESET = r"\x1b\[0m" class Option: @@ -1494,6 +1498,43 @@ class TestProgressOutputStyle: ] ) + def test_colored_progress(self, testdir, monkeypatch): + monkeypatch.setenv("PY_COLORS", "1") + testdir.makepyfile( + test_bar=""" + import pytest + @pytest.mark.parametrize('i', range(10)) + def test_bar(i): pass + """, + test_foo=""" + import pytest + import warnings + @pytest.mark.parametrize('i', range(5)) + def test_foo(i): + warnings.warn(DeprecationWarning("collection")) + pass + """, + test_foobar=""" + import pytest + @pytest.mark.parametrize('i', range(5)) + def test_foobar(i): raise ValueError() + """, + ) + output = testdir.runpytest() + output.stdout.re_match_lines( + [ + r"test_bar.py ({green}\.{reset}){{10}}{green} \s+ \[ 50%\]{reset}".format( + green=GREEN, reset=RESET + ), + r"test_foo.py ({green}\.{reset}){{5}}{yellow} \s+ \[ 75%\]{reset}".format( + green=GREEN, reset=RESET, yellow=YELLOW + ), + r"test_foobar.py ({red}F{reset}){{5}}{red} \s+ \[100%\]{reset}".format( + reset=RESET, red=RED + ), + ] + ) + def test_count(self, many_tests_files, testdir): testdir.makeini( """ @@ -1781,13 +1822,13 @@ def test_collecterror(testdir): result = testdir.runpytest("-ra", str(p1)) result.stdout.fnmatch_lines( [ - "collected 0 items / 1 errors", + "collected 0 items / 1 error", "*= ERRORS =*", "*_ ERROR collecting test_collecterror.py _*", "E SyntaxError: *", "*= short test summary info =*", "ERROR test_collecterror.py", - "*! Interrupted: 1 errors during collection !*", + "*! Interrupted: 1 error during collection !*", "*= 1 error in *", ] ) From 741f0fedd1d8661ed671fdc7a1091dd42203a56f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 4 Nov 2019 14:53:43 +0100 Subject: [PATCH 082/153] typing around Node.location, reportinfo, repr_excinfo etc --- src/_pytest/_code/code.py | 37 +++++++++++++----------- src/_pytest/doctest.py | 2 +- src/_pytest/fixtures.py | 14 ++++++--- src/_pytest/main.py | 13 ++++++--- src/_pytest/nodes.py | 55 +++++++++++++++++++++++++----------- src/_pytest/python.py | 3 +- src/_pytest/reports.py | 20 +++++++++---- src/_pytest/runner.py | 6 ++-- testing/code/test_excinfo.py | 2 +- 9 files changed, 100 insertions(+), 52 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 18f576332..1a35521ad 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -549,7 +549,7 @@ class ExceptionInfo(Generic[_E]): funcargs: bool = False, truncate_locals: bool = True, chain: bool = True, - ): + ) -> Union["ReprExceptionInfo", "ExceptionChainRepr"]: """ Return str()able representation of this exception info. @@ -818,19 +818,19 @@ class FormattedExcinfo: return traceback, extraline - def repr_excinfo(self, excinfo): - + def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr": repr_chain = ( [] ) # type: List[Tuple[ReprTraceback, Optional[ReprFileLocation], Optional[str]]] e = excinfo.value + excinfo_ = excinfo # type: Optional[ExceptionInfo] descr = None seen = set() # type: Set[int] while e is not None and id(e) not in seen: seen.add(id(e)) - if excinfo: - reprtraceback = self.repr_traceback(excinfo) - reprcrash = excinfo._getreprcrash() + if excinfo_: + reprtraceback = self.repr_traceback(excinfo_) + reprcrash = excinfo_._getreprcrash() # type: Optional[ReprFileLocation] else: # fallback to native repr if the exception doesn't have a traceback: # ExceptionInfo objects require a full traceback to work @@ -842,7 +842,7 @@ class FormattedExcinfo: repr_chain += [(reprtraceback, reprcrash, descr)] if e.__cause__ is not None and self.chain: e = e.__cause__ - excinfo = ( + excinfo_ = ( ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None @@ -852,7 +852,7 @@ class FormattedExcinfo: e.__context__ is not None and not e.__suppress_context__ and self.chain ): e = e.__context__ - excinfo = ( + excinfo_ = ( ExceptionInfo((type(e), e, e.__traceback__)) if e.__traceback__ else None @@ -876,6 +876,9 @@ class TerminalRepr: def __repr__(self): return "<{} instance at {:0x}>".format(self.__class__, id(self)) + def toterminal(self, tw) -> None: + raise NotImplementedError() + class ExceptionRepr(TerminalRepr): def __init__(self) -> None: @@ -884,7 +887,7 @@ class ExceptionRepr(TerminalRepr): def addsection(self, name, content, sep="-"): self.sections.append((name, content, sep)) - def toterminal(self, tw): + def toterminal(self, tw) -> None: for name, content, sep in self.sections: tw.sep(sep, name) tw.line(content) @@ -899,7 +902,7 @@ class ExceptionChainRepr(ExceptionRepr): self.reprtraceback = chain[-1][0] self.reprcrash = chain[-1][1] - def toterminal(self, tw): + def toterminal(self, tw) -> None: for element in self.chain: element[0].toterminal(tw) if element[2] is not None: @@ -914,7 +917,7 @@ class ReprExceptionInfo(ExceptionRepr): self.reprtraceback = reprtraceback self.reprcrash = reprcrash - def toterminal(self, tw): + def toterminal(self, tw) -> None: self.reprtraceback.toterminal(tw) super().toterminal(tw) @@ -927,7 +930,7 @@ class ReprTraceback(TerminalRepr): self.extraline = extraline self.style = style - def toterminal(self, tw): + def toterminal(self, tw) -> None: # the entries might have different styles for i, entry in enumerate(self.reprentries): if entry.style == "long": @@ -959,7 +962,7 @@ class ReprEntryNative(TerminalRepr): def __init__(self, tblines): self.lines = tblines - def toterminal(self, tw): + def toterminal(self, tw) -> None: tw.write("".join(self.lines)) @@ -971,7 +974,7 @@ class ReprEntry(TerminalRepr): self.reprfileloc = filelocrepr self.style = style - def toterminal(self, tw): + def toterminal(self, tw) -> None: if self.style == "short": self.reprfileloc.toterminal(tw) for line in self.lines: @@ -1003,7 +1006,7 @@ class ReprFileLocation(TerminalRepr): self.lineno = lineno self.message = message - def toterminal(self, tw): + def toterminal(self, tw) -> None: # filename and lineno output for each entry, # using an output format that most editors unterstand msg = self.message @@ -1018,7 +1021,7 @@ class ReprLocals(TerminalRepr): def __init__(self, lines): self.lines = lines - def toterminal(self, tw): + def toterminal(self, tw) -> None: for line in self.lines: tw.line(line) @@ -1027,7 +1030,7 @@ class ReprFuncArgs(TerminalRepr): def __init__(self, args): self.args = args - def toterminal(self, tw): + def toterminal(self, tw) -> None: if self.args: linesofar = "" for name, value in self.args: diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 48c934e3a..f7d96257e 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -305,7 +305,7 @@ class DoctestItem(pytest.Item): else: return super().repr_failure(excinfo) - def reportinfo(self): + def reportinfo(self) -> Tuple[str, int, str]: return self.fspath, self.dtest.lineno, "[doctest] %s" % self.name diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index aae1371ec..fc55ef2cf 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -7,13 +7,13 @@ from collections import defaultdict from collections import deque from collections import OrderedDict from typing import Dict +from typing import List from typing import Tuple import attr import py import _pytest -from _pytest import nodes from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest.compat import _format_args @@ -35,6 +35,8 @@ from _pytest.outcomes import TEST_OUTCOME if False: # TYPE_CHECKING from typing import Type + from _pytest import nodes + @attr.s(frozen=True) class PseudoFixtureDef: @@ -689,8 +691,8 @@ class FixtureLookupError(LookupError): self.fixturestack = request._get_fixturestack() self.msg = msg - def formatrepr(self): - tblines = [] + def formatrepr(self) -> "FixtureLookupErrorRepr": + tblines = [] # type: List[str] addline = tblines.append stack = [self.request._pyfuncitem.obj] stack.extend(map(lambda x: x.func, self.fixturestack)) @@ -742,7 +744,7 @@ class FixtureLookupErrorRepr(TerminalRepr): self.firstlineno = firstlineno self.argname = argname - def toterminal(self, tw): + def toterminal(self, tw) -> None: # tw.line("FixtureLookupError: %s" %(self.argname), red=True) for tbline in self.tblines: tw.line(tbline.rstrip()) @@ -1283,6 +1285,8 @@ class FixtureManager: except AttributeError: pass else: + from _pytest import nodes + # construct the base nodeid which is later used to check # what fixtures are visible for particular tests (as denoted # by their test id) @@ -1459,6 +1463,8 @@ class FixtureManager: return tuple(self._matchfactories(fixturedefs, nodeid)) def _matchfactories(self, fixturedefs, nodeid): + from _pytest import nodes + for fixturedef in fixturedefs: if nodes.ischildnode(fixturedef.baseid, nodeid): yield fixturedef diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 7b3855e6c..084d68dab 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -5,6 +5,7 @@ import functools import importlib import os import sys +from typing import Dict import attr import py @@ -16,6 +17,7 @@ from _pytest.config import hookimpl from _pytest.config import UsageError from _pytest.outcomes import exit from _pytest.runner import collect_one_node +from _pytest.runner import SetupState class ExitCode(enum.IntEnum): @@ -359,8 +361,8 @@ class Failed(Exception): class _bestrelpath_cache(dict): path = attr.ib() - def __missing__(self, path): - r = self.path.bestrelpath(path) + def __missing__(self, path: str) -> str: + r = self.path.bestrelpath(path) # type: str self[path] = r return r @@ -368,6 +370,7 @@ class _bestrelpath_cache(dict): class Session(nodes.FSCollector): Interrupted = Interrupted Failed = Failed + _setupstate = None # type: SetupState def __init__(self, config): nodes.FSCollector.__init__( @@ -383,7 +386,9 @@ class Session(nodes.FSCollector): self._initialpaths = frozenset() # Keep track of any collected nodes in here, so we don't duplicate fixtures self._node_cache = {} - self._bestrelpathcache = _bestrelpath_cache(config.rootdir) + self._bestrelpathcache = _bestrelpath_cache( + config.rootdir + ) # type: Dict[str, str] # Dirnames of pkgs with dunder-init files. self._pkg_roots = {} @@ -398,7 +403,7 @@ class Session(nodes.FSCollector): self.testscollected, ) - def _node_location_to_relpath(self, node_path): + def _node_location_to_relpath(self, node_path: str) -> str: # bestrelpath is a quite slow function return self._bestrelpathcache[node_path] diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index 71036dc7e..d72eaeb0a 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -4,6 +4,7 @@ from functools import lru_cache from typing import Any from typing import Dict from typing import List +from typing import Optional from typing import Set from typing import Tuple from typing import Union @@ -11,15 +12,21 @@ from typing import Union import py import _pytest._code +from _pytest._code.code import ExceptionChainRepr +from _pytest._code.code import ExceptionInfo +from _pytest._code.code import ReprExceptionInfo from _pytest.compat import getfslineno +from _pytest.fixtures import FixtureDef +from _pytest.fixtures import FixtureLookupError +from _pytest.fixtures import FixtureLookupErrorRepr from _pytest.mark.structures import Mark from _pytest.mark.structures import MarkDecorator from _pytest.mark.structures import NodeKeywords -from _pytest.outcomes import fail +from _pytest.outcomes import Failed if False: # TYPE_CHECKING # Imported here due to circular import. - from _pytest.fixtures import FixtureDef + from _pytest.main import Session # noqa: F401 SEP = "/" @@ -69,8 +76,14 @@ class Node: Collector subclasses have children, Items are terminal nodes.""" def __init__( - self, name, parent=None, config=None, session=None, fspath=None, nodeid=None - ): + self, + name, + parent=None, + config=None, + session: Optional["Session"] = None, + fspath=None, + nodeid=None, + ) -> None: #: a unique name within the scope of the parent node self.name = name @@ -81,7 +94,11 @@ class Node: self.config = config or parent.config #: the session this node is part of - self.session = session or parent.session + if session is None: + assert parent.session is not None + self.session = parent.session + else: + self.session = session #: filesystem path where this node was collected from (can be None) self.fspath = fspath or getattr(parent, "fspath", None) @@ -254,13 +271,13 @@ class Node: def _prunetraceback(self, excinfo): pass - def _repr_failure_py(self, excinfo, style=None): - # Type ignored: see comment where fail.Exception is defined. - if excinfo.errisinstance(fail.Exception): # type: ignore + def _repr_failure_py( + self, excinfo: ExceptionInfo[Union[Failed, FixtureLookupError]], style=None + ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: + if isinstance(excinfo.value, Failed): if not excinfo.value.pytrace: return str(excinfo.value) - fm = self.session._fixturemanager - if excinfo.errisinstance(fm.FixtureLookupError): + if isinstance(excinfo.value, FixtureLookupError): return excinfo.value.formatrepr() if self.config.getoption("fulltrace", False): style = "long" @@ -298,7 +315,9 @@ class Node: truncate_locals=truncate_locals, ) - def repr_failure(self, excinfo, style=None): + def repr_failure( + self, excinfo, style=None + ) -> Union[str, ReprExceptionInfo, ExceptionChainRepr, FixtureLookupErrorRepr]: return self._repr_failure_py(excinfo, style) @@ -425,16 +444,20 @@ class Item(Node): if content: self._report_sections.append((when, key, content)) - def reportinfo(self): + def reportinfo(self) -> Tuple[str, Optional[int], str]: return self.fspath, None, "" @property - def location(self): + def location(self) -> Tuple[str, Optional[int], str]: try: return self._location except AttributeError: location = self.reportinfo() fspath = self.session._node_location_to_relpath(location[0]) - location = (fspath, location[1], str(location[2])) - self._location = location - return location + assert type(location[2]) is str + self._location = ( + fspath, + location[1], + location[2], + ) # type: Tuple[str, Optional[int], str] + return self._location diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 913a93bc0..61cbfec8a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -9,6 +9,7 @@ from collections import Counter from collections.abc import Sequence from functools import partial from textwrap import dedent +from typing import Tuple import py @@ -288,7 +289,7 @@ class PyobjMixin(PyobjContext): s = ".".join(parts) return s.replace(".[", "[") - def reportinfo(self): + def reportinfo(self) -> Tuple[str, int, str]: # XXX caching? obj = self.obj compat_co_firstlineno = getattr(obj, "compat_co_firstlineno", None) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index b1592f817..53f28e73f 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -1,6 +1,8 @@ from io import StringIO from pprint import pprint +from typing import List from typing import Optional +from typing import Tuple from typing import Union import py @@ -15,6 +17,7 @@ from _pytest._code.code import ReprFuncArgs from _pytest._code.code import ReprLocals from _pytest._code.code import ReprTraceback from _pytest._code.code import TerminalRepr +from _pytest.nodes import Node from _pytest.outcomes import skip from _pytest.pathlib import Path @@ -34,13 +37,16 @@ def getslaveinfoline(node): class BaseReport: when = None # type: Optional[str] location = None + longrepr = None + sections = [] # type: List[Tuple[str, str]] + nodeid = None # type: str def __init__(self, **kw): self.__dict__.update(kw) - def toterminal(self, out): + def toterminal(self, out) -> None: if hasattr(self, "node"): - out.line(getslaveinfoline(self.node)) + out.line(getslaveinfoline(self.node)) # type: ignore longrepr = self.longrepr if longrepr is None: @@ -300,7 +306,9 @@ class TestReport(BaseReport): class CollectReport(BaseReport): when = "collect" - def __init__(self, nodeid, outcome, longrepr, result, sections=(), **extra): + def __init__( + self, nodeid: str, outcome, longrepr, result: List[Node], sections=(), **extra + ) -> None: self.nodeid = nodeid self.outcome = outcome self.longrepr = longrepr @@ -322,7 +330,7 @@ class CollectErrorRepr(TerminalRepr): def __init__(self, msg): self.longrepr = msg - def toterminal(self, out): + def toterminal(self, out) -> None: out.line(self.longrepr, red=True) @@ -472,7 +480,9 @@ def _report_kwargs_from_json(reportdict): description, ) ) - exception_info = ExceptionChainRepr(chain) + exception_info = ExceptionChainRepr( + chain + ) # type: Union[ExceptionChainRepr,ReprExceptionInfo] else: exception_info = ReprExceptionInfo(reprtraceback, reprcrash) diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 29f9658ee..c383146c3 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -6,6 +6,7 @@ from time import time from typing import Callable from typing import Dict from typing import List +from typing import Optional from typing import Tuple import attr @@ -207,8 +208,7 @@ class CallInfo: """ Result/Exception info a function invocation. """ _result = attr.ib() - # Optional[ExceptionInfo] - excinfo = attr.ib() + excinfo = attr.ib(type=Optional[ExceptionInfo]) start = attr.ib() stop = attr.ib() when = attr.ib() @@ -220,7 +220,7 @@ class CallInfo: return self._result @classmethod - def from_call(cls, func, when, reraise=None): + def from_call(cls, func, when, reraise=None) -> "CallInfo": #: context of invocation: one of "setup", "call", #: "teardown", "memocollect" start = time() diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 3f205b131..b431bb66d 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -902,7 +902,7 @@ raise ValueError() from _pytest._code.code import TerminalRepr class MyRepr(TerminalRepr): - def toterminal(self, tw): + def toterminal(self, tw) -> None: tw.line("я") x = str(MyRepr()) From 262ed567d065cf2455f988a60def75133a711345 Mon Sep 17 00:00:00 2001 From: Tibor Arpas Date: Tue, 5 Nov 2019 22:10:27 +0100 Subject: [PATCH 083/153] tests: clean up chmod-related tests to fix rm_rf warnings Fixed https://github.com/pytest-dev/pytest/issues/5974#issuecomment-549822509. --- testing/test_cacheprovider.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index dfc3c5320..6513909bc 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -1,5 +1,6 @@ import os import shutil +import stat import sys import textwrap @@ -45,14 +46,17 @@ class TestNewAPI: ) def test_cache_writefail_permissions(self, testdir): testdir.makeini("[pytest]") + mode = os.stat(testdir.tmpdir.ensure_dir(".pytest_cache"))[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) config = testdir.parseconfigure() cache = config.cache cache.set("test/broken", []) + testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode) @pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows") @pytest.mark.filterwarnings("default") def test_cache_failure_warns(self, testdir): + mode = os.stat(testdir.tmpdir.ensure_dir(".pytest_cache"))[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) testdir.makepyfile( """ @@ -62,6 +66,7 @@ class TestNewAPI: """ ) result = testdir.runpytest("-rw") + testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode) assert result.ret == 1 # warnings from nodeids, lastfailed, and stepwise result.stdout.fnmatch_lines(["*could not create cache path*", "*3 warnings*"]) From dc2c51302ab06f9f643944fee788839de8d26b33 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 5 Nov 2019 22:11:56 +0100 Subject: [PATCH 084/153] Revert "tests: filterwarnings: do not crash with "(rm_rf)" warning" This reverts commit 6b2bae9392f4fdbf295fbca8082e58f280c90aac. --- tox.ini | 2 -- 1 file changed, 2 deletions(-) diff --git a/tox.ini b/tox.ini index edc9a5667..93889322f 100644 --- a/tox.ini +++ b/tox.ini @@ -142,8 +142,6 @@ filterwarnings = error default:Using or importing the ABCs:DeprecationWarning:unittest2.* ignore:Module already imported so cannot be rewritten:pytest.PytestWarning - # https://github.com/pytest-dev/pytest/issues/5974 - default:\(rm_rf\) error removing.*:pytest.PytestWarning # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8). ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest)) # produced by pytest-xdist From 9309ae299ae94caf66187f98ad500ef6f082d762 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 5 Nov 2019 22:28:32 +0100 Subject: [PATCH 085/153] Use try/finally to ensure chmod is run, filter warning --- testing/test_cacheprovider.py | 40 +++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 6513909bc..6a5a5af8b 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -48,28 +48,36 @@ class TestNewAPI: testdir.makeini("[pytest]") mode = os.stat(testdir.tmpdir.ensure_dir(".pytest_cache"))[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) - config = testdir.parseconfigure() - cache = config.cache - cache.set("test/broken", []) - testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode) + try: + config = testdir.parseconfigure() + cache = config.cache + cache.set("test/broken", []) + finally: + testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode) @pytest.mark.skipif(sys.platform.startswith("win"), reason="no chmod on windows") - @pytest.mark.filterwarnings("default") + @pytest.mark.filterwarnings( + "ignore:could not create cache path:pytest.PytestWarning" + ) def test_cache_failure_warns(self, testdir): mode = os.stat(testdir.tmpdir.ensure_dir(".pytest_cache"))[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) - testdir.makepyfile( - """ - def test_error(): - raise Exception + try: + testdir.makepyfile( + """ + def test_error(): + raise Exception - """ - ) - result = testdir.runpytest("-rw") - testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode) - assert result.ret == 1 - # warnings from nodeids, lastfailed, and stepwise - result.stdout.fnmatch_lines(["*could not create cache path*", "*3 warnings*"]) + """ + ) + result = testdir.runpytest("-rw") + assert result.ret == 1 + # warnings from nodeids, lastfailed, and stepwise + result.stdout.fnmatch_lines( + ["*could not create cache path*", "*3 warnings*"] + ) + finally: + testdir.tmpdir.ensure_dir(".pytest_cache").chmod(mode) def test_config_cache(self, testdir): testdir.makeconftest( From d8096925fac4093deda89bc952e12e46bb9557ab Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 5 Nov 2019 23:04:27 +0100 Subject: [PATCH 086/153] Fix for Python 3.5 not handling LocalPath --- testing/test_cacheprovider.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 6a5a5af8b..3f03b5ff9 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -46,7 +46,8 @@ class TestNewAPI: ) def test_cache_writefail_permissions(self, testdir): testdir.makeini("[pytest]") - mode = os.stat(testdir.tmpdir.ensure_dir(".pytest_cache"))[stat.ST_MODE] + cache_dir = str(testdir.tmpdir.ensure_dir(".pytest_cache")) + mode = os.stat(cache_dir)[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) try: config = testdir.parseconfigure() @@ -60,7 +61,8 @@ class TestNewAPI: "ignore:could not create cache path:pytest.PytestWarning" ) def test_cache_failure_warns(self, testdir): - mode = os.stat(testdir.tmpdir.ensure_dir(".pytest_cache"))[stat.ST_MODE] + cache_dir = str(testdir.tmpdir.ensure_dir(".pytest_cache")) + mode = os.stat(cache_dir)[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) try: testdir.makepyfile( From ce3d43100267622edbfcf5786d20bdd23c8e29af Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 6 Nov 2019 11:18:20 +0100 Subject: [PATCH 087/153] assert: fix _compare_eq_iterable: re-format both sides Follow-up to 946434c61 (#5924). Before this patch the test would look like this: {'env': {'sub...s wrapped'}}}} == {'env': {'sub...}}}, 'new': 1} Omitting 1 identical items, use -vv to show Right contains 1 more item: {'new': 1} Full diff: { 'env': {'sub': {'long_a': 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', - 'sub1': {'long_a': 'substring ' + 'sub1': {'long_a': 'substring that gets wrapped'}}}, ? +++++++++++++++++ ++++ + 'new': 1, - 'that ' - 'gets ' - 'wrapped'}}}, } --- src/_pytest/assertion/util.py | 9 +++++---- testing/test_assertion.py | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 46e578188..dff275902 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -273,12 +273,13 @@ def _compare_eq_iterable( if lines_left != lines_right: if lines_left > lines_right: max_width = min(len(x) for x in left_formatting) - right_formatting = pprint.pformat(right, width=max_width).splitlines() - lines_right = len(right_formatting) else: max_width = min(len(x) for x in right_formatting) - left_formatting = pprint.pformat(left, width=max_width).splitlines() - lines_left = len(left_formatting) + + right_formatting = pprint.pformat(right, width=max_width).splitlines() + lines_right = len(right_formatting) + left_formatting = pprint.pformat(left, width=max_width).splitlines() + lines_left = len(left_formatting) if lines_left > 1 or lines_right > 1: _surrounding_parens_on_own_lines(left_formatting) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 7b99a65b4..b7b84528b 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -459,6 +459,43 @@ class TestAssert_reprcompare: " ]", ] + def test_dict_wrap(self): + d1 = {"common": 1, "env": {"env1": 1}} + d2 = {"common": 1, "env": {"env1": 1, "env2": 2}} + + diff = callequal(d1, d2, verbose=True) + assert diff == [ + "{'common': 1,...: {'env1': 1}} == {'common': 1,...1, 'env2': 2}}", + "Omitting 1 identical items, use -vv to show", + "Differing items:", + "{'env': {'env1': 1}} != {'env': {'env1': 1, 'env2': 2}}", + "Full diff:", + "- {'common': 1, 'env': {'env1': 1}}", + "+ {'common': 1, 'env': {'env1': 1, 'env2': 2}}", + "? +++++++++++", + ] + + long_a = "a" * 80 + sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped"}} + d1 = {"env": {"sub": sub}} + d2 = {"env": {"sub": sub}, "new": 1} + diff = callequal(d1, d2, verbose=True) + assert diff == [ + "{'env': {'sub...s wrapped'}}}} == {'env': {'sub...}}}, 'new': 1}", + "Omitting 1 identical items, use -vv to show", + "Right contains 1 more item:", + "{'new': 1}", + "Full diff:", + " {", + " 'env': {'sub': {'long_a': '" + long_a + "',", + " 'sub1': {'long_a': 'substring '", + " 'that '", + " 'gets '", + " 'wrapped'}}},", + "+ 'new': 1,", + " }", + ] + def test_dict(self): expl = callequal({"a": 0}, {"a": 1}) assert len(expl) > 1 From eb7a4e32ad920b4cdd9c956763535fed194ae8a7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 23 Oct 2019 06:59:18 +0200 Subject: [PATCH 088/153] saferepr: handle BaseExceptions This causes INTERNALERRORs with pytest-django, which uses `pytest.fail` (derived from `BaseException`) to prevent DB access, when pytest then tries to e.g. display the `repr()` for a Django `QuerySet` etc. Ref: https://github.com/pytest-dev/pytest-django/pull/776 --- changelog/6047.bugfix.rst | 1 + src/_pytest/_io/saferepr.py | 32 ++++++++++----- testing/code/test_excinfo.py | 4 +- testing/io/test_saferepr.py | 78 ++++++++++++++++++++++++++++++++++-- testing/test_session.py | 18 ++++----- 5 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 changelog/6047.bugfix.rst diff --git a/changelog/6047.bugfix.rst b/changelog/6047.bugfix.rst new file mode 100644 index 000000000..11a997f71 --- /dev/null +++ b/changelog/6047.bugfix.rst @@ -0,0 +1 @@ +BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 908fd2183..7fded872d 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -3,14 +3,24 @@ import reprlib from typing import Any -def _format_repr_exception(exc: Exception, obj: Any) -> str: - exc_name = type(exc).__name__ +def _try_repr_or_str(obj): try: - exc_info = str(exc) - except Exception: - exc_info = "unknown" - return '<[{}("{}") raised in repr()] {} object at 0x{:x}>'.format( - exc_name, exc_info, obj.__class__.__name__, id(obj) + return repr(obj) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException: + return '{}("{}")'.format(type(obj).__name__, obj) + + +def _format_repr_exception(exc: BaseException, obj: Any) -> str: + try: + exc_info = _try_repr_or_str(exc) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: + exc_info = "unpresentable exception ({})".format(_try_repr_or_str(exc)) + return "<[{} raised in repr()] {} object at 0x{:x}>".format( + exc_info, obj.__class__.__name__, id(obj) ) @@ -35,14 +45,18 @@ class SafeRepr(reprlib.Repr): def repr(self, x: Any) -> str: try: s = super().repr(x) - except Exception as exc: + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: s = _format_repr_exception(exc, x) return _ellipsize(s, self.maxsize) def repr_instance(self, x: Any, level: int) -> str: try: s = repr(x) - except Exception as exc: + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: s = _format_repr_exception(exc, x) return _ellipsize(s, self.maxsize) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index b431bb66d..262d1d184 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -584,7 +584,7 @@ raise ValueError() reprlocals = p.repr_locals(loc) assert reprlocals.lines assert reprlocals.lines[0] == "__builtins__ = " - assert '[NotImplementedError("") raised in repr()]' in reprlocals.lines[1] + assert "[NotImplementedError() raised in repr()]" in reprlocals.lines[1] def test_repr_local_with_exception_in_class_property(self): class ExceptionWithBrokenClass(Exception): @@ -602,7 +602,7 @@ raise ValueError() reprlocals = p.repr_locals(loc) assert reprlocals.lines assert reprlocals.lines[0] == "__builtins__ = " - assert '[ExceptionWithBrokenClass("") raised in repr()]' in reprlocals.lines[1] + assert "[ExceptionWithBrokenClass() raised in repr()]" in reprlocals.lines[1] def test_repr_local_truncated(self): loc = {"l": [i for i in range(10)]} diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 86897b57c..db86ea4d5 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -1,3 +1,4 @@ +import pytest from _pytest._io.saferepr import saferepr @@ -40,9 +41,80 @@ def test_exceptions(): assert "TypeError" in s assert "TypeError" in saferepr(BrokenRepr("string")) - s2 = saferepr(BrokenRepr(BrokenReprException("omg even worse"))) - assert "NameError" not in s2 - assert "unknown" in s2 + try: + None() + except Exception as exc: + exp_exc = repr(exc) + obj = BrokenRepr(BrokenReprException("omg even worse")) + s2 = saferepr(obj) + assert s2 == ( + "<[unpresentable exception ({!s}) raised in repr()] BrokenRepr object at 0x{:x}>".format( + exp_exc, id(obj) + ) + ) + + +def test_baseexception(): + """Test saferepr() with BaseExceptions, which includes pytest outcomes.""" + + class RaisingOnStrRepr(BaseException): + def __init__(self, exc_types): + self.exc_types = exc_types + + def raise_exc(self, *args): + try: + self.exc_type = self.exc_types.pop(0) + except IndexError: + pass + if hasattr(self.exc_type, "__call__"): + raise self.exc_type(*args) + raise self.exc_type + + def __str__(self): + self.raise_exc("__str__") + + def __repr__(self): + self.raise_exc("__repr__") + + class BrokenObj: + def __init__(self, exc): + self.exc = exc + + def __repr__(self): + raise self.exc + + __str__ = __repr__ + + baseexc_str = BaseException("__str__") + obj = BrokenObj(RaisingOnStrRepr([BaseException])) + assert saferepr(obj) == ( + "<[unpresentable exception ({!r}) " + "raised in repr()] BrokenObj object at 0x{:x}>".format(baseexc_str, id(obj)) + ) + obj = BrokenObj(RaisingOnStrRepr([RaisingOnStrRepr([BaseException])])) + assert saferepr(obj) == ( + "<[{!r} raised in repr()] BrokenObj object at 0x{:x}>".format( + baseexc_str, id(obj) + ) + ) + + with pytest.raises(KeyboardInterrupt): + saferepr(BrokenObj(KeyboardInterrupt())) + + with pytest.raises(SystemExit): + saferepr(BrokenObj(SystemExit())) + + with pytest.raises(KeyboardInterrupt): + saferepr(BrokenObj(RaisingOnStrRepr([KeyboardInterrupt]))) + + with pytest.raises(SystemExit): + saferepr(BrokenObj(RaisingOnStrRepr([SystemExit]))) + + with pytest.raises(KeyboardInterrupt): + print(saferepr(BrokenObj(RaisingOnStrRepr([BaseException, KeyboardInterrupt])))) + + with pytest.raises(SystemExit): + saferepr(BrokenObj(RaisingOnStrRepr([BaseException, SystemExit]))) def test_buggy_builtin_repr(): diff --git a/testing/test_session.py b/testing/test_session.py index dbe057376..7b4eb817a 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -102,15 +102,20 @@ class SessionTests: p = testdir.makepyfile( """ import pytest + + class reprexc(BaseException): + def __str__(self): + return "Ha Ha fooled you, I'm a broken repr()." + class BrokenRepr1(object): foo=0 def __repr__(self): - raise Exception("Ha Ha fooled you, I'm a broken repr().") + raise reprexc class TestBrokenClass(object): def test_explicit_bad_repr(self): t = BrokenRepr1() - with pytest.raises(Exception, match="I'm a broken repr"): + with pytest.raises(BaseException, match="broken repr"): repr(t) def test_implicit_bad_repr1(self): @@ -123,12 +128,7 @@ class SessionTests: passed, skipped, failed = reprec.listoutcomes() assert (len(passed), len(skipped), len(failed)) == (1, 0, 1) out = failed[0].longrepr.reprcrash.message - assert ( - out.find( - """[Exception("Ha Ha fooled you, I'm a broken repr().") raised in repr()]""" - ) - != -1 - ) + assert out.find("<[reprexc() raised in repr()] BrokenRepr1") != -1 def test_broken_repr_with_showlocals_verbose(self, testdir): p = testdir.makepyfile( @@ -151,7 +151,7 @@ class SessionTests: assert repr_locals.lines assert len(repr_locals.lines) == 1 assert repr_locals.lines[0].startswith( - 'x = <[NotImplementedError("") raised in repr()] ObjWithErrorInRepr' + "x = <[NotImplementedError() raised in repr()] ObjWithErrorInRepr" ) def test_skip_file_by_conftest(self, testdir): From fee7c7b032b5995339375e8cfbaf8f9832aeb512 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 6 Nov 2019 14:10:20 +0100 Subject: [PATCH 089/153] py38: do not call None() directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Works around: _____ ERROR collecting testing/io/test_saferepr.py _____ src/_pytest/python.py:502: in _importtestmodule mod = self.fspath.pyimport(ensuresyspath=importmode) .venv38/lib/python3.8/site-packages/py/_path/local.py:701: in pyimport __import__(modname) :991: in _find_and_load ??? :975: in _find_and_load_unlocked ??? :671: in _load_unlocked ??? src/_pytest/assertion/rewrite.py:136: in exec_module source_stat, co = _rewrite_test(fn, self.config) src/_pytest/assertion/rewrite.py:288: in _rewrite_test co = compile(tree, fn, "exec", dont_inherit=True) E File "…/Vcs/pytest/testing/io/test_saferepr.py", line 45 E None() E ^ E SyntaxError: 'NoneType' object is not callable; perhaps you missed a comma? --- testing/io/test_saferepr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index db86ea4d5..e24d9b470 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -41,9 +41,10 @@ def test_exceptions(): assert "TypeError" in s assert "TypeError" in saferepr(BrokenRepr("string")) + none = None try: - None() - except Exception as exc: + none() + except BaseException as exc: exp_exc = repr(exc) obj = BrokenRepr(BrokenReprException("omg even worse")) s2 = saferepr(obj) From c4a110b20a8c26c9eec89e8f0e496633dab42c98 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 2 Nov 2019 10:07:54 +0100 Subject: [PATCH 090/153] Session: collect: keep/use already parsed initialpart Via https://github.com/blueyed/pytest/pull/42. --- src/_pytest/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 084d68dab..d10d2d871 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -488,22 +488,22 @@ class Session(nodes.FSCollector): def collect(self): for initialpart in self._initialparts: - arg = "::".join(map(str, initialpart)) - self.trace("processing argument", arg) + self.trace("processing argument", initialpart) self.trace.root.indent += 1 try: - yield from self._collect(arg) + yield from self._collect(initialpart) except NoMatch: + report_arg = "::".join(map(str, initialpart)) # we are inside a make_report hook so # we cannot directly pass through the exception - self._notfound.append((arg, sys.exc_info()[1])) + self._notfound.append((report_arg, sys.exc_info()[1])) self.trace.root.indent -= 1 def _collect(self, arg): from _pytest.python import Package - names = self._parsearg(arg) + names = arg[:] argpath = names.pop(0) # Start with a Session root, and delve to argpath item (dir or file) From cb21a8db1d0dbbf1284eb0e644a8cd7b8fc837be Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Nov 2019 11:43:59 +0100 Subject: [PATCH 091/153] test_source: do not instantiate Source objects during collection --- testing/code/test_source.py | 43 +++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 15e0bf24a..5e7e1abf5 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -127,14 +127,15 @@ def test_isparseable(): class TestAccesses: - source = Source( - """\ - def f(x): - pass - def g(x): - pass - """ - ) + def setup_class(self): + self.source = Source( + """\ + def f(x): + pass + def g(x): + pass + """ + ) def test_getrange(self): x = self.source[0:2] @@ -155,14 +156,15 @@ class TestAccesses: class TestSourceParsingAndCompiling: - source = Source( - """\ - def f(x): - assert (x == - 3 + - 4) - """ - ).strip() + def setup_class(self): + self.source = Source( + """\ + def f(x): + assert (x == + 3 + + 4) + """ + ).strip() def test_compile(self): co = _pytest._code.compile("x=3") @@ -619,7 +621,8 @@ x = 3 class TestTry: - source = """\ + def setup_class(self): + self.source = """\ try: raise ValueError except Something: @@ -646,7 +649,8 @@ else: class TestTryFinally: - source = """\ + def setup_class(self): + self.source = """\ try: raise ValueError finally: @@ -663,7 +667,8 @@ finally: class TestIf: - source = """\ + def setup_class(self): + self.source = """\ if 1: y = 3 elif False: From e8a3d1adf2c2688e01e0eaff2110df0d8fc5acd6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Nov 2019 11:48:51 +0100 Subject: [PATCH 092/153] Fix test_trace_with_parametrize_handles_shared_fixtureinfo for colors --- testing/test_pdb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 718a463ed..25d2292e9 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1086,7 +1086,7 @@ class TestTraceOption: child.sendline("c") child.expect_exact("> PDB continue (IO-capturing resumed) >") rest = child.read().decode("utf8") - assert "6 passed in" in rest + assert "= \x1b[32m\x1b[1m6 passed\x1b[0m\x1b[32m in" in rest assert "reading from stdin while output" not in rest # Only printed once - not on stderr. assert "Exit: Quitting debugger" not in child.before.decode("utf8") From 5c00226847e41f734b496e3ba7c7fb8ded0f6739 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Nov 2019 12:23:39 +0100 Subject: [PATCH 093/153] test_iterable_full_diff: use test ids --- testing/test_assertion.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index b7b84528b..aac21a0df 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -358,7 +358,7 @@ class TestAssert_reprcompare: @pytest.mark.parametrize( ["left", "right", "expected"], [ - ( + pytest.param( [0, 1], [0, 2], """ @@ -368,8 +368,9 @@ class TestAssert_reprcompare: + [0, 2] ? ^ """, + id="lists", ), - ( + pytest.param( {0: 1}, {0: 2}, """ @@ -379,8 +380,9 @@ class TestAssert_reprcompare: + {0: 2} ? ^ """, + id="dicts", ), - ( + pytest.param( {0, 1}, {0, 2}, """ @@ -390,6 +392,7 @@ class TestAssert_reprcompare: + {0, 2} ? ^ """, + id="sets", ), ], ) From dd6cf7c172afbe6a67483e6a5986432f2f5f043f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Nov 2019 12:25:46 +0100 Subject: [PATCH 094/153] test_exc_chain_repr_without_traceback: use ids --- testing/code/test_excinfo.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index b431bb66d..3fa561225 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1229,13 +1229,15 @@ raise ValueError() @pytest.mark.parametrize( "reason, description", [ - ( + pytest.param( "cause", "The above exception was the direct cause of the following exception:", + id="cause", ), - ( + pytest.param( "context", "During handling of the above exception, another exception occurred:", + id="context", ), ], ) From dd852ded705e10c85452fba1458736e71dcb06e0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 6 Nov 2019 19:51:58 +0100 Subject: [PATCH 095/153] _perform_collect: remove comment about untested code Harden one test where it is tested. All tests testing this: testing/acceptance_test.py:184(TestGeneralUsage::test_not_collectable_arguments) testing/acceptance_test.py:373(TestGeneralUsage::test_direct_addressing_notfound) testing/acceptance_test.py:403(TestGeneralUsage::test_issue134_report_error_when_collecting_member[test_fun.py::test_a]) testing/acceptance_test.py:420(TestGeneralUsage::test_report_all_failed_collections_initargs) testing/test_config.py:1309(test_config_blocked_default_plugins[python]) (via https://github.com/blueyed/pytest/pull/88) --- src/_pytest/main.py | 1 - testing/acceptance_test.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 084d68dab..ed4204b08 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -476,7 +476,6 @@ class Session(nodes.FSCollector): for arg, exc in self._notfound: line = "(no name {!r} in any of {!r})".format(arg, exc.args[0]) errors.append("not found: {}\n{}".format(arg, line)) - # XXX: test this raise UsageError(*errors) if not genitems: return rep.result diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 82c727fc6..578ab45eb 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -178,8 +178,14 @@ class TestGeneralUsage: p1 = testdir.makepyfile("") p2 = testdir.makefile(".pyc", "123") result = testdir.runpytest(p1, p2) - assert result.ret - result.stderr.fnmatch_lines(["*ERROR: not found:*{}".format(p2.basename)]) + assert result.ret == ExitCode.USAGE_ERROR + result.stderr.fnmatch_lines( + [ + "ERROR: not found: {}".format(p2), + "(no name {!r} in any of [[][]])".format(str(p2)), + "", + ] + ) @pytest.mark.filterwarnings("default") def test_better_reporting_on_conftest_load_failure(self, testdir, request): From 8aa0809fbc9a91ec286ca02bd7f0b673e06905de Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 27 Oct 2019 02:28:35 +0100 Subject: [PATCH 096/153] on_rm_rf_error: ignore os.open (no warning) Ref: https://github.com/pytest-dev/pytest/pull/6044/files#r339321752 --- src/_pytest/pathlib.py | 11 ++++++----- testing/test_tmpdir.py | 7 +++++++ 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index 543103fb5..bd76fac6b 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -68,13 +68,14 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: return False if func not in (os.rmdir, os.remove, os.unlink): - warnings.warn( - PytestWarning( - "(rm_rf) unknown function {} when removing {}:\n{}: {}".format( - path, func, exctype, excvalue + if func not in (os.open,): + warnings.warn( + PytestWarning( + "(rm_rf) unknown function {} when removing {}:\n{}: {}".format( + path, func, exctype, excvalue + ) ) ) - ) return False # Chmod + retry. diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 0ebed22ac..2433d6145 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -393,6 +393,13 @@ class TestRmRf: on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path) assert fn.is_file() + # ignored function + with pytest.warns(None) as warninfo: + exc_info = (None, PermissionError(), None) + on_rm_rf_error(os.open, str(fn), exc_info, start_path=tmp_path) + assert fn.is_file() + assert not [x.message for x in warninfo] + exc_info = (None, PermissionError(), None) on_rm_rf_error(os.unlink, str(fn), exc_info, start_path=tmp_path) assert not fn.is_file() From 2e5cf1cc789908ab4856c84a99d7d1120f84e694 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 28 Oct 2019 12:34:40 +0100 Subject: [PATCH 097/153] Fix order of format args with warning --- src/_pytest/pathlib.py | 2 +- testing/test_tmpdir.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/_pytest/pathlib.py b/src/_pytest/pathlib.py index bd76fac6b..8d25b21dd 100644 --- a/src/_pytest/pathlib.py +++ b/src/_pytest/pathlib.py @@ -72,7 +72,7 @@ def on_rm_rf_error(func, path: str, exc, *, start_path: Path) -> bool: warnings.warn( PytestWarning( "(rm_rf) unknown function {} when removing {}:\n{}: {}".format( - path, func, exctype, excvalue + func, path, exctype, excvalue ) ) ) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 2433d6145..29b6db947 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -388,7 +388,10 @@ class TestRmRf: assert not on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path) # unknown function - with pytest.warns(pytest.PytestWarning): + with pytest.warns( + pytest.PytestWarning, + match=r"^\(rm_rf\) unknown function None when removing .*foo.txt:\nNone: ", + ): exc_info = (None, PermissionError(), None) on_rm_rf_error(None, str(fn), exc_info, start_path=tmp_path) assert fn.is_file() From 2adc84ed6c67116ceb8f1fb22c20d943f0bd0c38 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Nov 2019 12:55:01 +0100 Subject: [PATCH 098/153] changelog --- changelog/6074.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/6074.bugfix.rst diff --git a/changelog/6074.bugfix.rst b/changelog/6074.bugfix.rst new file mode 100644 index 000000000..624cf5d1c --- /dev/null +++ b/changelog/6074.bugfix.rst @@ -0,0 +1 @@ +pytester: fix order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. From b2537b22d7799d02621a68c1ff4b49adc98693cf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 3 Nov 2019 21:52:12 +0200 Subject: [PATCH 099/153] Add type annotations to _pytest.warning_types --- src/_pytest/warning_types.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/_pytest/warning_types.py b/src/_pytest/warning_types.py index 80353ccbc..22cb17dba 100644 --- a/src/_pytest/warning_types.py +++ b/src/_pytest/warning_types.py @@ -1,6 +1,14 @@ +from typing import Any +from typing import Generic +from typing import TypeVar + import attr +if False: # TYPE_CHECKING + from typing import Type # noqa: F401 (used in type string) + + class PytestWarning(UserWarning): """ Bases: :class:`UserWarning`. @@ -72,7 +80,7 @@ class PytestExperimentalApiWarning(PytestWarning, FutureWarning): __module__ = "pytest" @classmethod - def simple(cls, apiname): + def simple(cls, apiname: str) -> "PytestExperimentalApiWarning": return cls( "{apiname} is an experimental api that may change over time".format( apiname=apiname @@ -103,17 +111,20 @@ class PytestUnknownMarkWarning(PytestWarning): __module__ = "pytest" +_W = TypeVar("_W", bound=PytestWarning) + + @attr.s -class UnformattedWarning: +class UnformattedWarning(Generic[_W]): """Used to hold warnings that need to format their message at runtime, as opposed to a direct message. Using this class avoids to keep all the warning types and messages in this module, avoiding misuse. """ - category = attr.ib() - template = attr.ib() + category = attr.ib(type="Type[_W]") + template = attr.ib(type=str) - def format(self, **kwargs): + def format(self, **kwargs: Any) -> _W: """Returns an instance of the warning category, formatted with given kwargs""" return self.category(self.template.format(**kwargs)) From 58f2849bf6c46790b2d2f9975b61cf1a9bbd9e92 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 3 Nov 2019 23:05:42 +0200 Subject: [PATCH 100/153] Add type annotations to _pytest._code.source At least most of it. --- src/_pytest/_code/source.py | 73 +++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 23 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index db78bbd0d..1e9dd5031 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -7,10 +7,17 @@ import tokenize import warnings from ast import PyCF_ONLY_AST as _AST_FLAG from bisect import bisect_right +from types import FrameType from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple +from typing import Union import py +from _pytest.compat import overload + class Source: """ an immutable object holding a source code fragment, @@ -19,7 +26,7 @@ class Source: _compilecounter = 0 - def __init__(self, *parts, **kwargs): + def __init__(self, *parts, **kwargs) -> None: self.lines = lines = [] # type: List[str] de = kwargs.get("deindent", True) for part in parts: @@ -48,7 +55,15 @@ class Source: # Ignore type because of https://github.com/python/mypy/issues/4266. __hash__ = None # type: ignore - def __getitem__(self, key): + @overload + def __getitem__(self, key: int) -> str: + raise NotImplementedError() + + @overload # noqa: F811 + def __getitem__(self, key: slice) -> "Source": + raise NotImplementedError() + + def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811 if isinstance(key, int): return self.lines[key] else: @@ -58,10 +73,10 @@ class Source: newsource.lines = self.lines[key.start : key.stop] return newsource - def __len__(self): + def __len__(self) -> int: return len(self.lines) - def strip(self): + def strip(self) -> "Source": """ return new source object with trailing and leading blank lines removed. """ @@ -74,18 +89,20 @@ class Source: source.lines[:] = self.lines[start:end] return source - def putaround(self, before="", after="", indent=" " * 4): + def putaround( + self, before: str = "", after: str = "", indent: str = " " * 4 + ) -> "Source": """ return a copy of the source object with 'before' and 'after' wrapped around it. """ - before = Source(before) - after = Source(after) + beforesource = Source(before) + aftersource = Source(after) newsource = Source() lines = [(indent + line) for line in self.lines] - newsource.lines = before.lines + lines + after.lines + newsource.lines = beforesource.lines + lines + aftersource.lines return newsource - def indent(self, indent=" " * 4): + def indent(self, indent: str = " " * 4) -> "Source": """ return a copy of the source object with all lines indented by the given indent-string. """ @@ -93,14 +110,14 @@ class Source: newsource.lines = [(indent + line) for line in self.lines] return newsource - def getstatement(self, lineno): + def getstatement(self, lineno: int) -> "Source": """ return Source statement which contains the given linenumber (counted from 0). """ start, end = self.getstatementrange(lineno) return self[start:end] - def getstatementrange(self, lineno): + def getstatementrange(self, lineno: int): """ return (start, end) tuple which spans the minimal statement region which containing the given lineno. """ @@ -109,13 +126,13 @@ class Source: ast, start, end = getstatementrange_ast(lineno, self) return start, end - def deindent(self): + def deindent(self) -> "Source": """return a new source object deindented.""" newsource = Source() newsource.lines[:] = deindent(self.lines) return newsource - def isparseable(self, deindent=True): + def isparseable(self, deindent: bool = True) -> bool: """ return True if source is parseable, heuristically deindenting it by default. """ @@ -135,11 +152,16 @@ class Source: else: return True - def __str__(self): + def __str__(self) -> str: return "\n".join(self.lines) def compile( - self, filename=None, mode="exec", flag=0, dont_inherit=0, _genframe=None + self, + filename=None, + mode="exec", + flag: int = 0, + dont_inherit: int = 0, + _genframe: Optional[FrameType] = None, ): """ return compiled code object. if filename is None invent an artificial filename which displays @@ -183,7 +205,7 @@ class Source: # -def compile_(source, filename=None, mode="exec", flags=0, dont_inherit=0): +def compile_(source, filename=None, mode="exec", flags: int = 0, dont_inherit: int = 0): """ compile the given source to a raw code object, and maintain an internal cache which allows later retrieval of the source code for the code object @@ -233,7 +255,7 @@ def getfslineno(obj): # -def findsource(obj): +def findsource(obj) -> Tuple[Optional[Source], int]: try: sourcelines, lineno = inspect.findsource(obj) except Exception: @@ -243,7 +265,7 @@ def findsource(obj): return source, lineno -def getsource(obj, **kwargs): +def getsource(obj, **kwargs) -> Source: from .code import getrawcode obj = getrawcode(obj) @@ -255,21 +277,21 @@ def getsource(obj, **kwargs): return Source(strsrc, **kwargs) -def deindent(lines): +def deindent(lines: Sequence[str]) -> List[str]: return textwrap.dedent("\n".join(lines)).splitlines() -def get_statement_startend2(lineno, node): +def get_statement_startend2(lineno: int, node: ast.AST) -> Tuple[int, Optional[int]]: import ast # flatten all statements and except handlers into one lineno-list # AST's line numbers start indexing at 1 - values = [] + values = [] # type: List[int] for x in ast.walk(node): if isinstance(x, (ast.stmt, ast.ExceptHandler)): values.append(x.lineno - 1) for name in ("finalbody", "orelse"): - val = getattr(x, name, None) + val = getattr(x, name, None) # type: Optional[List[ast.stmt]] if val: # treat the finally/orelse part as its own statement values.append(val[0].lineno - 1 - 1) @@ -283,7 +305,12 @@ def get_statement_startend2(lineno, node): return start, end -def getstatementrange_ast(lineno, source: Source, assertion=False, astnode=None): +def getstatementrange_ast( + lineno: int, + source: Source, + assertion: bool = False, + astnode: Optional[ast.AST] = None, +) -> Tuple[ast.AST, int, int]: if astnode is None: content = str(source) # See #4260: From 265a9eb6a2916a8224d0561c269dc52d59d2ac48 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 6 Nov 2019 21:35:39 +0200 Subject: [PATCH 101/153] Add type annotations to some of _pytest.pytester --- src/_pytest/pytester.py | 118 +++++++++++++++++++++++++++------------- 1 file changed, 81 insertions(+), 37 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 14db8409e..6b45e077b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1,4 +1,5 @@ """(disabled by default) support for testing pytest and pytest plugins.""" +import collections.abc import gc import importlib import os @@ -8,9 +9,15 @@ import subprocess import sys import time import traceback -from collections.abc import Sequence from fnmatch import fnmatch from io import StringIO +from typing import Callable +from typing import Dict +from typing import Iterable +from typing import List +from typing import Optional +from typing import Sequence +from typing import Tuple from typing import Union from weakref import WeakKeyDictionary @@ -21,10 +28,16 @@ from _pytest._code import Source from _pytest._io.saferepr import saferepr from _pytest.capture import MultiCapture from _pytest.capture import SysCapture +from _pytest.fixtures import FixtureRequest from _pytest.main import ExitCode from _pytest.main import Session from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import Path +from _pytest.reports import TestReport + +if False: # TYPE_CHECKING + from typing import Type + IGNORE_PAM = [ # filenames added when obtaining details about the current user "/var/lib/sss/mc/passwd" @@ -142,7 +155,7 @@ class LsofFdLeakChecker: @pytest.fixture -def _pytest(request): +def _pytest(request: FixtureRequest) -> "PytestArg": """Return a helper which offers a gethookrecorder(hook) method which returns a HookRecorder instance which helps to make assertions about called hooks. @@ -152,10 +165,10 @@ def _pytest(request): class PytestArg: - def __init__(self, request): + def __init__(self, request: FixtureRequest) -> None: self.request = request - def gethookrecorder(self, hook): + def gethookrecorder(self, hook) -> "HookRecorder": hookrecorder = HookRecorder(hook._pm) self.request.addfinalizer(hookrecorder.finish_recording) return hookrecorder @@ -176,6 +189,11 @@ class ParsedCall: del d["_name"] return "".format(self._name, d) + if False: # TYPE_CHECKING + # The class has undetermined attributes, this tells mypy about it. + def __getattr__(self, key): + raise NotImplementedError() + class HookRecorder: """Record all hooks called in a plugin manager. @@ -185,27 +203,27 @@ class HookRecorder: """ - def __init__(self, pluginmanager): + def __init__(self, pluginmanager) -> None: self._pluginmanager = pluginmanager - self.calls = [] + self.calls = [] # type: List[ParsedCall] - def before(hook_name, hook_impls, kwargs): + def before(hook_name: str, hook_impls, kwargs) -> None: self.calls.append(ParsedCall(hook_name, kwargs)) - def after(outcome, hook_name, hook_impls, kwargs): + def after(outcome, hook_name: str, hook_impls, kwargs) -> None: pass self._undo_wrapping = pluginmanager.add_hookcall_monitoring(before, after) - def finish_recording(self): + def finish_recording(self) -> None: self._undo_wrapping() - def getcalls(self, names): + def getcalls(self, names: Union[str, Iterable[str]]) -> List[ParsedCall]: if isinstance(names, str): names = names.split() return [call for call in self.calls if call._name in names] - def assert_contains(self, entries): + def assert_contains(self, entries) -> None: __tracebackhide__ = True i = 0 entries = list(entries) @@ -226,7 +244,7 @@ class HookRecorder: else: pytest.fail("could not find {!r} check {!r}".format(name, check)) - def popcall(self, name): + def popcall(self, name: str) -> ParsedCall: __tracebackhide__ = True for i, call in enumerate(self.calls): if call._name == name: @@ -236,20 +254,27 @@ class HookRecorder: lines.extend([" %s" % x for x in self.calls]) pytest.fail("\n".join(lines)) - def getcall(self, name): + def getcall(self, name: str) -> ParsedCall: values = self.getcalls(name) assert len(values) == 1, (name, values) return values[0] # functionality for test reports - def getreports(self, names="pytest_runtest_logreport pytest_collectreport"): + def getreports( + self, + names: Union[ + str, Iterable[str] + ] = "pytest_runtest_logreport pytest_collectreport", + ) -> List[TestReport]: return [x.report for x in self.getcalls(names)] def matchreport( self, - inamepart="", - names="pytest_runtest_logreport pytest_collectreport", + inamepart: str = "", + names: Union[ + str, Iterable[str] + ] = "pytest_runtest_logreport pytest_collectreport", when=None, ): """return a testreport whose dotted import path matches""" @@ -275,13 +300,20 @@ class HookRecorder: ) return values[0] - def getfailures(self, names="pytest_runtest_logreport pytest_collectreport"): + def getfailures( + self, + names: Union[ + str, Iterable[str] + ] = "pytest_runtest_logreport pytest_collectreport", + ) -> List[TestReport]: return [rep for rep in self.getreports(names) if rep.failed] - def getfailedcollections(self): + def getfailedcollections(self) -> List[TestReport]: return self.getfailures("pytest_collectreport") - def listoutcomes(self): + def listoutcomes( + self + ) -> Tuple[List[TestReport], List[TestReport], List[TestReport]]: passed = [] skipped = [] failed = [] @@ -296,31 +328,31 @@ class HookRecorder: failed.append(rep) return passed, skipped, failed - def countoutcomes(self): + def countoutcomes(self) -> List[int]: return [len(x) for x in self.listoutcomes()] - def assertoutcome(self, passed=0, skipped=0, failed=0): + def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: realpassed, realskipped, realfailed = self.listoutcomes() assert passed == len(realpassed) assert skipped == len(realskipped) assert failed == len(realfailed) - def clear(self): + def clear(self) -> None: self.calls[:] = [] @pytest.fixture -def linecomp(request): +def linecomp(request: FixtureRequest) -> "LineComp": return LineComp() @pytest.fixture(name="LineMatcher") -def LineMatcher_fixture(request): +def LineMatcher_fixture(request: FixtureRequest) -> "Type[LineMatcher]": return LineMatcher @pytest.fixture -def testdir(request, tmpdir_factory): +def testdir(request: FixtureRequest, tmpdir_factory) -> "Testdir": return Testdir(request, tmpdir_factory) @@ -363,7 +395,13 @@ class RunResult: :ivar duration: duration in seconds """ - def __init__(self, ret: Union[int, ExitCode], outlines, errlines, duration) -> None: + def __init__( + self, + ret: Union[int, ExitCode], + outlines: Sequence[str], + errlines: Sequence[str], + duration: float, + ) -> None: try: self.ret = pytest.ExitCode(ret) # type: Union[int, ExitCode] except ValueError: @@ -374,13 +412,13 @@ class RunResult: self.stderr = LineMatcher(errlines) self.duration = duration - def __repr__(self): + def __repr__(self) -> str: return ( "" % (self.ret, len(self.stdout.lines), len(self.stderr.lines), self.duration) ) - def parseoutcomes(self): + def parseoutcomes(self) -> Dict[str, int]: """Return a dictionary of outcomestring->num from parsing the terminal output that the test process produced. @@ -393,8 +431,14 @@ class RunResult: raise ValueError("Pytest terminal summary report not found") def assert_outcomes( - self, passed=0, skipped=0, failed=0, error=0, xpassed=0, xfailed=0 - ): + self, + passed: int = 0, + skipped: int = 0, + failed: int = 0, + error: int = 0, + xpassed: int = 0, + xfailed: int = 0, + ) -> None: """Assert that the specified outcomes appear with the respective numbers (0 means it didn't occur) in the text output from a test run. @@ -420,19 +464,19 @@ class RunResult: class CwdSnapshot: - def __init__(self): + def __init__(self) -> None: self.__saved = os.getcwd() - def restore(self): + def restore(self) -> None: os.chdir(self.__saved) class SysModulesSnapshot: - def __init__(self, preserve=None): + def __init__(self, preserve: Optional[Callable[[str], bool]] = None): self.__preserve = preserve self.__saved = dict(sys.modules) - def restore(self): + def restore(self) -> None: if self.__preserve: self.__saved.update( (k, m) for k, m in sys.modules.items() if self.__preserve(k) @@ -442,10 +486,10 @@ class SysModulesSnapshot: class SysPathsSnapshot: - def __init__(self): + def __init__(self) -> None: self.__saved = list(sys.path), list(sys.meta_path) - def restore(self): + def restore(self) -> None: sys.path[:], sys.meta_path[:] = self.__saved @@ -1357,7 +1401,7 @@ class LineMatcher: :param str match_nickname: the nickname for the match function that will be logged to stdout when a match occurs """ - assert isinstance(lines2, Sequence) + assert isinstance(lines2, collections.abc.Sequence) lines2 = self._getlines(lines2) lines1 = self.lines[:] nextline = None From 45c4a8fb3d33698704ac17a376de36ada3cabecf Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Nov 2019 14:41:26 +0100 Subject: [PATCH 102/153] Use atomicrewrites only on Windows Fixes https://github.com/pytest-dev/pytest/issues/6147 --- changelog/6148.improvement.rst | 1 + setup.py | 2 +- src/_pytest/assertion/rewrite.py | 69 +++++++++++++++++++++++--------- testing/test_assertrewrite.py | 34 ++++++++++------ 4 files changed, 74 insertions(+), 32 deletions(-) create mode 100644 changelog/6148.improvement.rst diff --git a/changelog/6148.improvement.rst b/changelog/6148.improvement.rst new file mode 100644 index 000000000..c60d2c921 --- /dev/null +++ b/changelog/6148.improvement.rst @@ -0,0 +1 @@ +``python-atomicwrites`` is only used on Windows, fixing a performance regression with assertion rewriting on Unix. diff --git a/setup.py b/setup.py index dcf63f6fd..d7f3d7dbf 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ INSTALL_REQUIRES = [ "packaging", "attrs>=17.4.0", # should match oldattrs tox env. "more-itertools>=4.0.0", - "atomicwrites>=1.0", + 'atomicwrites>=1.0;sys_platform=="win32"', 'pathlib2>=2.2.0;python_version<"3.6"', 'colorama;sys_platform=="win32"', "pluggy>=0.12,<1.0", diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 9c9d6135b..b84929936 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -20,8 +20,6 @@ from typing import Optional from typing import Set from typing import Tuple -import atomicwrites - from _pytest._io.saferepr import saferepr from _pytest._version import version from _pytest.assertion import util @@ -255,26 +253,59 @@ class AssertionRewritingHook(importlib.abc.MetaPathFinder): return f.read() -def _write_pyc(state, co, source_stat, pyc): +def _write_pyc_fp(fp, source_stat, co): # Technically, we don't have to have the same pyc format as # (C)Python, since these "pycs" should never be seen by builtin # import. However, there's little reason deviate. - try: - with atomicwrites.atomic_write(fspath(pyc), mode="wb", overwrite=True) as fp: - fp.write(importlib.util.MAGIC_NUMBER) - # as of now, bytecode header expects 32-bit numbers for size and mtime (#4903) - mtime = int(source_stat.st_mtime) & 0xFFFFFFFF - size = source_stat.st_size & 0xFFFFFFFF - # " Date: Fri, 1 Nov 2019 22:01:37 +0200 Subject: [PATCH 103/153] Add --co option to collect-only Fix #5845 --- changelog/6116.improvement.rst | 1 + src/_pytest/main.py | 1 + testing/test_collection.py | 4 ++-- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 changelog/6116.improvement.rst diff --git a/changelog/6116.improvement.rst b/changelog/6116.improvement.rst new file mode 100644 index 000000000..4fc96ec77 --- /dev/null +++ b/changelog/6116.improvement.rst @@ -0,0 +1 @@ +Add ``--co`` as a synonym to ``--collect-only``. diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 843c5b5f7..b4261c188 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -109,6 +109,7 @@ def pytest_addoption(parser): group.addoption( "--collectonly", "--collect-only", + "--co", action="store_true", help="only collect tests, don't execute them.", ), diff --git a/testing/test_collection.py b/testing/test_collection.py index 259868357..83345d2c6 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -402,7 +402,7 @@ class TestCustomConftests: ) testdir.mkdir("sub") testdir.makepyfile("def test_x(): pass") - result = testdir.runpytest("--collect-only") + result = testdir.runpytest("--co") result.stdout.fnmatch_lines(["*MyModule*", "*test_x*"]) def test_pytest_collect_file_from_sister_dir(self, testdir): @@ -433,7 +433,7 @@ class TestCustomConftests: p = testdir.makepyfile("def test_x(): pass") p.copy(sub1.join(p.basename)) p.copy(sub2.join(p.basename)) - result = testdir.runpytest("--collect-only") + result = testdir.runpytest("--co") result.stdout.fnmatch_lines(["*MyModule1*", "*MyModule2*", "*test_x*"]) From 0cf2002a1fcc7eda7d8ff7ff78bb9528098bebe4 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 6 Nov 2019 15:36:33 -0300 Subject: [PATCH 104/153] Explicitly implement pytest_assertrepr_compare in assertion plugin Previously it was an alias, which makes it unnecessary hard to find all implementations (either by IDE or using a simple search). --- src/_pytest/assertion/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 3b42b356d..34d6701ed 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -163,5 +163,5 @@ def pytest_sessionfinish(session): assertstate.hook.set_session(None) -# Expose this plugin's implementation for the pytest_assertrepr_compare hook -pytest_assertrepr_compare = util.assertrepr_compare +def pytest_assertrepr_compare(config, op, left, right): + return util.assertrepr_compare(config=config, op=op, left=left, right=right) From 40626f48e77b2eeedc6b012c199fe49ff377dd3f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Nov 2019 22:13:03 +0100 Subject: [PATCH 105/153] Update changelog/6148.improvement.rst Co-Authored-By: Bruno Oliveira --- changelog/6148.improvement.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/6148.improvement.rst b/changelog/6148.improvement.rst index c60d2c921..3d77ab528 100644 --- a/changelog/6148.improvement.rst +++ b/changelog/6148.improvement.rst @@ -1 +1 @@ -``python-atomicwrites`` is only used on Windows, fixing a performance regression with assertion rewriting on Unix. +``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix. From cc503c1821e709376d9a05ee4f6459e4e4f41a0c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 8 Nov 2019 04:04:23 +0100 Subject: [PATCH 106/153] _compare_eq_iterable: use AlwaysDispatchingPrettyPrinter This fixes/removes the previous hack of re-trying with minimum width, which fails short when it splits strings. This inherits from `pprint.PrettyPrinter` to override `_format` in a minimal way to always dispatch, regardless of the given width. Code ref: https://github.com/python/cpython/blob/5c0c325453a175350e3c18ebb10cc10c37f9595c/Lib/pprint.py#L170-L178 --- src/_pytest/assertion/util.py | 32 +++++++++++++++++++++++--------- testing/test_assertion.py | 33 +++++++++++++++++++++++++++------ 2 files changed, 50 insertions(+), 15 deletions(-) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 7521c08e4..4af35bd57 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -28,6 +28,27 @@ _reprcompare = None # type: Optional[Callable[[str, object, object], Optional[s _assertion_pass = None # type: Optional[Callable[[int, str, str], None]] +class AlwaysDispatchingPrettyPrinter(pprint.PrettyPrinter): + """PrettyPrinter that always dispatches (regardless of width).""" + + def _format(self, object, stream, indent, allowance, context, level): + p = self._dispatch.get(type(object).__repr__, None) + + objid = id(object) + if objid in context or p is None: + return super()._format(object, stream, indent, allowance, context, level) + + context[objid] = 1 + p(self, object, stream, indent, allowance, context, level + 1) + del context[objid] + + +def _pformat_dispatch(object, indent=1, width=80, depth=None, *, compact=False): + return AlwaysDispatchingPrettyPrinter( + indent=1, width=80, depth=None, compact=False + ).pformat(object) + + def format_explanation(explanation: str) -> str: """This formats an explanation @@ -270,15 +291,8 @@ def _compare_eq_iterable( lines_left = len(left_formatting) lines_right = len(right_formatting) if lines_left != lines_right: - if lines_left > lines_right: - max_width = min(len(x) for x in left_formatting) - else: - max_width = min(len(x) for x in right_formatting) - - right_formatting = pprint.pformat(right, width=max_width).splitlines() - lines_right = len(right_formatting) - left_formatting = pprint.pformat(left, width=max_width).splitlines() - lines_left = len(left_formatting) + left_formatting = _pformat_dispatch(left).splitlines() + right_formatting = _pformat_dispatch(right).splitlines() if lines_left > 1 or lines_right > 1: _surrounding_parens_on_own_lines(left_formatting) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index aac21a0df..6c700567a 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -462,6 +462,29 @@ class TestAssert_reprcompare: " ]", ] + def test_list_dont_wrap_strings(self): + long_a = "a" * 10 + l1 = ["a"] + [long_a for _ in range(0, 7)] + l2 = ["should not get wrapped"] + diff = callequal(l1, l2, verbose=True) + assert diff == [ + "['a', 'aaaaaa...aaaaaaa', ...] == ['should not get wrapped']", + "At index 0 diff: 'a' != 'should not get wrapped'", + "Left contains 7 more items, first extra item: 'aaaaaaaaaa'", + "Full diff:", + " [", + "+ 'should not get wrapped',", + "- 'a',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + "- 'aaaaaaaaaa',", + " ]", + ] + def test_dict_wrap(self): d1 = {"common": 1, "env": {"env1": 1}} d2 = {"common": 1, "env": {"env1": 1, "env2": 2}} @@ -479,22 +502,20 @@ class TestAssert_reprcompare: ] long_a = "a" * 80 - sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped"}} + sub = {"long_a": long_a, "sub1": {"long_a": "substring that gets wrapped " * 2}} d1 = {"env": {"sub": sub}} d2 = {"env": {"sub": sub}, "new": 1} diff = callequal(d1, d2, verbose=True) assert diff == [ - "{'env': {'sub...s wrapped'}}}} == {'env': {'sub...}}}, 'new': 1}", + "{'env': {'sub... wrapped '}}}} == {'env': {'sub...}}}, 'new': 1}", "Omitting 1 identical items, use -vv to show", "Right contains 1 more item:", "{'new': 1}", "Full diff:", " {", " 'env': {'sub': {'long_a': '" + long_a + "',", - " 'sub1': {'long_a': 'substring '", - " 'that '", - " 'gets '", - " 'wrapped'}}},", + " 'sub1': {'long_a': 'substring that gets wrapped substring '", + " 'that gets wrapped '}}},", "+ 'new': 1,", " }", ] From c22ce1a12cc1ad75d50696321a96bd2f1d9a7e86 Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Fri, 8 Nov 2019 00:52:43 -0500 Subject: [PATCH 107/153] parametrize: allow __name__ id for modules or other objects as well --- src/_pytest/python.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index d7fb3d78e..c1654b1c9 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1162,7 +1162,8 @@ def _idval(val, argname, idx, idfn, item, config): return ascii_escaped(val.pattern) elif isinstance(val, enum.Enum): return str(val) - elif (inspect.isclass(val) or inspect.isfunction(val)) and hasattr(val, "__name__"): + elif hasattr(val, "__name__") and isinstance(val.__name__, str): + # name of a class, function, module, etc. return val.__name__ return str(argname) + str(idx) From cc6c5e15b81200bf19c3e398a154d034341b73f6 Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Fri, 8 Nov 2019 01:06:33 -0500 Subject: [PATCH 108/153] update AUTHORS list --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index b0f9d1651..763d904a4 100644 --- a/AUTHORS +++ b/AUTHORS @@ -103,6 +103,7 @@ George Kussumoto Georgy Dyuldin Graham Horler Greg Price +Gregory Lee Grig Gheorghiu Grigorii Eremeev (budulianin) Guido Wesdorp From db82432ec850aed3ee654db131c1cd4879e0cb06 Mon Sep 17 00:00:00 2001 From: Gregory Lee Date: Fri, 8 Nov 2019 01:34:46 -0500 Subject: [PATCH 109/153] add minimal test case --- testing/test_mark.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/testing/test_mark.py b/testing/test_mark.py index 2c12c0451..d7ba6e230 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -314,6 +314,21 @@ def test_keyword_option_parametrize(spec, testdir): assert list(passed) == list(passed_result) +def test_parametrize_with_module(testdir): + testdir.makepyfile( + """ + import pytest + @pytest.mark.parametrize("arg", [pytest,]) + def test_func(arg): + pass + """ + ) + rec = testdir.inline_run() + passed, skipped, fail = rec.listoutcomes() + expected_id = "test_func[" + pytest.__name__ + "]" + assert passed[0].nodeid.split("::")[-1] == expected_id + + @pytest.mark.parametrize( "spec", [ From 984d90a811afc344518e64f8e8c8f300c69b8aaf Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 8 Nov 2019 00:50:32 +0200 Subject: [PATCH 110/153] Drop redundant custom MarkDecorator __eq__ implementation This is already covered by attrs. Also, the custom implementation returns False when the types don't match, but it's better to return `NotImplemented`. attrs does this. --- src/_pytest/mark/structures.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 2cab96d67..924d980f3 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -213,9 +213,6 @@ class MarkDecorator: def markname(self): return self.name # for backward-compat (2.4.1 had this attr) - def __eq__(self, other): - return self.mark == other.mark if isinstance(other, MarkDecorator) else False - def __repr__(self): return "".format(self.mark) From 84b2c81db4b998e41bc0d2583e4acbc47544e103 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 8 Nov 2019 00:55:23 +0200 Subject: [PATCH 111/153] Drop the "alias" helper used in MarkDecorator It is a little too obscure IMO, but the reason I want to drop it is that type checking has no hope of understanding such dynamic constructs. The warning argument wasn't used. --- src/_pytest/mark/structures.py | 28 ++++++++++++++-------------- testing/test_mark.py | 6 ++++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 924d980f3..18ebc506a 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -2,7 +2,6 @@ import inspect import warnings from collections import namedtuple from collections.abc import MutableMapping -from operator import attrgetter from typing import Set import attr @@ -17,16 +16,6 @@ from _pytest.warning_types import PytestUnknownMarkWarning EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark" -def alias(name, warning=None): - getter = attrgetter(name) - - def warned(self): - warnings.warn(warning, stacklevel=2) - return getter(self) - - return property(getter if warning is None else warned, doc="alias for " + name) - - def istestfunc(func): return ( hasattr(func, "__call__") @@ -205,9 +194,20 @@ class MarkDecorator: mark = attr.ib(validator=attr.validators.instance_of(Mark)) - name = alias("mark.name") - args = alias("mark.args") - kwargs = alias("mark.kwargs") + @property + def name(self): + """alias for mark.name""" + return self.mark.name + + @property + def args(self): + """alias for mark.args""" + return self.mark.args + + @property + def kwargs(self): + """alias for mark.kwargs""" + return self.mark.kwargs @property def markname(self): diff --git a/testing/test_mark.py b/testing/test_mark.py index 2c12c0451..071775aef 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -831,6 +831,12 @@ class TestMarkDecorator: def test__eq__(self, lhs, rhs, expected): assert (lhs == rhs) == expected + def test_aliases(self) -> None: + md = pytest.mark.foo(1, "2", three=3) + assert md.name == "foo" + assert md.args == (1, "2") + assert md.kwargs == {"three": 3} + @pytest.mark.parametrize("mark", [None, "", "skip", "xfail"]) def test_parameterset_for_parametrize_marks(testdir, mark): From c16b121594fae69a1fb63dc8dff54332fec2b0a2 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Fri, 8 Nov 2019 10:50:51 -0300 Subject: [PATCH 112/153] Add CHANGELOG for #6152 --- changelog/6152.improvement.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/6152.improvement.rst diff --git a/changelog/6152.improvement.rst b/changelog/6152.improvement.rst new file mode 100644 index 000000000..8e5f4d52a --- /dev/null +++ b/changelog/6152.improvement.rst @@ -0,0 +1 @@ +Now parametrization will use the ``__name__`` attribute of any object for the id, if present. Previously it would only use ``__name__`` for functions and classes. From 28edbaace4b5ebad5875d65eb518c29f495212e5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 7 Nov 2019 13:05:48 +0100 Subject: [PATCH 113/153] showversion: no need for `py.path.local` --- src/_pytest/helpconfig.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 50acc2d7d..21155de2c 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -115,9 +115,10 @@ def pytest_cmdline_parse(): def showversion(config): - p = py.path.local(pytest.__file__) sys.stderr.write( - "This is pytest version {}, imported from {}\n".format(pytest.__version__, p) + "This is pytest version {}, imported from {}\n".format( + pytest.__version__, pytest.__file__ + ) ) plugininfo = getpluginversioninfo(config) if plugininfo: From 0bbc032db0969efa42d35aa74a5a523bd977538d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 9 Nov 2019 00:09:48 +0100 Subject: [PATCH 114/153] [WIP] typing around terminal --- src/_pytest/reports.py | 8 +++---- src/_pytest/terminal.py | 50 ++++++++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 24 deletions(-) diff --git a/src/_pytest/reports.py b/src/_pytest/reports.py index 53f28e73f..5d445c2f8 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -36,7 +36,7 @@ def getslaveinfoline(node): class BaseReport: when = None # type: Optional[str] - location = None + location = None # type: Optional[Tuple[str, Optional[int], str]] longrepr = None sections = [] # type: List[Tuple[str, str]] nodeid = None # type: str @@ -207,7 +207,7 @@ class TestReport(BaseReport): def __init__( self, nodeid, - location, + location: Tuple[str, Optional[int], str], keywords, outcome, longrepr, @@ -216,14 +216,14 @@ class TestReport(BaseReport): duration=0, user_properties=None, **extra - ): + ) -> None: #: normalized collection node id self.nodeid = nodeid #: a (filesystempath, lineno, domaininfo) tuple indicating the #: actual location of a test item - it might be different from the #: collected one e.g. if a method is inherited from a different module. - self.location = location + self.location = location # type: Tuple[str, Optional[int], str] #: a name -> value dictionary containing all keywords and #: markers associated with a test invocation. diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index fc98e30a6..59f0fe0f3 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -9,6 +9,7 @@ import platform import sys import time from functools import partial +from typing import Any from typing import Callable from typing import Dict from typing import List @@ -24,7 +25,11 @@ from more_itertools import collapse import pytest from _pytest import nodes +from _pytest.config import Config from _pytest.main import ExitCode +from _pytest.main import Session +from _pytest.reports import CollectReport +from _pytest.reports import TestReport REPORT_COLLECTING_RESOLUTION = 0.5 @@ -148,7 +153,7 @@ def pytest_addoption(parser): ) -def pytest_configure(config): +def pytest_configure(config: Config) -> None: reporter = TerminalReporter(config, sys.stdout) config.pluginmanager.register(reporter, "terminalreporter") if config.option.debug or config.option.traceconfig: @@ -160,7 +165,7 @@ def pytest_configure(config): config.trace.root.setprocessor("pytest:config", mywriter) -def getreportopt(config): +def getreportopt(config: Config) -> str: reportopts = "" reportchars = config.option.reportchars if not config.option.disable_warnings and "w" not in reportchars: @@ -179,7 +184,7 @@ def getreportopt(config): @pytest.hookimpl(trylast=True) # after _pytest.runner -def pytest_report_teststatus(report): +def pytest_report_teststatus(report: TestReport) -> Tuple[str, str, str]: if report.passed: letter = "." elif report.skipped: @@ -233,15 +238,15 @@ class WarningReport: class TerminalReporter: - def __init__(self, config, file=None): + def __init__(self, config: Config, file=None) -> None: import _pytest.config self.config = config self._numcollected = 0 - self._session = None + self._session = None # type: Optional[Session] self._showfspath = None - self.stats = {} + self.stats = {} # type: Dict[str, List[Any]] self.startdir = config.invocation_dir if file is None: file = sys.stdout @@ -249,13 +254,13 @@ class TerminalReporter: # self.writer will be deprecated in pytest-3.4 self.writer = self._tw self._screen_width = self._tw.fullwidth - self.currentfspath = None + self.currentfspath = None # type: Optional[int] self.reportchars = getreportopt(config) self.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() self._progress_nodeids_reported = set() # type: Set[str] self._show_progress_info = self._determine_show_progress_info() - self._collect_report_last_write = None + self._collect_report_last_write = None # type: Optional[float] def _determine_show_progress_info(self): """Return True if we should display progress information based on the current config""" @@ -400,7 +405,7 @@ class TerminalReporter: fsid = nodeid.split("::")[0] self.write_fspath_result(fsid, "") - def pytest_runtest_logreport(self, report): + def pytest_runtest_logreport(self, report: TestReport) -> None: self._tests_ran = True rep = report res = self.config.hook.pytest_report_teststatus(report=rep, config=self.config) @@ -440,7 +445,7 @@ class TerminalReporter: self._write_progress_information_filling_space() else: self.ensure_newline() - self._tw.write("[%s]" % rep.node.gateway.id) + self._tw.write("[%s]" % rep.node.gateway.id) # type: ignore if self._show_progress_info: self._tw.write( self._get_progress_information_message() + " ", cyan=True @@ -452,6 +457,7 @@ class TerminalReporter: self.currentfspath = -2 def pytest_runtest_logfinish(self, nodeid): + assert self._session if self.verbosity <= 0 and self._show_progress_info: if self._show_progress_info == "count": num_tests = self._session.testscollected @@ -474,7 +480,8 @@ class TerminalReporter: msg = self._get_progress_information_message() self._tw.write(msg + "\n", **{main_color: True}) - def _get_progress_information_message(self): + def _get_progress_information_message(self) -> str: + assert self._session collected = self._session.testscollected if self._show_progress_info == "count": if collected: @@ -485,8 +492,9 @@ class TerminalReporter: return " [ {} / {} ]".format(collected, collected) else: if collected: - progress = len(self._progress_nodeids_reported) * 100 // collected - return " [{:3d}%]".format(progress) + return " [{:3d}%]".format( + len(self._progress_nodeids_reported) * 100 // collected + ) return " [100%]" def _write_progress_information_filling_space(self, color=None): @@ -514,7 +522,7 @@ class TerminalReporter: elif self.config.option.verbose >= 1: self.write("collecting ... ", bold=True) - def pytest_collectreport(self, report): + def pytest_collectreport(self, report: CollectReport) -> None: if report.failed: self.stats.setdefault("error", []).append(report) elif report.skipped: @@ -565,7 +573,7 @@ class TerminalReporter: self.write_line(line) @pytest.hookimpl(trylast=True) - def pytest_sessionstart(self, session): + def pytest_sessionstart(self, session: Session) -> None: self._session = session self._sessionstarttime = time.time() if not self.showheader: @@ -573,9 +581,10 @@ class TerminalReporter: self.write_sep("=", "test session starts", bold=True) verinfo = platform.python_version() msg = "platform {} -- Python {}".format(sys.platform, verinfo) - if hasattr(sys, "pypy_version_info"): - verinfo = ".".join(map(str, sys.pypy_version_info[:3])) - msg += "[pypy-{}-{}]".format(verinfo, sys.pypy_version_info[3]) + pypy_version_info = getattr(sys, "pypy_version_info", None) + if pypy_version_info: + verinfo = ".".join(map(str, pypy_version_info[:3])) + msg += "[pypy-{}-{}]".format(verinfo, pypy_version_info[3]) msg += ", pytest-{}, py-{}, pluggy-{}".format( pytest.__version__, py.__version__, pluggy.__version__ ) @@ -625,9 +634,10 @@ class TerminalReporter: self._write_report_lines_from_hooks(lines) if self.config.getoption("collectonly"): - if self.stats.get("failed"): + failed = self.stats.get("failed") + if failed: self._tw.sep("!", "collection failures") - for rep in self.stats.get("failed"): + for rep in failed: rep.toterminal(self._tw) def _printcollecteditems(self, items): From 42a46ea78617b8e210636bc2f9d9bf06435b60fa Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Wed, 6 Nov 2019 10:24:09 +0200 Subject: [PATCH 115/153] Add a @cached_property implementation This is a useful utility to abstract the caching property idiom. It is in compat.py since eventually it will be replaced by functools.cached_property. Fixes #6131. --- src/_pytest/compat.py | 41 +++++++++++++++++++++++++++++++++++++++++ src/_pytest/nodes.py | 19 ++++++------------- testing/test_compat.py | 21 +++++++++++++++++++++ 3 files changed, 68 insertions(+), 13 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 83947d3eb..5e066c18e 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -10,7 +10,11 @@ import sys from contextlib import contextmanager from inspect import Parameter from inspect import signature +from typing import Callable +from typing import Generic +from typing import Optional from typing import overload +from typing import TypeVar import attr import py @@ -20,6 +24,13 @@ from _pytest._io.saferepr import saferepr from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME +if False: # TYPE_CHECKING + from typing import Type # noqa: F401 (used in type string) + + +_T = TypeVar("_T") +_S = TypeVar("_S") + NOTSET = object() @@ -374,3 +385,33 @@ if getattr(attr, "__version_info__", ()) >= (19, 2): ATTRS_EQ_FIELD = "eq" else: ATTRS_EQ_FIELD = "cmp" + + +if sys.version_info >= (3, 8): + # TODO: Remove type ignore on next mypy update. + # https://github.com/python/typeshed/commit/add0b5e930a1db16560fde45a3b710eefc625709 + from functools import cached_property # type: ignore +else: + + class cached_property(Generic[_S, _T]): + __slots__ = ("func", "__doc__") + + def __init__(self, func: Callable[[_S], _T]) -> None: + self.func = func + self.__doc__ = func.__doc__ + + @overload + def __get__( + self, instance: None, owner: Optional["Type[_S]"] = ... + ) -> "cached_property[_S, _T]": + raise NotImplementedError() + + @overload # noqa: F811 + def __get__(self, instance: _S, owner: Optional["Type[_S]"] = ...) -> _T: + raise NotImplementedError() + + def __get__(self, instance, owner=None): # noqa: F811 + if instance is None: + return self + value = instance.__dict__[self.func.__name__] = self.func(instance) + return value diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index eae783f16..737bc11b7 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -15,6 +15,7 @@ import _pytest._code from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprExceptionInfo +from _pytest.compat import cached_property from _pytest.compat import getfslineno from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError @@ -448,17 +449,9 @@ class Item(Node): def reportinfo(self) -> Tuple[str, Optional[int], str]: return self.fspath, None, "" - @property + @cached_property def location(self) -> Tuple[str, Optional[int], str]: - try: - return self._location - except AttributeError: - location = self.reportinfo() - fspath = self.session._node_location_to_relpath(location[0]) - assert type(location[2]) is str - self._location = ( - fspath, - location[1], - location[2], - ) # type: Tuple[str, Optional[int], str] - return self._location + location = self.reportinfo() + fspath = self.session._node_location_to_relpath(location[0]) + assert type(location[2]) is str + return (fspath, location[1], location[2]) diff --git a/testing/test_compat.py b/testing/test_compat.py index 94dac439d..04d818b4e 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -4,6 +4,7 @@ from functools import wraps import pytest from _pytest.compat import _PytestWrapper +from _pytest.compat import cached_property from _pytest.compat import get_real_func from _pytest.compat import is_generator from _pytest.compat import safe_getattr @@ -178,3 +179,23 @@ def test_safe_isclass(): assert False, "Should be ignored" assert safe_isclass(CrappyClass()) is False + + +def test_cached_property() -> None: + ncalls = 0 + + class Class: + @cached_property + def prop(self) -> int: + nonlocal ncalls + ncalls += 1 + return ncalls + + c1 = Class() + assert ncalls == 0 + assert c1.prop == 1 + assert c1.prop == 1 + c2 = Class() + assert ncalls == 1 + assert c2.prop == 2 + assert c1.prop == 1 From 3ef8aa8173eb10c5a57fd38b4df7d3ba959e8b5f Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 9 Nov 2019 22:43:43 +0100 Subject: [PATCH 116/153] A bit more typing around Node --- src/_pytest/nodes.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/_pytest/nodes.py b/src/_pytest/nodes.py index eae783f16..0028e68dd 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -16,6 +16,7 @@ from _pytest._code.code import ExceptionChainRepr from _pytest._code.code import ExceptionInfo from _pytest._code.code import ReprExceptionInfo from _pytest.compat import getfslineno +from _pytest.config import Config from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureLookupError from _pytest.fixtures import FixtureLookupErrorRepr @@ -78,11 +79,11 @@ class Node: def __init__( self, name, - parent=None, - config=None, + parent: Optional["Node"] = None, + config: Optional[Config] = None, session: Optional["Session"] = None, - fspath=None, - nodeid=None, + fspath: Optional[py.path.local] = None, + nodeid: Optional[str] = None, ) -> None: #: a unique name within the scope of the parent node self.name = name @@ -91,14 +92,20 @@ class Node: self.parent = parent #: the pytest config object - self.config = config or parent.config + if config: + self.config = config + else: + if not parent: + raise TypeError("config or parent must be provided") + self.config = parent.config #: the session this node is part of - if session is None: - assert parent.session is not None - self.session = parent.session - else: + if session: self.session = session + else: + if not parent: + raise TypeError("session or parent must be provided") + self.session = parent.session #: filesystem path where this node was collected from (can be None) self.fspath = fspath or getattr(parent, "fspath", None) @@ -119,6 +126,8 @@ class Node: assert "::()" not in nodeid self._nodeid = nodeid else: + if not self.parent: + raise TypeError("nodeid or parent must be provided") self._nodeid = self.parent.nodeid if self.name != "()": self._nodeid += "::" + self.name @@ -182,7 +191,7 @@ class Node: """ return list of all parent collectors up to self, starting from root of collection tree. """ chain = [] - item = self + item = self # type: Optional[Node] while item is not None: chain.append(item) item = item.parent @@ -263,7 +272,7 @@ class Node: def getparent(self, cls): """ get the next parent node (including ourself) which is an instance of the given class""" - current = self + current = self # type: Optional[Node] while current and not isinstance(current, cls): current = current.parent return current From 4c7d971f13a3912a23398b3eea562178d5d5ec76 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 12 Nov 2019 14:29:50 +0100 Subject: [PATCH 117/153] filterwarnings: ignore DeprecationWarning from nose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This comes via hypothesis: ``` % COLUMNS=80 p testing/python/metafunc.py::TestMetafunc::test_idval_hypothesis -vv --tb=short ============================= test session starts ============================== platform linux -- Python 3.7.4, pytest-3.1.4.dev721+g3367bf03b.d20191112, py-1.8.1.dev11+g34f716fe, pluggy-0.13.1.dev8+ga5130ac.d20191103 -- …/Vcs/pytest/.venv/bin/python cachedir: .pytest_cache hypothesis profile 'default' -> database=DirectoryBasedExampleDatabase('…/Vcs/pytest/.hypothesis/examples') rootdir: …/Vcs/pytest, inifile: tox.ini plugins: forked-1.1.3, hypothesis-4.44.1, cov-2.8.1, coverage-pytest-plugin-0.1, enhancements-0.0.5.dev1-gf361636-dirty, xdist-1.30.0 collected 1 item testing/python/metafunc.py::TestMetafunc::test_idval_hypothesis FAILED [100%] =================================== FAILURES =================================== ______________________ TestMetafunc.test_idval_hypothesis ______________________ .venv/lib/python3.7/site-packages/hypothesis/core.py:588: in evaluate_test_data result = self.execute(data) .venv/lib/python3.7/site-packages/hypothesis/core.py:553: in execute result = self.test_runner(data, run) .venv/lib/python3.7/site-packages/hypothesis/executors.py:56: in default_new_style_executor return function(data) .venv/lib/python3.7/site-packages/hypothesis/core.py:536: in run args, kwargs = data.draw(self.search_strategy) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:857: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/core.py:223: in do_draw return self.mapped_strategy.do_draw(data) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:60: in do_draw return tuple(data.draw(e) for e in self.element_strategies) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:60: in return tuple(data.draw(e) for e in self.element_strategies) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/strategies.py:570: in do_draw result = self.pack(data.draw(self.mapped_strategy)) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/lazy.py:156: in do_draw return data.draw(self.wrapped_strategy) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/strategies.py:570: in do_draw result = self.pack(data.draw(self.mapped_strategy)) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:60: in do_draw return tuple(data.draw(e) for e in self.element_strategies) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:60: in return tuple(data.draw(e) for e in self.element_strategies) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/strategies.py:508: in do_draw return data.draw(self.element_strategies[i], label=self.branch_labels[i]) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/lazy.py:156: in do_draw return data.draw(self.wrapped_strategy) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/strategies.py:570: in do_draw result = self.pack(data.draw(self.mapped_strategy)) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/lazy.py:156: in do_draw return data.draw(self.wrapped_strategy) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/collections.py:120: in do_draw result.append(data.draw(self.element_strategy)) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:852: in draw return strategy.do_draw(self) .venv/lib/python3.7/site-packages/hypothesis/searchstrategy/numbers.py:62: in do_draw return d.integer_range(data, self.start, self.end) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/utils.py:105: in integer_range probe = data.draw_bits(bits) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:974: in draw_bits self.__check_capacity(n_bytes) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:1019: in __check_capacity self.mark_overrun() .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:1036: in mark_overrun self.conclude_test(Status.OVERRUN) .venv/lib/python3.7/site-packages/hypothesis/internal/conjecture/data.py:1027: in conclude_test raise StopTest(self.testcounter) E hypothesis.errors.StopTest: 0 During handling of the above exception, another exception occurred: testing/python/metafunc.py:195: in test_idval_hypothesis @hypothesis.settings( .venv/lib/python3.7/site-packages/nose/__init__.py:1: in from nose.core import collector, main, run, run_exit, runmodule .venv/lib/python3.7/site-packages/nose/core.py:12: in from nose.loader import defaultTestLoader .venv/lib/python3.7/site-packages/nose/loader.py:21: in from nose.importer import Importer, add_path, remove_path .venv/lib/python3.7/site-packages/nose/importer.py:12: in from imp import find_module, load_module, acquire_model1, release_model1 /usr/lib/python3.7/imp.py:33: in DeprecationWarning, stacklevel=2) E DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses ---------------------------------- Hypothesis ---------------------------------- You can add @seed(198901559535749756451579900660745168041) to this test or run pytest with --hypothesis-seed=198901559535749756451579900660745168041 to reproduce this failure. =============================== warnings summary =============================== testing/python/metafunc.py::TestMetafunc::test_idval_hypothesis …/Vcs/pytest/.venv/lib/python3.7/site-packages/unittest2/compatibility.py:143: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working class ChainMap(collections.MutableMapping): -- Docs: https://docs.pytest.org/en/latest/warnings.html =========================== short test summary info ============================ FAILED testing/python/metafunc.py::TestMetafunc::test_idval_hypothesis - Depr... ``` --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index 6bdc5d73f..863a30bb3 100644 --- a/tox.ini +++ b/tox.ini @@ -138,6 +138,7 @@ xfail_strict=true filterwarnings = error default:Using or importing the ABCs:DeprecationWarning:unittest2.* + default:the imp module is deprecated in favour of importlib:DeprecationWarning:nose.* ignore:Module already imported so cannot be rewritten:pytest.PytestWarning # produced by python3.6/site.py itself (3.6.7 on Travis, could not trigger it with 3.6.8). ignore:.*U.*mode is deprecated:DeprecationWarning:(?!(pytest|_pytest)) From fc1c015c6b79ead0d76793cf0b7ae155b365f3a6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 12 Nov 2019 14:42:10 +0100 Subject: [PATCH 118/153] tests: remove test_nested_marks (xfail) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It currently fails with a TypeError, and was not updated since 2013 - therefore it can be assumed that it is not important to support it. ``` ____________________ ERROR collecting test_nested_marks.py _____________________ …/Vcs/pluggy/src/pluggy/hooks.py:286: in __call__ return self._hookexec(self, self.get_hookimpls(), kwargs) …/Vcs/pluggy/src/pluggy/manager.py:93: in _hookexec return self._inner_hookexec(hook, methods, kwargs) …/Vcs/pluggy/src/pluggy/manager.py:337: in traced_hookexec return outcome.get_result() …/Vcs/pluggy/src/pluggy/manager.py:335: in outcome = _Result.from_call(lambda: oldcall(hook, hook_impls, kwargs)) …/Vcs/pluggy/src/pluggy/manager.py:87: in firstresult=hook.spec.opts.get("firstresult") if hook.spec else False, …/Vcs/pytest/src/_pytest/python.py:235: in pytest_pycollect_makeitem res = list(collector._genfunctions(name, obj)) …/Vcs/pytest/src/_pytest/python.py:404: in _genfunctions self.ihook.pytest_generate_tests.call_extra(methods, dict(metafunc=metafunc)) …/Vcs/pluggy/src/pluggy/hooks.py:324: in call_extra return self(**kwargs) …/Vcs/pluggy/src/pluggy/hooks.py:286: in __call__ return self._hookexec(self, self.get_hookimpls(), kwargs) …/Vcs/pluggy/src/pluggy/manager.py:93: in _hookexec return self._inner_hookexec(hook, methods, kwargs) …/Vcs/pluggy/src/pluggy/manager.py:337: in traced_hookexec return outcome.get_result() …/Vcs/pluggy/src/pluggy/manager.py:335: in outcome = _Result.from_call(lambda: oldcall(hook, hook_impls, kwargs)) …/Vcs/pluggy/src/pluggy/manager.py:87: in firstresult=hook.spec.opts.get("firstresult") if hook.spec else False, …/Vcs/pytest/src/_pytest/python.py:130: in pytest_generate_tests metafunc.parametrize(*marker.args, **marker.kwargs) …/Vcs/pytest/src/_pytest/python.py:965: in parametrize function_definition=self.definition, …/Vcs/pytest/src/_pytest/mark/structures.py:111: in _for_parametrize if len(param.values) != len(argnames): E TypeError: object of type 'MarkDecorator' has no len() !!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!! ``` --- testing/python/metafunc.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 5becb0f8c..15c146e90 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1538,27 +1538,6 @@ class TestMarkersWithParametrization: assert len(skipped) == 0 assert len(fail) == 0 - @pytest.mark.xfail(reason="is this important to support??") - def test_nested_marks(self, testdir): - s = """ - import pytest - mastermark = pytest.mark.foo(pytest.mark.bar) - - @pytest.mark.parametrize(("n", "expected"), [ - (1, 2), - mastermark((1, 3)), - (2, 3), - ]) - def test_increment(n, expected): - assert n + 1 == expected - """ - items = testdir.getitems(s) - assert len(items) == 3 - for mark in ["foo", "bar"]: - assert mark not in items[0].keywords - assert mark in items[1].keywords - assert mark not in items[2].keywords - def test_simple_xfail(self, testdir): s = """ import pytest From 86e9ae39f0ff0f16179d3e1d25674445432cfaef Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 12 Nov 2019 15:28:36 +0100 Subject: [PATCH 119/153] pytester: assert_outcomes: use/set __tracebackhide__ --- src/_pytest/pytester.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 6b45e077b..9f3b4d8ab 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -441,8 +441,9 @@ class RunResult: ) -> None: """Assert that the specified outcomes appear with the respective numbers (0 means it didn't occur) in the text output from a test run. - """ + __tracebackhide__ = True + d = self.parseoutcomes() obtained = { "passed": d.get("passed", 0), From 6ddf7c3d42efcf01c8641893e7331f9e33e6877d Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 13 Nov 2019 02:13:35 +0100 Subject: [PATCH 120/153] pytester: Hookrecorder: improve assertoutcome Before: def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: realpassed, realskipped, realfailed = self.listoutcomes() assert passed == len(realpassed) > assert skipped == len(realskipped) E assert 1 == 0 E + where 0 = len([]) After: > reprec = testdir.inline_run(testpath, "-s") E AssertionError: ([], [], []) E assert {'failed': 1, 'passed': 0, 'skipped': 0} == {'failed': 0, 'passed': 0, 'skipped': 1} --- changelog/6176.improvement.rst | 1 + src/_pytest/pytester.py | 15 +++++++++++---- testing/test_assertion.py | 9 ++++++++- 3 files changed, 20 insertions(+), 5 deletions(-) create mode 100644 changelog/6176.improvement.rst diff --git a/changelog/6176.improvement.rst b/changelog/6176.improvement.rst new file mode 100644 index 000000000..39787da2e --- /dev/null +++ b/changelog/6176.improvement.rst @@ -0,0 +1 @@ +Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 9f3b4d8ab..ca780a9f5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -332,10 +332,17 @@ class HookRecorder: return [len(x) for x in self.listoutcomes()] def assertoutcome(self, passed: int = 0, skipped: int = 0, failed: int = 0) -> None: - realpassed, realskipped, realfailed = self.listoutcomes() - assert passed == len(realpassed) - assert skipped == len(realskipped) - assert failed == len(realfailed) + __tracebackhide__ = True + + outcomes = self.listoutcomes() + realpassed, realskipped, realfailed = outcomes + obtained = { + "passed": len(realpassed), + "skipped": len(realskipped), + "failed": len(realfailed), + } + expected = {"passed": passed, "skipped": skipped, "failed": failed} + assert obtained == expected, outcomes def clear(self) -> None: self.calls[:] = [] diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 6c700567a..e4d68ff8c 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -70,7 +70,14 @@ class TestImportHookInstallation: """ ) result = testdir.runpytest_subprocess() - result.stdout.fnmatch_lines(["*assert 1 == 0*"]) + result.stdout.fnmatch_lines( + [ + "E * AssertionError: ([[][]], [[][]], [[][]])*", + "E * assert" + " {'failed': 1, 'passed': 0, 'skipped': 0} ==" + " {'failed': 0, 'passed': 1, 'skipped': 0}", + ] + ) @pytest.mark.parametrize("mode", ["plain", "rewrite"]) def test_pytest_plugins_rewrite(self, testdir, mode): From b06f33f4748ee1bf928c01b4e12d5506a4e05870 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 13 Nov 2019 15:55:11 +0100 Subject: [PATCH 121/153] terminal: report ``session.shouldfail`` reason (``-x``) Via https://github.com/blueyed/pytest/pull/108. --- changelog/6181.improvement.rst | 1 + doc/en/usage.rst | 4 ++-- src/_pytest/terminal.py | 6 +++++- testing/test_collection.py | 13 +++++++++---- testing/test_terminal.py | 26 +++++++++++++++++++++++++- 5 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 changelog/6181.improvement.rst diff --git a/changelog/6181.improvement.rst b/changelog/6181.improvement.rst new file mode 100644 index 000000000..0960f6203 --- /dev/null +++ b/changelog/6181.improvement.rst @@ -0,0 +1 @@ +The reason for a stopped session, e.g. with ``--maxfail`` / ``-x`` gets reported. diff --git a/doc/en/usage.rst b/doc/en/usage.rst index a23cf764a..3b5919363 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -66,8 +66,8 @@ To stop the testing process after the first (N) failures: .. code-block:: bash - pytest -x # stop after first failure - pytest --maxfail=2 # stop after two failures + pytest -x # stop after first failure + pytest --maxfail=2 # stop after two failures .. _select-tests: diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 59f0fe0f3..14267b208 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -676,7 +676,7 @@ class TerminalReporter: self._tw.line("{}{}".format(indent + " ", line.strip())) @pytest.hookimpl(hookwrapper=True) - def pytest_sessionfinish(self, exitstatus): + def pytest_sessionfinish(self, session: Session, exitstatus: ExitCode): outcome = yield outcome.get_result() self._tw.line("") @@ -691,9 +691,13 @@ class TerminalReporter: self.config.hook.pytest_terminal_summary( terminalreporter=self, exitstatus=exitstatus, config=self.config ) + if session.shouldfail: + self.write_sep("!", session.shouldfail, red=True) if exitstatus == ExitCode.INTERRUPTED: self._report_keyboardinterrupt() del self._keyboardinterrupt_memo + elif session.shouldstop: + self.write_sep("!", session.shouldstop, red=True) self.summary_stats() @pytest.hookimpl(hookwrapper=True) diff --git a/testing/test_collection.py b/testing/test_collection.py index 83345d2c6..f18d36d24 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -852,11 +852,15 @@ def test_exit_on_collection_with_maxfail_smaller_than_n_errors(testdir): res = testdir.runpytest("--maxfail=1") assert res.ret == 1 - res.stdout.fnmatch_lines( - ["*ERROR collecting test_02_import_error.py*", "*No module named *asdfa*"] + [ + "collected 1 item / 1 error", + "*ERROR collecting test_02_import_error.py*", + "*No module named *asdfa*", + "*! stopping after 1 failures !*", + "*= 1 error in *", + ] ) - res.stdout.no_fnmatch_line("*test_03*") @@ -869,7 +873,6 @@ def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir): res = testdir.runpytest("--maxfail=4") assert res.ret == 2 - res.stdout.fnmatch_lines( [ "collected 2 items / 2 errors", @@ -877,6 +880,8 @@ def test_exit_on_collection_with_maxfail_bigger_than_n_errors(testdir): "*No module named *asdfa*", "*ERROR collecting test_03_import_error.py*", "*No module named *asdfa*", + "*! Interrupted: 2 errors during collection !*", + "*= 2 errors in *", ] ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 1bec577b8..d31033197 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -963,7 +963,31 @@ class TestGenericReporting: ) result = testdir.runpytest("--maxfail=2", *option.args) result.stdout.fnmatch_lines( - ["*def test_1():*", "*def test_2():*", "*2 failed*"] + [ + "*def test_1():*", + "*def test_2():*", + "*! stopping after 2 failures !*", + "*2 failed*", + ] + ) + + def test_maxfailures_with_interrupted(self, testdir): + testdir.makepyfile( + """ + def test(request): + request.session.shouldstop = "session_interrupted" + assert 0 + """ + ) + result = testdir.runpytest("--maxfail=1", "-ra") + result.stdout.fnmatch_lines( + [ + "*= short test summary info =*", + "FAILED *", + "*! stopping after 1 failures !*", + "*! session_interrupted !*", + "*= 1 failed in*", + ] ) def test_tb_option(self, testdir, option): From b3bb60468331d49ff3eaa3241adb9fa070c90d19 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 23 Oct 2019 20:01:04 +0200 Subject: [PATCH 122/153] fix typo in _issue_warning_captured doc --- src/_pytest/warnings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/warnings.py b/src/_pytest/warnings.py index 8fdb61c2b..8ac1ee225 100644 --- a/src/_pytest/warnings.py +++ b/src/_pytest/warnings.py @@ -138,7 +138,7 @@ def _issue_warning_captured(warning, hook, stacklevel): """ This function should be used instead of calling ``warnings.warn`` directly when we are in the "configure" stage: at this point the actual options might not have been set, so we manually trigger the pytest_warning_captured - hook so we can display this warnings in the terminal. This is a hack until we can sort out #2891. + hook so we can display these warnings in the terminal. This is a hack until we can sort out #2891. :param warning: the warning instance. :param hook: the hook caller From 55bc084dcc783bda88a221a5437a515df3e7b2ae Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 31 Oct 2019 04:39:07 +0100 Subject: [PATCH 123/153] doc: s/_pytest.config.Parser/_pytest.config.argparsing.Parser/ --- src/_pytest/config/__init__.py | 2 +- src/_pytest/config/argparsing.py | 2 +- src/_pytest/hookspec.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 8e11b56e5..e94bec5d6 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -974,7 +974,7 @@ class Config: def getini(self, name: str): """ return configuration value from an :ref:`ini file `. If the specified name hasn't been registered through a prior - :py:func:`parser.addini <_pytest.config.Parser.addini>` + :py:func:`parser.addini <_pytest.config.argparsing.Parser.addini>` call (usually from a plugin), a ValueError is raised. """ try: return self._inicache[name] diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 9b526ff3e..4eec6be05 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -47,7 +47,7 @@ class Parser: The returned group object has an ``addoption`` method with the same signature as :py:func:`parser.addoption - <_pytest.config.Parser.addoption>` but will be shown in the + <_pytest.config.argparsing.Parser.addoption>` but will be shown in the respective group in the output of ``pytest. --help``. """ for group in self._groups: diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 8b45c5f9b..03e060eb8 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -45,10 +45,10 @@ def pytest_addoption(parser, pluginmanager): files situated at the tests root directory due to how pytest :ref:`discovers plugins during startup `. - :arg _pytest.config.Parser parser: To add command line options, call - :py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`. + :arg _pytest.config.argparsing.Parser parser: To add command line options, call + :py:func:`parser.addoption(...) <_pytest.config.argparsing.Parser.addoption>`. To add ini-file values call :py:func:`parser.addini(...) - <_pytest.config.Parser.addini>`. + <_pytest.config.argparsing.Parser.addini>`. :arg _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager, which can be used to install :py:func:`hookspec`'s or :py:func:`hookimpl`'s @@ -148,7 +148,7 @@ def pytest_load_initial_conftests(early_config, parser, args): :param _pytest.config.Config early_config: pytest config object :param list[str] args: list of arguments passed on the command line - :param _pytest.config.Parser parser: to add command line options + :param _pytest.config.argparsing.Parser parser: to add command line options """ From 772dfc4f9d610ebd735be48fd1c0924384c94e04 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 13 Nov 2019 23:24:17 +0100 Subject: [PATCH 124/153] terminal: fix/remove wrong typing for currentfspath Can be -2, or py.path.local (not typed). --- src/_pytest/terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 59f0fe0f3..a84733d45 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -254,7 +254,7 @@ class TerminalReporter: # self.writer will be deprecated in pytest-3.4 self.writer = self._tw self._screen_width = self._tw.fullwidth - self.currentfspath = None # type: Optional[int] + self.currentfspath = None # type: Any self.reportchars = getreportopt(config) self.hasmarkup = self._tw.hasmarkup self.isatty = file.isatty() From 2a67637accc9b5f25b4d3fda3b99ad37cfcab18b Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 13 Nov 2019 18:20:44 -0300 Subject: [PATCH 125/153] Issue a warning to prepare change of 'junit_family' default value Fix #6179 --- changelog/6179.deprecation.rst | 7 +++++++ doc/en/deprecations.rst | 21 +++++++++++++++++++++ src/_pytest/deprecated.py | 5 +++++ src/_pytest/junitxml.py | 12 ++++++++---- testing/deprecated_test.py | 29 +++++++++++++++++++++++++++++ 5 files changed, 70 insertions(+), 4 deletions(-) create mode 100644 changelog/6179.deprecation.rst diff --git a/changelog/6179.deprecation.rst b/changelog/6179.deprecation.rst new file mode 100644 index 000000000..97f7ec74b --- /dev/null +++ b/changelog/6179.deprecation.rst @@ -0,0 +1,7 @@ +The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given +that this is the version supported by default in modern tools that manipulate this type of file. + +In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option +is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``. + +For more information, `see the docs `__. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 5cf3b0903..34a05e1e6 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -19,6 +19,27 @@ Below is a complete list of all pytest features which are considered deprecated. :class:`_pytest.warning_types.PytestWarning` or subclasses, which can be filtered using :ref:`standard warning filters `. +``junit_family`` default value change to "xunit2" +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 5.2 + +The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given +that this is the version supported by default in modern tools that manipulate this type of file. + +In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option +is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``:: + + PytestDeprecationWarning: The 'junit_family' default value will change to 'xunit2' in pytest 6.0. + Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible. + +In order to silence this warning, users just need to configure the ``junit_family`` option explicitly: + +.. code-block:: ini + + [pytest] + junit_family=legacy + ``funcargnames`` alias for ``fixturenames`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 5186067ef..442f102d1 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -34,3 +34,8 @@ FIXTURE_POSITIONAL_ARGUMENTS = PytestDeprecationWarning( "Passing arguments to pytest.fixture() as positional arguments is deprecated - pass them " "as a keyword argument instead." ) + +JUNIT_XML_DEFAULT_FAMILY = PytestDeprecationWarning( + "The 'junit_family' default value will change to 'xunit2' in pytest 6.0.\n" + "Add 'junit_family=legacy' to your pytest.ini file to silence this warning and make your suite compatible." +) diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index fb951106f..9cf22705e 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -19,8 +19,10 @@ from datetime import datetime import py import pytest +from _pytest import deprecated from _pytest import nodes from _pytest.config import filename_arg +from _pytest.warnings import _issue_warning_captured class Junit(py.xml.Namespace): @@ -421,9 +423,7 @@ def pytest_addoption(parser): default="total", ) # choices=['total', 'call']) parser.addini( - "junit_family", - "Emit XML for schema: one of legacy|xunit1|xunit2", - default="xunit1", + "junit_family", "Emit XML for schema: one of legacy|xunit1|xunit2", default=None ) @@ -431,13 +431,17 @@ def pytest_configure(config): xmlpath = config.option.xmlpath # prevent opening xmllog on slave nodes (xdist) if xmlpath and not hasattr(config, "slaveinput"): + junit_family = config.getini("junit_family") + if not junit_family: + _issue_warning_captured(deprecated.JUNIT_XML_DEFAULT_FAMILY, config.hook, 2) + junit_family = "xunit1" config._xml = LogXML( xmlpath, config.option.junitprefix, config.getini("junit_suite_name"), config.getini("junit_logging"), config.getini("junit_duration_report"), - config.getini("junit_family"), + junit_family, config.getini("junit_log_passing_tests"), ) config.pluginmanager.register(config._xml) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index b8a22428f..64ec11b7a 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -44,3 +44,32 @@ def test_external_plugins_integrated(testdir, plugin): with pytest.warns(pytest.PytestConfigWarning): testdir.parseconfig("-p", plugin) + + +@pytest.mark.parametrize("junit_family", [None, "legacy", "xunit2"]) +def test_warn_about_imminent_junit_family_default_change(testdir, junit_family): + """Show a warning if junit_family is not defined and --junitxml is used (#6179)""" + testdir.makepyfile( + """ + def test_foo(): + pass + """ + ) + if junit_family: + testdir.makeini( + """ + [pytest] + junit_family={junit_family} + """.format( + junit_family=junit_family + ) + ) + + result = testdir.runpytest("--junit-xml=foo.xml") + warning_msg = ( + "*PytestDeprecationWarning: The 'junit_family' default value will change*" + ) + if junit_family: + result.stdout.no_fnmatch_line(warning_msg) + else: + result.stdout.fnmatch_lines([warning_msg]) From d2ea9e2db58dfb6c5a3c2981bfd062369e51fbf3 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Thu, 14 Nov 2019 18:26:49 -0300 Subject: [PATCH 126/153] Remove report_log in favor of pytest-reportlog Fix #6180 --- changelog/4488.feature.rst | 7 ++-- doc/en/contents.rst | 1 - doc/en/deprecations.rst | 7 ++-- doc/en/report_log.rst | 70 --------------------------------- doc/en/usage.rst | 2 +- src/_pytest/config/__init__.py | 1 - src/_pytest/deprecated.py | 2 +- src/_pytest/report_log.py | 72 ---------------------------------- testing/deprecated_test.py | 2 +- testing/test_report_log.py | 54 ------------------------- 10 files changed, 11 insertions(+), 207 deletions(-) delete mode 100644 doc/en/report_log.rst delete mode 100644 src/_pytest/report_log.py delete mode 100644 testing/test_report_log.py diff --git a/changelog/4488.feature.rst b/changelog/4488.feature.rst index ddbca65d6..1e0387f44 100644 --- a/changelog/4488.feature.rst +++ b/changelog/4488.feature.rst @@ -1,9 +1,10 @@ -New ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. +The pytest team has created the `pytest-reportlog `__ +plugin, which provides a new ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. Each line of the report log contains a self contained JSON object corresponding to a testing event, such as a collection or a test result report. The file is guaranteed to be flushed after writing each line, so systems can read and process events in real-time. -This option is meant to replace ``--resultlog``, which is deprecated and meant to be removed -in a future release. If you use ``--resultlog``, please try out ``--report-log`` and +The plugin is meant to replace the ``--resultlog`` option, which is deprecated and meant to be removed +in a future release. If you use ``--resultlog``, please try out ``pytest-reportlog`` and provide feedback. diff --git a/doc/en/contents.rst b/doc/en/contents.rst index 5d7599f50..c623d0602 100644 --- a/doc/en/contents.rst +++ b/doc/en/contents.rst @@ -27,7 +27,6 @@ Full pytest documentation unittest nose xunit_setup - report_log plugins writing_plugins logging diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 34a05e1e6..748d3ac65 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -64,11 +64,12 @@ The ``--result-log`` option produces a stream of test reports which can be analysed at runtime, but it uses a custom format which requires users to implement their own parser. -The :ref:`--report-log ` option provides a more standard and extensible alternative, producing +The `pytest-reportlog `__ plugin provides a ``--report-log`` option, a more standard and extensible alternative, producing one JSON object per-line, and should cover the same use cases. Please try it out and provide feedback. -The plan is remove the ``--result-log`` option in pytest 6.0 after ``--result-log`` proves satisfactory -to all users and is deemed stable. +The plan is remove the ``--result-log`` option in pytest 6.0 if ``pytest-reportlog`` proves satisfactory +to all users and is deemed stable. The ``pytest-reportlog`` plugin might even be merged into the core +at some point, depending on the plans for the plugins and number of users using it. Removed Features diff --git a/doc/en/report_log.rst b/doc/en/report_log.rst deleted file mode 100644 index 619925180..000000000 --- a/doc/en/report_log.rst +++ /dev/null @@ -1,70 +0,0 @@ -.. _report_log: - -Report files -============ - -.. versionadded:: 5.3 - -The ``--report-log=FILE`` option writes *report logs* into a file as the test session executes. - -Each line of the report log contains a self contained JSON object corresponding to a testing event, -such as a collection or a test result report. The file is guaranteed to be flushed after writing -each line, so systems can read and process events in real-time. - -Each JSON object contains a special key ``$report_type``, which contains a unique identifier for -that kind of report object. For future compatibility, consumers of the file should ignore reports -they don't recognize, as well as ignore unknown properties/keys in JSON objects that they do know, -as future pytest versions might enrich the objects with more properties/keys. - -.. note:: - This option is meant to the replace ``--resultlog``, which is deprecated and meant to be removed - in a future release. If you use ``--resultlog``, please try out ``--report-log`` and - provide feedback. - -Example -------- - -Consider this file: - -.. code-block:: python - - # content of test_report_example.py - - - def test_ok(): - assert 5 + 5 == 10 - - - def test_fail(): - assert 4 + 4 == 1 - - -.. code-block:: pytest - - $ pytest test_report_example.py -q --report-log=log.json - .F [100%] - ================================= FAILURES ================================= - ________________________________ test_fail _________________________________ - - def test_fail(): - > assert 4 + 4 == 1 - E assert (4 + 4) == 1 - - test_report_example.py:8: AssertionError - ------------------- generated report log file: log.json -------------------- - 1 failed, 1 passed in 0.12s - -The generated ``log.json`` will contain a JSON object per line: - -:: - - $ cat log.json - {"pytest_version": "5.2.3.dev90+gd1129cf96.d20191026", "$report_type": "Header"} - {"nodeid": "", "outcome": "passed", "longrepr": null, "result": null, "sections": [], "$report_type": "CollectReport"} - {"nodeid": "test_report_example.py", "outcome": "passed", "longrepr": null, "result": null, "sections": [], "$report_type": "CollectReport"} - {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "setup", "user_properties": [], "sections": [], "duration": 0.00021314620971679688, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "call", "user_properties": [], "sections": [], "duration": 0.00014543533325195312, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_ok", "location": ["test_report_example.py", 2, "test_ok"], "keywords": {"report_log.rst-39": 1, "test_report_example.py": 1, "test_ok": 1}, "outcome": "passed", "longrepr": null, "when": "teardown", "user_properties": [], "sections": [], "duration": 0.00016427040100097656, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "passed", "longrepr": null, "when": "setup", "user_properties": [], "sections": [], "duration": 0.00013589859008789062, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "failed", "longrepr": {"reprcrash": {"path": "$REGENDOC_TMPDIR/test_report_example.py", "lineno": 8, "message": "assert (4 + 4) == 1"}, "reprtraceback": {"reprentries": [{"type": "ReprEntry", "data": {"lines": [" def test_fail():", "> assert 4 + 4 == 1", "E assert (4 + 4) == 1"], "reprfuncargs": {"args": []}, "reprlocals": null, "reprfileloc": {"path": "test_report_example.py", "lineno": 8, "message": "AssertionError"}, "style": "long"}}], "extraline": null, "style": "long"}, "sections": [], "chain": [[{"reprentries": [{"type": "ReprEntry", "data": {"lines": [" def test_fail():", "> assert 4 + 4 == 1", "E assert (4 + 4) == 1"], "reprfuncargs": {"args": []}, "reprlocals": null, "reprfileloc": {"path": "test_report_example.py", "lineno": 8, "message": "AssertionError"}, "style": "long"}}], "extraline": null, "style": "long"}, {"path": "$REGENDOC_TMPDIR/test_report_example.py", "lineno": 8, "message": "assert (4 + 4) == 1"}, null]]}, "when": "call", "user_properties": [], "sections": [], "duration": 0.00027489662170410156, "$report_type": "TestReport"} - {"nodeid": "test_report_example.py::test_fail", "location": ["test_report_example.py", 6, "test_fail"], "keywords": {"test_fail": 1, "test_report_example.py": 1, "report_log.rst-39": 1}, "outcome": "passed", "longrepr": null, "when": "teardown", "user_properties": [], "sections": [], "duration": 0.00016689300537109375, "$report_type": "TestReport"} diff --git a/doc/en/usage.rst b/doc/en/usage.rst index 3b5919363..ea849c1a7 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -692,7 +692,7 @@ by the `PyPy-test`_ web page to show test results over several revisions. This option is rarely used and is scheduled for removal in pytest 6.0. - If you use this option, consider using the new :ref:`--result-log `. + If you use this option, consider using the new `pytest-reportlog `__ plugin instead. See `the deprecation docs `__ for more information. diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e94bec5d6..c5bf32bbf 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -154,7 +154,6 @@ default_plugins = essential_plugins + ( "assertion", "junitxml", "resultlog", - "report_log", "doctest", "cacheprovider", "freeze_support", diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 442f102d1..5a7066041 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -26,7 +26,7 @@ FUNCARGNAMES = PytestDeprecationWarning( RESULT_LOG = PytestDeprecationWarning( - "--result-log is deprecated and scheduled for removal in pytest 6.0.\n" + "--result-log is deprecated, please try the new pytest-reportlog plugin.\n" "See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information." ) diff --git a/src/_pytest/report_log.py b/src/_pytest/report_log.py deleted file mode 100644 index b12d0a55d..000000000 --- a/src/_pytest/report_log.py +++ /dev/null @@ -1,72 +0,0 @@ -import json -from pathlib import Path - -import pytest - - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting", "report-log plugin options") - group.addoption( - "--report-log", - action="store", - metavar="path", - default=None, - help="Path to line-based json objects of test session events.", - ) - - -def pytest_configure(config): - report_log = config.option.report_log - if report_log and not hasattr(config, "slaveinput"): - config._report_log_plugin = ReportLogPlugin(config, Path(report_log)) - config.pluginmanager.register(config._report_log_plugin) - - -def pytest_unconfigure(config): - report_log_plugin = getattr(config, "_report_log_plugin", None) - if report_log_plugin: - report_log_plugin.close() - del config._report_log_plugin - - -class ReportLogPlugin: - def __init__(self, config, log_path: Path): - self._config = config - self._log_path = log_path - - log_path.parent.mkdir(parents=True, exist_ok=True) - self._file = log_path.open("w", buffering=1, encoding="UTF-8") - - def close(self): - if self._file is not None: - self._file.close() - self._file = None - - def _write_json_data(self, data): - self._file.write(json.dumps(data) + "\n") - self._file.flush() - - def pytest_sessionstart(self): - data = {"pytest_version": pytest.__version__, "$report_type": "SessionStart"} - self._write_json_data(data) - - def pytest_sessionfinish(self, exitstatus): - data = {"exitstatus": exitstatus, "$report_type": "SessionFinish"} - self._write_json_data(data) - - def pytest_runtest_logreport(self, report): - data = self._config.hook.pytest_report_to_serializable( - config=self._config, report=report - ) - self._write_json_data(data) - - def pytest_collectreport(self, report): - data = self._config.hook.pytest_report_to_serializable( - config=self._config, report=report - ) - self._write_json_data(data) - - def pytest_terminal_summary(self, terminalreporter): - terminalreporter.write_sep( - "-", "generated report log file: {}".format(self._log_path) - ) diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 64ec11b7a..5390d038d 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -16,7 +16,7 @@ def test_resultlog_is_deprecated(testdir): result = testdir.runpytest("--result-log=%s" % testdir.tmpdir.join("result.log")) result.stdout.fnmatch_lines( [ - "*--result-log is deprecated and scheduled for removal in pytest 6.0*", + "*--result-log is deprecated, please try the new pytest-reportlog plugin.", "*See https://docs.pytest.org/en/latest/deprecations.html#result-log-result-log for more information*", ] ) diff --git a/testing/test_report_log.py b/testing/test_report_log.py deleted file mode 100644 index cc2a431ec..000000000 --- a/testing/test_report_log.py +++ /dev/null @@ -1,54 +0,0 @@ -import json - -import pytest -from _pytest.reports import BaseReport - - -def test_basics(testdir, tmp_path, pytestconfig): - """Basic testing of the report log functionality. - - We don't test the test reports extensively because they have been - tested already in ``test_reports``. - """ - testdir.makepyfile( - """ - def test_ok(): - pass - - def test_fail(): - assert 0 - """ - ) - - log_file = tmp_path / "log.json" - - result = testdir.runpytest("--report-log", str(log_file)) - assert result.ret == pytest.ExitCode.TESTS_FAILED - result.stdout.fnmatch_lines(["* generated report log file: {}*".format(log_file)]) - - json_objs = [json.loads(x) for x in log_file.read_text().splitlines()] - assert len(json_objs) == 10 - - # first line should be the session_start - session_start = json_objs[0] - assert session_start == { - "pytest_version": pytest.__version__, - "$report_type": "SessionStart", - } - - # last line should be the session_finish - session_start = json_objs[-1] - assert session_start == { - "exitstatus": pytest.ExitCode.TESTS_FAILED, - "$report_type": "SessionFinish", - } - - # rest of the json objects should be unserialized into report objects; we don't test - # the actual report object extensively because it has been tested in ``test_reports`` - # already. - pm = pytestconfig.pluginmanager - for json_obj in json_objs[1:-1]: - rep = pm.hook.pytest_report_from_serializable( - config=pytestconfig, data=json_obj - ) - assert isinstance(rep, BaseReport) From 5979837c6084e80367ce4f7e1b97aabd755221b0 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Nov 2019 03:53:05 +0100 Subject: [PATCH 127/153] Import Path from _pytest.pathlib for py35 This is important for `isinstance` checks etc. --- src/_pytest/assertion/rewrite.py | 2 +- src/_pytest/config/__init__.py | 2 +- testing/test_assertrewrite.py | 2 +- testing/test_config.py | 2 +- testing/test_conftest.py | 2 +- testing/test_junitxml.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index b84929936..af4d00194 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -13,7 +13,6 @@ import struct import sys import tokenize import types -from pathlib import Path from typing import Dict from typing import List from typing import Optional @@ -28,6 +27,7 @@ from _pytest.assertion.util import ( # noqa: F401 ) from _pytest.compat import fspath from _pytest.pathlib import fnmatch_ex +from _pytest.pathlib import Path from _pytest.pathlib import PurePath # pytest caches rewritten pycs in pycache dirs diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index e94bec5d6..06296fd35 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -8,7 +8,6 @@ import sys import types import warnings from functools import lru_cache -from pathlib import Path from types import TracebackType from typing import Any from typing import Callable @@ -40,6 +39,7 @@ from _pytest._code import filter_traceback from _pytest.compat import importlib_metadata from _pytest.outcomes import fail from _pytest.outcomes import Skipped +from _pytest.pathlib import Path from _pytest.warning_types import PytestConfigWarning if False: # TYPE_CHECKING diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index e2d6b89c8..8490a59e6 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -9,7 +9,6 @@ import sys import textwrap import zipfile from functools import partial -from pathlib import Path import py @@ -23,6 +22,7 @@ from _pytest.assertion.rewrite import PYC_TAIL from _pytest.assertion.rewrite import PYTEST_TAG from _pytest.assertion.rewrite import rewrite_asserts from _pytest.main import ExitCode +from _pytest.pathlib import Path def setup_module(mod): diff --git a/testing/test_config.py b/testing/test_config.py index d4d624348..f146b52a4 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,7 +1,6 @@ import os import sys import textwrap -from pathlib import Path import _pytest._code import pytest @@ -13,6 +12,7 @@ from _pytest.config.findpaths import determine_setup from _pytest.config.findpaths import get_common_ancestor from _pytest.config.findpaths import getcfg from _pytest.main import ExitCode +from _pytest.pathlib import Path class TestParseIni: diff --git a/testing/test_conftest.py b/testing/test_conftest.py index 0374db0b3..2918ff04c 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,12 +1,12 @@ import os import textwrap -from pathlib import Path import py import pytest from _pytest.config import PytestPluginManager from _pytest.main import ExitCode +from _pytest.pathlib import Path def ConftestWithSetinitial(path): diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 885d25941..4c2f22a3d 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1,7 +1,6 @@ import os import platform from datetime import datetime -from pathlib import Path from xml.dom import minidom import py @@ -9,6 +8,7 @@ import xmlschema import pytest from _pytest.junitxml import LogXML +from _pytest.pathlib import Path from _pytest.reports import BaseReport From 1abb08d52f165e46bb3cc80a9ece860aa4afe5b5 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Nov 2019 23:12:25 +0100 Subject: [PATCH 128/153] tests: use sys.dont_write_bytecode Setting PYTHONDONTWRITEBYTECODE in the environment does not change it for the current process. --- testing/test_cacheprovider.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index 3f03b5ff9..ea78358d6 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -253,7 +253,7 @@ def test_cache_show(testdir): class TestLastFailed: def test_lastfailed_usecase(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") + monkeypatch.setattr("sys.dont_write_bytecode", True) p = testdir.makepyfile( """ def test_1(): @@ -345,7 +345,7 @@ class TestLastFailed: result.stdout.no_fnmatch_line("*test_a.py*") def test_lastfailed_difference_invocations(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") + monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( test_a="""\ def test_a1(): @@ -379,7 +379,7 @@ class TestLastFailed: result.stdout.fnmatch_lines(["*1 failed*1 desel*"]) def test_lastfailed_usecase_splice(self, testdir, monkeypatch): - monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") + monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( """\ def test_1(): From f760356578836fd888a1cd825460d8daa8808ea6 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 15:16:40 +0200 Subject: [PATCH 129/153] A few linting fixes Add some Python 3.8 type: ignores; all are already fixed in the next mypy release, so can be removed once we upgrade. Also move some flake8 ignores which seem to have changed places. --- src/_pytest/_code/source.py | 2 +- src/_pytest/assertion/rewrite.py | 5 +++-- src/_pytest/compat.py | 7 +++++-- src/_pytest/python_api.py | 2 +- src/_pytest/recwarn.py | 6 +++--- 5 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index 1e9dd5031..a26a70e68 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -60,7 +60,7 @@ class Source: raise NotImplementedError() @overload # noqa: F811 - def __getitem__(self, key: slice) -> "Source": + def __getitem__(self, key: slice) -> "Source": # noqa: F811 raise NotImplementedError() def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: F811 diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index af4d00194..2f9ca6de0 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1074,13 +1074,14 @@ def try_makedirs(cache_dir) -> bool: def get_cache_dir(file_path: Path) -> Path: """Returns the cache directory to write .pyc files for the given .py file path""" - if sys.version_info >= (3, 8) and sys.pycache_prefix: + # Type ignored until added in next mypy release. + if sys.version_info >= (3, 8) and sys.pycache_prefix: # type: ignore # given: # prefix = '/tmp/pycs' # path = '/home/user/proj/test_app.py' # we want: # '/tmp/pycs/home/user/proj' - return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) + return Path(sys.pycache_prefix) / Path(*file_path.parts[1:-1]) # type: ignore else: # classic pycache directory return file_path.parent / "__pycache__" diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 5e066c18e..c115ae98d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -40,7 +40,8 @@ MODULE_NOT_FOUND_ERROR = ( if sys.version_info >= (3, 8): - from importlib import metadata as importlib_metadata # noqa: F401 + # Type ignored until next mypy release. + from importlib import metadata as importlib_metadata # type: ignore else: import importlib_metadata # noqa: F401 @@ -407,7 +408,9 @@ else: raise NotImplementedError() @overload # noqa: F811 - def __get__(self, instance: _S, owner: Optional["Type[_S]"] = ...) -> _T: + def __get__( # noqa: F811 + self, instance: _S, owner: Optional["Type[_S]"] = ... + ) -> _T: raise NotImplementedError() def __get__(self, instance, owner=None): # noqa: F811 diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 52a91a905..9f206ce9b 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -552,7 +552,7 @@ def raises( @overload # noqa: F811 -def raises( +def raises( # noqa: F811 expected_exception: Union["Type[_E]", Tuple["Type[_E]", ...]], func: Callable, *args: Any, diff --git a/src/_pytest/recwarn.py b/src/_pytest/recwarn.py index 4967106d9..5cf32c894 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -60,18 +60,18 @@ def warns( *, match: "Optional[Union[str, Pattern]]" = ... ) -> "WarningsChecker": - ... # pragma: no cover + raise NotImplementedError() @overload # noqa: F811 -def warns( +def warns( # noqa: F811 expected_warning: Union["Type[Warning]", Tuple["Type[Warning]", ...]], func: Callable, *args: Any, match: Optional[Union[str, "Pattern"]] = ..., **kwargs: Any ) -> Union[Any]: - ... # pragma: no cover + raise NotImplementedError() def warns( # noqa: F811 From c7a83a0f316c8bebd42df636b8d5cccfe948a72c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 26 Oct 2019 15:15:16 +0300 Subject: [PATCH 130/153] Remove a PyPy version check for an unsupported version pytest doesn't support these PyPy versions anymore, so no need to have checks for them. --- testing/test_capture.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/testing/test_capture.py b/testing/test_capture.py index 85b0b05ae..94af3aef7 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -92,8 +92,6 @@ class TestCaptureManager: @pytest.mark.parametrize("method", ["fd", "sys"]) def test_capturing_unicode(testdir, method): - if hasattr(sys, "pypy_version_info") and sys.pypy_version_info < (2, 2): - pytest.xfail("does not work on pypy < 2.2") obj = "'b\u00f6y'" testdir.makepyfile( """\ From 04d68fbc9e53ff01962ed5da8686ced276425c95 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 15:51:44 +0200 Subject: [PATCH 131/153] Remove checks for Python2-only fields im_func and func_code --- src/_pytest/_code/code.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 3c2acfe7f..19d5efaa6 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -1054,8 +1054,6 @@ def getrawcode(obj, trycall=True): try: return obj.__code__ except AttributeError: - obj = getattr(obj, "im_func", obj) - obj = getattr(obj, "func_code", obj) obj = getattr(obj, "f_code", obj) obj = getattr(obj, "__code__", obj) if trycall and not hasattr(obj, "co_firstlineno"): From 5bfe793fd5455c04049c4a564d1a6dc666cc647a Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 15:54:56 +0200 Subject: [PATCH 132/153] Remove unneeded getrawcode() calls from tests --- testing/code/test_excinfo.py | 6 +++--- testing/code/test_source.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index 199b8716f..b83ad93e2 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -59,9 +59,9 @@ def test_excinfo_getstatement(): except ValueError: excinfo = _pytest._code.ExceptionInfo.from_current() linenumbers = [ - _pytest._code.getrawcode(f).co_firstlineno - 1 + 4, - _pytest._code.getrawcode(f).co_firstlineno - 1 + 1, - _pytest._code.getrawcode(g).co_firstlineno - 1 + 1, + f.__code__.co_firstlineno - 1 + 4, + f.__code__.co_firstlineno - 1 + 1, + g.__code__.co_firstlineno - 1 + 1, ] values = list(excinfo.traceback) foundlinenumbers = [x.lineno for x in values] diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 5e7e1abf5..519344dd4 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -478,7 +478,7 @@ def test_getfslineno(): fspath, lineno = getfslineno(f) assert fspath.basename == "test_source.py" - assert lineno == _pytest._code.getrawcode(f).co_firstlineno - 1 # see findsource + assert lineno == f.__code__.co_firstlineno - 1 # see findsource class A: pass From e3ac44df360849809d8d156bd24827025238072c Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 16:14:04 +0200 Subject: [PATCH 133/153] Inline the FuncargnamesCompatAttr compat helper It doesn't help much IMO, just adds indirection and makes it harder to type. --- src/_pytest/compat.py | 15 --------------- src/_pytest/fixtures.py | 10 ++++++++-- src/_pytest/python.py | 17 +++++++++++++++-- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index c115ae98d..09e621c5d 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -361,21 +361,6 @@ class CaptureIO(io.TextIOWrapper): return self.buffer.getvalue().decode("UTF-8") -class FuncargnamesCompatAttr: - """ helper class so that Metafunc, Function and FixtureRequest - don't need to each define the "funcargnames" compatibility attribute. - """ - - @property - def funcargnames(self): - """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" - import warnings - from _pytest.deprecated import FUNCARGNAMES - - warnings.warn(FUNCARGNAMES, stacklevel=2) - return self.fixturenames - - if sys.version_info < (3, 5, 2): # pragma: no cover def overload(f): # noqa: F811 diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index fc55ef2cf..34ecf2e21 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -18,7 +18,6 @@ from _pytest._code.code import FormattedExcinfo from _pytest._code.code import TerminalRepr from _pytest.compat import _format_args from _pytest.compat import _PytestWrapper -from _pytest.compat import FuncargnamesCompatAttr from _pytest.compat import get_real_func from _pytest.compat import get_real_method from _pytest.compat import getfslineno @@ -29,6 +28,7 @@ from _pytest.compat import is_generator from _pytest.compat import NOTSET from _pytest.compat import safe_getattr from _pytest.deprecated import FIXTURE_POSITIONAL_ARGUMENTS +from _pytest.deprecated import FUNCARGNAMES from _pytest.outcomes import fail from _pytest.outcomes import TEST_OUTCOME @@ -336,7 +336,7 @@ class FuncFixtureInfo: self.names_closure[:] = sorted(closure, key=self.names_closure.index) -class FixtureRequest(FuncargnamesCompatAttr): +class FixtureRequest: """ A request for a fixture from a test or fixture function. A request object gives access to the requesting test context @@ -363,6 +363,12 @@ class FixtureRequest(FuncargnamesCompatAttr): result.extend(set(self._fixture_defs).difference(result)) return result + @property + def funcargnames(self): + """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + warnings.warn(FUNCARGNAMES, stacklevel=2) + return self.fixturenames + @property def node(self): """ underlying collection node (depends on current request scope)""" diff --git a/src/_pytest/python.py b/src/_pytest/python.py index c1654b1c9..3cee09332 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -31,6 +31,7 @@ from _pytest.compat import safe_getattr from _pytest.compat import safe_isclass from _pytest.compat import STRING_TYPES from _pytest.config import hookimpl +from _pytest.deprecated import FUNCARGNAMES from _pytest.main import FSHookProxy from _pytest.mark import MARK_GEN from _pytest.mark.structures import get_unpacked_marks @@ -882,7 +883,7 @@ class CallSpec2: self.marks.extend(normalize_mark_list(marks)) -class Metafunc(fixtures.FuncargnamesCompatAttr): +class Metafunc: """ Metafunc objects are passed to the :func:`pytest_generate_tests <_pytest.hookspec.pytest_generate_tests>` hook. They help to inspect a test function and to generate tests according to @@ -916,6 +917,12 @@ class Metafunc(fixtures.FuncargnamesCompatAttr): self._ids = set() self._arg2fixturedefs = fixtureinfo.name2fixturedefs + @property + def funcargnames(self): + """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + warnings.warn(FUNCARGNAMES, stacklevel=2) + return self.fixturenames + def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None): """ Add new invocations to the underlying test function using the list of argvalues for the given argnames. Parametrization is performed @@ -1333,7 +1340,7 @@ def write_docstring(tw, doc, indent=" "): tw.write(indent + line + "\n") -class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): +class Function(FunctionMixin, nodes.Item): """ a Function Item is responsible for setting up and executing a Python test function. """ @@ -1420,6 +1427,12 @@ class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr): "(compatonly) for code expecting pytest-2.2 style request objects" return self + @property + def funcargnames(self): + """ alias attribute for ``fixturenames`` for pre-2.3 compatibility""" + warnings.warn(FUNCARGNAMES, stacklevel=2) + return self.fixturenames + def runtest(self): """ execute the underlying test function. """ self.ihook.pytest_pyfunc_call(pyfuncitem=self) From 307add025b5464e87a55b49c6e5e4ce4c6373eee Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Nov 2019 12:53:49 +0200 Subject: [PATCH 134/153] Simplify a FormattedExcinfo test The previous test was better in that it used fakes to test all of the real code paths. The problem with that is that it makes it impossible to simplify the code with `isinstance` checks. So let's just simulate the issue directly with a monkeypatch. --- src/_pytest/_code/code.py | 4 +-- testing/code/test_excinfo.py | 63 +++++------------------------------- 2 files changed, 9 insertions(+), 58 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 19d5efaa6..334365042 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -283,8 +283,6 @@ class Traceback(list): access to Traceback entries. """ - Entry = TracebackEntry - def __init__(self, tb, excinfo=None): """ initialize from given python traceback object and ExceptionInfo """ self._excinfo = excinfo @@ -292,7 +290,7 @@ class Traceback(list): def f(cur): while cur is not None: - yield self.Entry(cur, excinfo=excinfo) + yield TracebackEntry(cur, excinfo=excinfo) cur = cur.tb_next list.__init__(self, f(tb)) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index b83ad93e2..f08808648 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -502,65 +502,18 @@ raise ValueError() assert repr.reprtraceback.reprentries[1].lines[0] == "> ???" assert repr.chain[0][0].reprentries[1].lines[0] == "> ???" - def test_repr_source_failing_fullsource(self): + def test_repr_source_failing_fullsource(self, monkeypatch) -> None: pr = FormattedExcinfo() - class FakeCode: - class raw: - co_filename = "?" + try: + 1 / 0 + except ZeroDivisionError: + excinfo = ExceptionInfo.from_current() - path = "?" - firstlineno = 5 + with monkeypatch.context() as m: + m.setattr(_pytest._code.Code, "fullsource", property(lambda self: None)) + repr = pr.repr_excinfo(excinfo) - def fullsource(self): - return None - - fullsource = property(fullsource) - - class FakeFrame: - code = FakeCode() - f_locals = {} - f_globals = {} - - class FakeTracebackEntry(_pytest._code.Traceback.Entry): - def __init__(self, tb, excinfo=None): - self.lineno = 5 + 3 - - @property - def frame(self): - return FakeFrame() - - class Traceback(_pytest._code.Traceback): - Entry = FakeTracebackEntry - - class FakeExcinfo(_pytest._code.ExceptionInfo): - typename = "Foo" - value = Exception() - - def __init__(self): - pass - - def exconly(self, tryshort): - return "EXC" - - def errisinstance(self, cls): - return False - - excinfo = FakeExcinfo() - - class FakeRawTB: - tb_next = None - - tb = FakeRawTB() - excinfo.traceback = Traceback(tb) - - fail = IOError() - repr = pr.repr_excinfo(excinfo) - assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" - assert repr.chain[0][0].reprentries[0].lines[0] == "> ???" - - fail = py.error.ENOENT # noqa - repr = pr.repr_excinfo(excinfo) assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" assert repr.chain[0][0].reprentries[0].lines[0] == "> ???" From a649f157de8bc16fb99152b2f64e407f1e842692 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sat, 16 Nov 2019 15:50:58 +0200 Subject: [PATCH 135/153] Make Source explicitly implement __iter__() Source was previously iterable because it implements `__getitem__()`, which is apparently a thing from before `__iter__()` was introduced. To reduce mypy's and my own confusion, implement `__iter__()` directly. --- src/_pytest/_code/source.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/_pytest/_code/source.py b/src/_pytest/_code/source.py index a26a70e68..d7cef683d 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -8,6 +8,7 @@ import warnings from ast import PyCF_ONLY_AST as _AST_FLAG from bisect import bisect_right from types import FrameType +from typing import Iterator from typing import List from typing import Optional from typing import Sequence @@ -73,6 +74,9 @@ class Source: newsource.lines = self.lines[key.start : key.stop] return newsource + def __iter__(self) -> Iterator[str]: + return iter(self.lines) + def __len__(self) -> int: return len(self.lines) From 562d4811d59e495bdfd3123a7f725d55462769ec Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 16:26:46 +0200 Subject: [PATCH 136/153] Add type annotations to _pytest.compat --- src/_pytest/compat.py | 47 ++++++++++++++++++++++++++----------------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 09e621c5d..fc810b3e5 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -10,11 +10,14 @@ import sys from contextlib import contextmanager from inspect import Parameter from inspect import signature +from typing import Any from typing import Callable from typing import Generic from typing import Optional from typing import overload +from typing import Tuple from typing import TypeVar +from typing import Union import attr import py @@ -46,7 +49,7 @@ else: import importlib_metadata # noqa: F401 -def _format_args(func): +def _format_args(func: Callable[..., Any]) -> str: return str(signature(func)) @@ -67,12 +70,12 @@ else: fspath = os.fspath -def is_generator(func): +def is_generator(func: object) -> bool: genfunc = inspect.isgeneratorfunction(func) return genfunc and not iscoroutinefunction(func) -def iscoroutinefunction(func): +def iscoroutinefunction(func: object) -> bool: """ Return True if func is a coroutine function (a function defined with async def syntax, and doesn't contain yield), or a function decorated with @@ -85,7 +88,7 @@ def iscoroutinefunction(func): return inspect.iscoroutinefunction(func) or getattr(func, "_is_coroutine", False) -def getlocation(function, curdir=None): +def getlocation(function, curdir=None) -> str: function = get_real_func(function) fn = py.path.local(inspect.getfile(function)) lineno = function.__code__.co_firstlineno @@ -94,7 +97,7 @@ def getlocation(function, curdir=None): return "%s:%d" % (fn, lineno + 1) -def num_mock_patch_args(function): +def num_mock_patch_args(function) -> int: """ return number of arguments used up by mock arguments (if any) """ patchings = getattr(function, "patchings", None) if not patchings: @@ -113,7 +116,13 @@ def num_mock_patch_args(function): ) -def getfuncargnames(function, *, name: str = "", is_method=False, cls=None): +def getfuncargnames( + function: Callable[..., Any], + *, + name: str = "", + is_method: bool = False, + cls: Optional[type] = None +) -> Tuple[str, ...]: """Returns the names of a function's mandatory arguments. This should return the names of all function arguments that: @@ -181,7 +190,7 @@ else: from contextlib import nullcontext # noqa -def get_default_arg_names(function): +def get_default_arg_names(function: Callable[..., Any]) -> Tuple[str, ...]: # Note: this code intentionally mirrors the code at the beginning of getfuncargnames, # to get the arguments which were excluded from its result because they had default values return tuple( @@ -200,18 +209,18 @@ _non_printable_ascii_translate_table.update( ) -def _translate_non_printable(s): +def _translate_non_printable(s: str) -> str: return s.translate(_non_printable_ascii_translate_table) STRING_TYPES = bytes, str -def _bytes_to_ascii(val): +def _bytes_to_ascii(val: bytes) -> str: return val.decode("ascii", "backslashreplace") -def ascii_escaped(val): +def ascii_escaped(val: Union[bytes, str]): """If val is pure ascii, returns it as a str(). Otherwise, escapes bytes objects into a sequence of escaped bytes: @@ -308,7 +317,7 @@ def getimfunc(func): return func -def safe_getattr(object, name, default): +def safe_getattr(object: Any, name: str, default: Any) -> Any: """ Like getattr but return default upon any Exception or any OutcomeException. Attribute access can potentially fail for 'evil' Python objects. @@ -322,7 +331,7 @@ def safe_getattr(object, name, default): return default -def safe_isclass(obj): +def safe_isclass(obj: object) -> bool: """Ignore any exception via isinstance on Python 3.""" try: return inspect.isclass(obj) @@ -343,21 +352,23 @@ COLLECT_FAKEMODULE_ATTRIBUTES = ( ) -def _setup_collect_fakemodule(): +def _setup_collect_fakemodule() -> None: from types import ModuleType import pytest - pytest.collect = ModuleType("pytest.collect") - pytest.collect.__all__ = [] # used for setns + # Types ignored because the module is created dynamically. + pytest.collect = ModuleType("pytest.collect") # type: ignore + pytest.collect.__all__ = [] # type: ignore # used for setns for attr_name in COLLECT_FAKEMODULE_ATTRIBUTES: - setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) + setattr(pytest.collect, attr_name, getattr(pytest, attr_name)) # type: ignore class CaptureIO(io.TextIOWrapper): - def __init__(self): + def __init__(self) -> None: super().__init__(io.BytesIO(), encoding="UTF-8", newline="", write_through=True) - def getvalue(self): + def getvalue(self) -> str: + assert isinstance(self.buffer, io.BytesIO) return self.buffer.getvalue().decode("UTF-8") From 786d839db1e584e33f2f0543b6c16cdfeefe11ee Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 17:17:57 +0100 Subject: [PATCH 137/153] cacheprovider: set: use json.dumps + write ``json.dump`` is slower since it iterates over chunks [1]. For 100 ``cache.set`` calls this saved ~0.5s (2.5s => 2s), using a dict with 1500 entries, and an encoded size of 500kb. Python 3.7.4. 1: https://github.com/blueyed/cpython/blob/1c2e81ed00/Lib/json/__init__.py#L177-L180 --- changelog/6206.improvement.rst | 1 + src/_pytest/cacheprovider.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 changelog/6206.improvement.rst diff --git a/changelog/6206.improvement.rst b/changelog/6206.improvement.rst new file mode 100644 index 000000000..67d8363b3 --- /dev/null +++ b/changelog/6206.improvement.rst @@ -0,0 +1 @@ +cacheprovider: improved robustness and performance with ``cache.set``. diff --git a/src/_pytest/cacheprovider.py b/src/_pytest/cacheprovider.py index 3c60fdb33..6e53545d6 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -125,13 +125,14 @@ class Cache: return if not cache_dir_exists_already: self._ensure_supporting_files() + data = json.dumps(value, indent=2, sort_keys=True) try: f = path.open("w") except (IOError, OSError): self.warn("cache could not write path {path}", path=path) else: with f: - json.dump(value, f, indent=2, sort_keys=True) + f.write(data) def _ensure_supporting_files(self): """Create supporting files in the cache dir that are not really part of the cache.""" From 5d247b9caf5c74ced8fcf48950fd4a15b678c7f6 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 18:42:17 +0100 Subject: [PATCH 138/153] pre-commit: upgrade black This brings https://github.com/psf/black/pull/826, which helps with https://github.com/psf/black/issues/601. --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8481848f7..8210ef5d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ exclude: doc/en/example/py2py3/test_py2.py repos: - repo: https://github.com/psf/black - rev: 19.3b0 + rev: 19.10b0 hooks: - id: black args: [--safe, --quiet] From b1a597ab0292a1360a750f7df8cd9bf498e2cd72 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 18:51:02 +0100 Subject: [PATCH 139/153] Remove (now) unnecessary fmt: off --- src/_pytest/config/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 06296fd35..d4521adf6 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -131,13 +131,13 @@ def directory_arg(path, optname): # Plugins that cannot be disabled via "-p no:X" currently. -essential_plugins = ( # fmt: off +essential_plugins = ( "mark", "main", "runner", "fixtures", "helpconfig", # Provides -p. -) # fmt: on +) default_plugins = essential_plugins + ( "python", From 54a954514b5a02b4858707ef653a1a204cd05509 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 16 Nov 2019 18:53:29 +0100 Subject: [PATCH 140/153] re-run black --- src/_pytest/config/argparsing.py | 2 +- src/_pytest/pytester.py | 2 +- testing/python/fixtures.py | 6 +++--- testing/python/raises.py | 2 +- testing/test_collection.py | 8 ++++---- testing/test_mark.py | 2 +- testing/test_skipping.py | 2 +- testing/test_tmpdir.py | 2 +- testing/test_unittest.py | 2 +- 9 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/_pytest/config/argparsing.py b/src/_pytest/config/argparsing.py index 4eec6be05..7cbb676bd 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -395,7 +395,7 @@ class MyOptionParser(argparse.ArgumentParser): options = ", ".join(option for _, option, _ in option_tuples) self.error(msg % {"option": arg_string, "matches": options}) elif len(option_tuples) == 1: - option_tuple, = option_tuples + (option_tuple,) = option_tuples return option_tuple if self._negative_number_matcher.match(arg_string): if not self._has_negative_number_optionals: diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index ca780a9f5..02414a299 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -312,7 +312,7 @@ class HookRecorder: return self.getfailures("pytest_collectreport") def listoutcomes( - self + self, ) -> Tuple[List[TestReport], List[TestReport], List[TestReport]]: passed = [] skipped = [] diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 6dca793e0..52fd32cc4 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -503,7 +503,7 @@ class TestRequestBasic: assert repr(req).find(req.function.__name__) != -1 def test_request_attributes_method(self, testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ import pytest class TestB(object): @@ -531,7 +531,7 @@ class TestRequestBasic: pass """ ) - item1, = testdir.genitems([modcol]) + (item1,) = testdir.genitems([modcol]) assert item1.name == "test_method" arg2fixturedefs = fixtures.FixtureRequest(item1)._arg2fixturedefs assert len(arg2fixturedefs) == 1 @@ -781,7 +781,7 @@ class TestRequestBasic: def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") - item, = testdir.genitems([modcol]) + (item,) = testdir.genitems([modcol]) req = fixtures.FixtureRequest(item) assert req.fspath == modcol.fspath diff --git a/testing/python/raises.py b/testing/python/raises.py index 28b0715c0..1c701796a 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -205,7 +205,7 @@ class TestRaises: with pytest.raises(AssertionError) as excinfo: with pytest.raises(AssertionError, match="'foo"): raise AssertionError("'bar") - msg, = excinfo.value.args + (msg,) = excinfo.value.args assert msg == 'Pattern "\'foo" not found in "\'bar"' def test_raises_match_wrong_type(self): diff --git a/testing/test_collection.py b/testing/test_collection.py index f18d36d24..e4a70b1a7 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -486,7 +486,7 @@ class TestSession: p = testdir.makepyfile("def test_func(): pass") id = "::".join([p.basename, "test_func"]) items, hookrec = testdir.inline_genitems(id) - item, = items + (item,) = items assert item.name == "test_func" newid = item.nodeid assert newid == id @@ -605,9 +605,9 @@ class TestSession: testdir.makepyfile("def test_func(): pass") items, hookrec = testdir.inline_genitems() assert len(items) == 1 - item, = items + (item,) = items items2, hookrec = testdir.inline_genitems(item.nodeid) - item2, = items2 + (item2,) = items2 assert item2.name == item.name assert item2.fspath == item.fspath @@ -622,7 +622,7 @@ class TestSession: arg = p.basename + "::TestClass::test_method" items, hookrec = testdir.inline_genitems(arg) assert len(items) == 1 - item, = items + (item,) = items assert item.nodeid.endswith("TestClass::test_method") # ensure we are reporting the collection of the single test item (#2464) assert [x.name for x in self.get_reported_items(hookrec)] == ["test_method"] diff --git a/testing/test_mark.py b/testing/test_mark.py index ba7599804..0e4422025 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1011,7 +1011,7 @@ def test_markers_from_parametrize(testdir): def test_pytest_param_id_requires_string(): with pytest.raises(TypeError) as excinfo: pytest.param(id=True) - msg, = excinfo.value.args + (msg,) = excinfo.value.args assert msg == "Expected id to be a string, got : True" diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 86f328a93..67714d030 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -115,7 +115,7 @@ class TestEvaluator: ) def test_skipif_class(self, testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ import pytest class TestClass(object): diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 29b6db947..eb1c1f300 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -258,7 +258,7 @@ class TestNumberedDir: registry = [] register_cleanup_lock_removal(lock, register=registry.append) - cleanup_func, = registry + (cleanup_func,) = registry assert lock.is_file() diff --git a/testing/test_unittest.py b/testing/test_unittest.py index f56284d85..885178402 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -383,7 +383,7 @@ def test_testcase_custom_exception_info(testdir, type): def test_testcase_totally_incompatible_exception_info(testdir): - item, = testdir.getitems( + (item,) = testdir.getitems( """ from unittest import TestCase class MyTestCase(TestCase): From eaa34a9df0fa341c1b21bd3b232928812e6e8a06 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Fri, 15 Nov 2019 23:02:55 +0200 Subject: [PATCH 141/153] Add type annotations to _pytest._code.code --- src/_pytest/_code/code.py | 240 ++++++++++++++++++++++------------- testing/code/test_code.py | 57 +++++---- testing/code/test_excinfo.py | 19 +-- testing/code/test_source.py | 197 +++++++++++++++------------- 4 files changed, 299 insertions(+), 214 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 334365042..a8f117366 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -7,13 +7,17 @@ from inspect import CO_VARKEYWORDS from io import StringIO from traceback import format_exception_only from types import CodeType +from types import FrameType from types import TracebackType from typing import Any +from typing import Callable from typing import Dict from typing import Generic +from typing import Iterable from typing import List from typing import Optional from typing import Pattern +from typing import Sequence from typing import Set from typing import Tuple from typing import TypeVar @@ -27,9 +31,16 @@ import py import _pytest from _pytest._io.saferepr import safeformat from _pytest._io.saferepr import saferepr +from _pytest.compat import overload if False: # TYPE_CHECKING from typing import Type + from typing_extensions import Literal + from weakref import ReferenceType # noqa: F401 + + from _pytest._code import Source + + _TracebackStyle = Literal["long", "short", "no", "native"] class Code: @@ -38,13 +49,12 @@ class Code: def __init__(self, rawcode) -> None: if not hasattr(rawcode, "co_filename"): rawcode = getrawcode(rawcode) - try: - self.filename = rawcode.co_filename - self.firstlineno = rawcode.co_firstlineno - 1 - self.name = rawcode.co_name - except AttributeError: + if not isinstance(rawcode, CodeType): raise TypeError("not a code object: {!r}".format(rawcode)) - self.raw = rawcode # type: CodeType + self.filename = rawcode.co_filename + self.firstlineno = rawcode.co_firstlineno - 1 + self.name = rawcode.co_name + self.raw = rawcode def __eq__(self, other): return self.raw == other.raw @@ -72,7 +82,7 @@ class Code: return p @property - def fullsource(self): + def fullsource(self) -> Optional["Source"]: """ return a _pytest._code.Source object for the full source file of the code """ from _pytest._code import source @@ -80,7 +90,7 @@ class Code: full, _ = source.findsource(self.raw) return full - def source(self): + def source(self) -> "Source": """ return a _pytest._code.Source object for the code object's source only """ # return source only for that part of code @@ -88,7 +98,7 @@ class Code: return _pytest._code.Source(self.raw) - def getargs(self, var=False): + def getargs(self, var: bool = False) -> Tuple[str, ...]: """ return a tuple with the argument names for the code object if 'var' is set True also return the names of the variable and @@ -107,7 +117,7 @@ class Frame: """Wrapper around a Python frame holding f_locals and f_globals in which expressions can be evaluated.""" - def __init__(self, frame): + def __init__(self, frame: FrameType) -> None: self.lineno = frame.f_lineno - 1 self.f_globals = frame.f_globals self.f_locals = frame.f_locals @@ -115,7 +125,7 @@ class Frame: self.code = Code(frame.f_code) @property - def statement(self): + def statement(self) -> "Source": """ statement this frame is at """ import _pytest._code @@ -134,7 +144,7 @@ class Frame: f_locals.update(vars) return eval(code, self.f_globals, f_locals) - def exec_(self, code, **vars): + def exec_(self, code, **vars) -> None: """ exec 'code' in the frame 'vars' are optional; additional local variables @@ -143,7 +153,7 @@ class Frame: f_locals.update(vars) exec(code, self.f_globals, f_locals) - def repr(self, object): + def repr(self, object: object) -> str: """ return a 'safe' (non-recursive, one-line) string repr for 'object' """ return saferepr(object) @@ -151,7 +161,7 @@ class Frame: def is_true(self, object): return object - def getargs(self, var=False): + def getargs(self, var: bool = False): """ return a list of tuples (name, value) for all arguments if 'var' is set True also include the variable and keyword @@ -169,35 +179,34 @@ class Frame: class TracebackEntry: """ a single entry in a traceback """ - _repr_style = None + _repr_style = None # type: Optional[Literal["short", "long"]] exprinfo = None - def __init__(self, rawentry, excinfo=None): + def __init__(self, rawentry: TracebackType, excinfo=None) -> None: self._excinfo = excinfo self._rawentry = rawentry self.lineno = rawentry.tb_lineno - 1 - def set_repr_style(self, mode): + def set_repr_style(self, mode: "Literal['short', 'long']") -> None: assert mode in ("short", "long") self._repr_style = mode @property - def frame(self): - import _pytest._code - - return _pytest._code.Frame(self._rawentry.tb_frame) + def frame(self) -> Frame: + return Frame(self._rawentry.tb_frame) @property - def relline(self): + def relline(self) -> int: return self.lineno - self.frame.code.firstlineno - def __repr__(self): + def __repr__(self) -> str: return "" % (self.frame.code.path, self.lineno + 1) @property - def statement(self): + def statement(self) -> "Source": """ _pytest._code.Source object for the current statement """ source = self.frame.code.fullsource + assert source is not None return source.getstatement(self.lineno) @property @@ -206,14 +215,14 @@ class TracebackEntry: return self.frame.code.path @property - def locals(self): + def locals(self) -> Dict[str, Any]: """ locals of underlying frame """ return self.frame.f_locals - def getfirstlinesource(self): + def getfirstlinesource(self) -> int: return self.frame.code.firstlineno - def getsource(self, astcache=None): + def getsource(self, astcache=None) -> Optional["Source"]: """ return failing source code. """ # we use the passed in astcache to not reparse asttrees # within exception info printing @@ -258,7 +267,7 @@ class TracebackEntry: return tbh(None if self._excinfo is None else self._excinfo()) return tbh - def __str__(self): + def __str__(self) -> str: try: fn = str(self.path) except py.error.Error: @@ -273,31 +282,42 @@ class TracebackEntry: return " File %r:%d in %s\n %s\n" % (fn, self.lineno + 1, name, line) @property - def name(self): + def name(self) -> str: """ co_name of underlying code """ return self.frame.code.raw.co_name -class Traceback(list): +class Traceback(List[TracebackEntry]): """ Traceback objects encapsulate and offer higher level access to Traceback entries. """ - def __init__(self, tb, excinfo=None): + def __init__( + self, + tb: Union[TracebackType, Iterable[TracebackEntry]], + excinfo: Optional["ReferenceType[ExceptionInfo]"] = None, + ) -> None: """ initialize from given python traceback object and ExceptionInfo """ self._excinfo = excinfo - if hasattr(tb, "tb_next"): + if isinstance(tb, TracebackType): - def f(cur): - while cur is not None: - yield TracebackEntry(cur, excinfo=excinfo) - cur = cur.tb_next + def f(cur: TracebackType) -> Iterable[TracebackEntry]: + cur_ = cur # type: Optional[TracebackType] + while cur_ is not None: + yield TracebackEntry(cur_, excinfo=excinfo) + cur_ = cur_.tb_next - list.__init__(self, f(tb)) + super().__init__(f(tb)) else: - list.__init__(self, tb) + super().__init__(tb) - def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): + def cut( + self, + path=None, + lineno: Optional[int] = None, + firstlineno: Optional[int] = None, + excludepath=None, + ) -> "Traceback": """ return a Traceback instance wrapping part of this Traceback by providing any combination of path, lineno and firstlineno, the @@ -323,13 +343,25 @@ class Traceback(list): return Traceback(x._rawentry, self._excinfo) return self - def __getitem__(self, key): - val = super().__getitem__(key) - if isinstance(key, type(slice(0))): - val = self.__class__(val) - return val + @overload + def __getitem__(self, key: int) -> TracebackEntry: + raise NotImplementedError() - def filter(self, fn=lambda x: not x.ishidden()): + @overload # noqa: F811 + def __getitem__(self, key: slice) -> "Traceback": # noqa: F811 + raise NotImplementedError() + + def __getitem__( # noqa: F811 + self, key: Union[int, slice] + ) -> Union[TracebackEntry, "Traceback"]: + if isinstance(key, slice): + return self.__class__(super().__getitem__(key)) + else: + return super().__getitem__(key) + + def filter( + self, fn: Callable[[TracebackEntry], bool] = lambda x: not x.ishidden() + ) -> "Traceback": """ return a Traceback instance with certain items removed fn is a function that gets a single argument, a TracebackEntry @@ -341,7 +373,7 @@ class Traceback(list): """ return Traceback(filter(fn, self), self._excinfo) - def getcrashentry(self): + def getcrashentry(self) -> TracebackEntry: """ return last non-hidden traceback entry that lead to the exception of a traceback. """ @@ -351,7 +383,7 @@ class Traceback(list): return entry return self[-1] - def recursionindex(self): + def recursionindex(self) -> Optional[int]: """ return the index of the frame/TracebackEntry where recursion originates if appropriate, None if no recursion occurred """ @@ -541,7 +573,7 @@ class ExceptionInfo(Generic[_E]): def getrepr( self, showlocals: bool = False, - style: str = "long", + style: "_TracebackStyle" = "long", abspath: bool = False, tbfilter: bool = True, funcargs: bool = False, @@ -619,16 +651,16 @@ class FormattedExcinfo: flow_marker = ">" fail_marker = "E" - showlocals = attr.ib(default=False) - style = attr.ib(default="long") - abspath = attr.ib(default=True) - tbfilter = attr.ib(default=True) - funcargs = attr.ib(default=False) - truncate_locals = attr.ib(default=True) - chain = attr.ib(default=True) + showlocals = attr.ib(type=bool, default=False) + style = attr.ib(type="_TracebackStyle", default="long") + abspath = attr.ib(type=bool, default=True) + tbfilter = attr.ib(type=bool, default=True) + funcargs = attr.ib(type=bool, default=False) + truncate_locals = attr.ib(type=bool, default=True) + chain = attr.ib(type=bool, default=True) astcache = attr.ib(default=attr.Factory(dict), init=False, repr=False) - def _getindent(self, source): + def _getindent(self, source: "Source") -> int: # figure out indent for given source try: s = str(source.getstatement(len(source) - 1)) @@ -643,20 +675,27 @@ class FormattedExcinfo: return 0 return 4 + (len(s) - len(s.lstrip())) - def _getentrysource(self, entry): + def _getentrysource(self, entry: TracebackEntry) -> Optional["Source"]: source = entry.getsource(self.astcache) if source is not None: source = source.deindent() return source - def repr_args(self, entry): + def repr_args(self, entry: TracebackEntry) -> Optional["ReprFuncArgs"]: if self.funcargs: args = [] for argname, argvalue in entry.frame.getargs(var=True): args.append((argname, saferepr(argvalue))) return ReprFuncArgs(args) + return None - def get_source(self, source, line_index=-1, excinfo=None, short=False) -> List[str]: + def get_source( + self, + source: "Source", + line_index: int = -1, + excinfo: Optional[ExceptionInfo] = None, + short: bool = False, + ) -> List[str]: """ return formatted and marked up source lines. """ import _pytest._code @@ -680,19 +719,21 @@ class FormattedExcinfo: lines.extend(self.get_exconly(excinfo, indent=indent, markall=True)) return lines - def get_exconly(self, excinfo, indent=4, markall=False): + def get_exconly( + self, excinfo: ExceptionInfo, indent: int = 4, markall: bool = False + ) -> List[str]: lines = [] - indent = " " * indent + indentstr = " " * indent # get the real exception information out exlines = excinfo.exconly(tryshort=True).split("\n") - failindent = self.fail_marker + indent[1:] + failindent = self.fail_marker + indentstr[1:] for line in exlines: lines.append(failindent + line) if not markall: - failindent = indent + failindent = indentstr return lines - def repr_locals(self, locals): + def repr_locals(self, locals: Dict[str, object]) -> Optional["ReprLocals"]: if self.showlocals: lines = [] keys = [loc for loc in locals if loc[0] != "@"] @@ -717,8 +758,11 @@ class FormattedExcinfo: # # XXX # pprint.pprint(value, stream=self.excinfowriter) return ReprLocals(lines) + return None - def repr_traceback_entry(self, entry, excinfo=None): + def repr_traceback_entry( + self, entry: TracebackEntry, excinfo: Optional[ExceptionInfo] = None + ) -> "ReprEntry": import _pytest._code source = self._getentrysource(entry) @@ -729,9 +773,7 @@ class FormattedExcinfo: line_index = entry.lineno - entry.getfirstlinesource() lines = [] # type: List[str] - style = entry._repr_style - if style is None: - style = self.style + style = entry._repr_style if entry._repr_style is not None else self.style if style in ("short", "long"): short = style == "short" reprargs = self.repr_args(entry) if not short else None @@ -761,7 +803,7 @@ class FormattedExcinfo: path = np return path - def repr_traceback(self, excinfo): + def repr_traceback(self, excinfo: ExceptionInfo) -> "ReprTraceback": traceback = excinfo.traceback if self.tbfilter: traceback = traceback.filter() @@ -779,7 +821,9 @@ class FormattedExcinfo: entries.append(reprentry) return ReprTraceback(entries, extraline, style=self.style) - def _truncate_recursive_traceback(self, traceback): + def _truncate_recursive_traceback( + self, traceback: Traceback + ) -> Tuple[Traceback, Optional[str]]: """ Truncate the given recursive traceback trying to find the starting point of the recursion. @@ -806,7 +850,9 @@ class FormattedExcinfo: max_frames=max_frames, total=len(traceback), ) # type: Optional[str] - traceback = traceback[:max_frames] + traceback[-max_frames:] + # Type ignored because adding two instaces of a List subtype + # currently incorrectly has type List instead of the subtype. + traceback = traceback[:max_frames] + traceback[-max_frames:] # type: ignore else: if recursionindex is not None: extraline = "!!! Recursion detected (same locals & position)" @@ -863,7 +909,7 @@ class FormattedExcinfo: class TerminalRepr: - def __str__(self): + def __str__(self) -> str: # FYI this is called from pytest-xdist's serialization of exception # information. io = StringIO() @@ -871,7 +917,7 @@ class TerminalRepr: self.toterminal(tw) return io.getvalue().strip() - def __repr__(self): + def __repr__(self) -> str: return "<{} instance at {:0x}>".format(self.__class__, id(self)) def toterminal(self, tw) -> None: @@ -882,7 +928,7 @@ class ExceptionRepr(TerminalRepr): def __init__(self) -> None: self.sections = [] # type: List[Tuple[str, str, str]] - def addsection(self, name, content, sep="-"): + def addsection(self, name: str, content: str, sep: str = "-") -> None: self.sections.append((name, content, sep)) def toterminal(self, tw) -> None: @@ -892,7 +938,12 @@ class ExceptionRepr(TerminalRepr): class ExceptionChainRepr(ExceptionRepr): - def __init__(self, chain): + def __init__( + self, + chain: Sequence[ + Tuple["ReprTraceback", Optional["ReprFileLocation"], Optional[str]] + ], + ) -> None: super().__init__() self.chain = chain # reprcrash and reprtraceback of the outermost (the newest) exception @@ -910,7 +961,9 @@ class ExceptionChainRepr(ExceptionRepr): class ReprExceptionInfo(ExceptionRepr): - def __init__(self, reprtraceback, reprcrash): + def __init__( + self, reprtraceback: "ReprTraceback", reprcrash: "ReprFileLocation" + ) -> None: super().__init__() self.reprtraceback = reprtraceback self.reprcrash = reprcrash @@ -923,7 +976,12 @@ class ReprExceptionInfo(ExceptionRepr): class ReprTraceback(TerminalRepr): entrysep = "_ " - def __init__(self, reprentries, extraline, style): + def __init__( + self, + reprentries: Sequence[Union["ReprEntry", "ReprEntryNative"]], + extraline: Optional[str], + style: "_TracebackStyle", + ) -> None: self.reprentries = reprentries self.extraline = extraline self.style = style @@ -948,16 +1006,16 @@ class ReprTraceback(TerminalRepr): class ReprTracebackNative(ReprTraceback): - def __init__(self, tblines): + def __init__(self, tblines: Sequence[str]) -> None: self.style = "native" self.reprentries = [ReprEntryNative(tblines)] self.extraline = None class ReprEntryNative(TerminalRepr): - style = "native" + style = "native" # type: _TracebackStyle - def __init__(self, tblines): + def __init__(self, tblines: Sequence[str]) -> None: self.lines = tblines def toterminal(self, tw) -> None: @@ -965,7 +1023,14 @@ class ReprEntryNative(TerminalRepr): class ReprEntry(TerminalRepr): - def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): + def __init__( + self, + lines: Sequence[str], + reprfuncargs: Optional["ReprFuncArgs"], + reprlocals: Optional["ReprLocals"], + filelocrepr: Optional["ReprFileLocation"], + style: "_TracebackStyle", + ) -> None: self.lines = lines self.reprfuncargs = reprfuncargs self.reprlocals = reprlocals @@ -974,6 +1039,7 @@ class ReprEntry(TerminalRepr): def toterminal(self, tw) -> None: if self.style == "short": + assert self.reprfileloc is not None self.reprfileloc.toterminal(tw) for line in self.lines: red = line.startswith("E ") @@ -992,14 +1058,14 @@ class ReprEntry(TerminalRepr): tw.line("") self.reprfileloc.toterminal(tw) - def __str__(self): + def __str__(self) -> str: return "{}\n{}\n{}".format( "\n".join(self.lines), self.reprlocals, self.reprfileloc ) class ReprFileLocation(TerminalRepr): - def __init__(self, path, lineno, message): + def __init__(self, path, lineno: int, message: str) -> None: self.path = str(path) self.lineno = lineno self.message = message @@ -1016,7 +1082,7 @@ class ReprFileLocation(TerminalRepr): class ReprLocals(TerminalRepr): - def __init__(self, lines): + def __init__(self, lines: Sequence[str]) -> None: self.lines = lines def toterminal(self, tw) -> None: @@ -1025,7 +1091,7 @@ class ReprLocals(TerminalRepr): class ReprFuncArgs(TerminalRepr): - def __init__(self, args): + def __init__(self, args: Sequence[Tuple[str, object]]) -> None: self.args = args def toterminal(self, tw) -> None: @@ -1047,7 +1113,7 @@ class ReprFuncArgs(TerminalRepr): tw.line("") -def getrawcode(obj, trycall=True): +def getrawcode(obj, trycall: bool = True): """ return code object for given function. """ try: return obj.__code__ @@ -1075,7 +1141,7 @@ _PYTEST_DIR = py.path.local(_pytest.__file__).dirpath() _PY_DIR = py.path.local(py.__file__).dirpath() -def filter_traceback(entry): +def filter_traceback(entry: TracebackEntry) -> bool: """Return True if a TracebackEntry instance should be removed from tracebacks: * dynamically generated code (no code to show up for it); * internal traceback from pytest or its internal libraries, py and pluggy. diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 2f55720b4..f8e1ce17f 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -1,18 +1,19 @@ import sys +from types import FrameType from unittest import mock import _pytest._code import pytest -def test_ne(): +def test_ne() -> None: code1 = _pytest._code.Code(compile('foo = "bar"', "", "exec")) assert code1 == code1 code2 = _pytest._code.Code(compile('foo = "baz"', "", "exec")) assert code2 != code1 -def test_code_gives_back_name_for_not_existing_file(): +def test_code_gives_back_name_for_not_existing_file() -> None: name = "abc-123" co_code = compile("pass\n", name, "exec") assert co_code.co_filename == name @@ -21,68 +22,67 @@ def test_code_gives_back_name_for_not_existing_file(): assert code.fullsource is None -def test_code_with_class(): +def test_code_with_class() -> None: class A: pass pytest.raises(TypeError, _pytest._code.Code, A) -def x(): +def x() -> None: raise NotImplementedError() -def test_code_fullsource(): +def test_code_fullsource() -> None: code = _pytest._code.Code(x) full = code.fullsource assert "test_code_fullsource()" in str(full) -def test_code_source(): +def test_code_source() -> None: code = _pytest._code.Code(x) src = code.source() - expected = """def x(): + expected = """def x() -> None: raise NotImplementedError()""" assert str(src) == expected -def test_frame_getsourcelineno_myself(): - def func(): +def test_frame_getsourcelineno_myself() -> None: + def func() -> FrameType: return sys._getframe(0) - f = func() - f = _pytest._code.Frame(f) + f = _pytest._code.Frame(func()) source, lineno = f.code.fullsource, f.lineno + assert source is not None assert source[lineno].startswith(" return sys._getframe(0)") -def test_getstatement_empty_fullsource(): - def func(): +def test_getstatement_empty_fullsource() -> None: + def func() -> FrameType: return sys._getframe(0) - f = func() - f = _pytest._code.Frame(f) + f = _pytest._code.Frame(func()) with mock.patch.object(f.code.__class__, "fullsource", None): assert f.statement == "" -def test_code_from_func(): +def test_code_from_func() -> None: co = _pytest._code.Code(test_frame_getsourcelineno_myself) assert co.firstlineno assert co.path -def test_unicode_handling(): +def test_unicode_handling() -> None: value = "ąć".encode() - def f(): + def f() -> None: raise Exception(value) excinfo = pytest.raises(Exception, f) str(excinfo) -def test_code_getargs(): +def test_code_getargs() -> None: def f1(x): raise NotImplementedError() @@ -108,26 +108,26 @@ def test_code_getargs(): assert c4.getargs(var=True) == ("x", "y", "z") -def test_frame_getargs(): - def f1(x): +def test_frame_getargs() -> None: + def f1(x) -> FrameType: return sys._getframe(0) fr1 = _pytest._code.Frame(f1("a")) assert fr1.getargs(var=True) == [("x", "a")] - def f2(x, *y): + def f2(x, *y) -> FrameType: return sys._getframe(0) fr2 = _pytest._code.Frame(f2("a", "b", "c")) assert fr2.getargs(var=True) == [("x", "a"), ("y", ("b", "c"))] - def f3(x, **z): + def f3(x, **z) -> FrameType: return sys._getframe(0) fr3 = _pytest._code.Frame(f3("a", b="c")) assert fr3.getargs(var=True) == [("x", "a"), ("z", {"b": "c"})] - def f4(x, *y, **z): + def f4(x, *y, **z) -> FrameType: return sys._getframe(0) fr4 = _pytest._code.Frame(f4("a", "b", c="d")) @@ -135,7 +135,7 @@ def test_frame_getargs(): class TestExceptionInfo: - def test_bad_getsource(self): + def test_bad_getsource(self) -> None: try: if False: pass @@ -145,13 +145,13 @@ class TestExceptionInfo: exci = _pytest._code.ExceptionInfo.from_current() assert exci.getrepr() - def test_from_current_with_missing(self): + def test_from_current_with_missing(self) -> None: with pytest.raises(AssertionError, match="no current exception"): _pytest._code.ExceptionInfo.from_current() class TestTracebackEntry: - def test_getsource(self): + def test_getsource(self) -> None: try: if False: pass @@ -161,12 +161,13 @@ class TestTracebackEntry: exci = _pytest._code.ExceptionInfo.from_current() entry = exci.traceback[0] source = entry.getsource() + assert source is not None assert len(source) == 6 assert "assert False" in source[5] class TestReprFuncArgs: - def test_not_raise_exception_with_mixed_encoding(self, tw_mock): + def test_not_raise_exception_with_mixed_encoding(self, tw_mock) -> None: from _pytest._code.code import ReprFuncArgs args = [("unicode_string", "São Paulo"), ("utf8_string", b"S\xc3\xa3o Paulo")] diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index f08808648..997b14e2f 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -3,6 +3,7 @@ import os import queue import sys import textwrap +from typing import Union import py @@ -224,23 +225,25 @@ class TestTraceback_f_g_h: repr = excinfo.getrepr() assert "RuntimeError: hello" in str(repr.reprcrash) - def test_traceback_no_recursion_index(self): - def do_stuff(): + def test_traceback_no_recursion_index(self) -> None: + def do_stuff() -> None: raise RuntimeError - def reraise_me(): + def reraise_me() -> None: import sys exc, val, tb = sys.exc_info() + assert val is not None raise val.with_traceback(tb) - def f(n): + def f(n: int) -> None: try: do_stuff() except: # noqa reraise_me() excinfo = pytest.raises(RuntimeError, f, 8) + assert excinfo is not None traceback = excinfo.traceback recindex = traceback.recursionindex() assert recindex is None @@ -596,7 +599,6 @@ raise ValueError() assert lines[3] == "E world" assert not lines[4:] - loc = repr_entry.reprlocals is not None loc = repr_entry.reprfileloc assert loc.path == mod.__file__ assert loc.lineno == 3 @@ -1286,9 +1288,10 @@ raise ValueError() @pytest.mark.parametrize("style", ["short", "long"]) @pytest.mark.parametrize("encoding", [None, "utf8", "utf16"]) def test_repr_traceback_with_unicode(style, encoding): - msg = "☹" - if encoding is not None: - msg = msg.encode(encoding) + if encoding is None: + msg = "☹" # type: Union[str, bytes] + else: + msg = "☹".encode(encoding) try: raise RuntimeError(msg) except RuntimeError: diff --git a/testing/code/test_source.py b/testing/code/test_source.py index 519344dd4..bf52dccd7 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -4,13 +4,16 @@ import ast import inspect import sys +from typing import Any +from typing import Dict +from typing import Optional import _pytest._code import pytest from _pytest._code import Source -def test_source_str_function(): +def test_source_str_function() -> None: x = Source("3") assert str(x) == "3" @@ -25,7 +28,7 @@ def test_source_str_function(): assert str(x) == "\n3" -def test_unicode(): +def test_unicode() -> None: x = Source("4") assert str(x) == "4" co = _pytest._code.compile('"å"', mode="eval") @@ -33,12 +36,12 @@ def test_unicode(): assert isinstance(val, str) -def test_source_from_function(): +def test_source_from_function() -> None: source = _pytest._code.Source(test_source_str_function) - assert str(source).startswith("def test_source_str_function():") + assert str(source).startswith("def test_source_str_function() -> None:") -def test_source_from_method(): +def test_source_from_method() -> None: class TestClass: def test_method(self): pass @@ -47,13 +50,13 @@ def test_source_from_method(): assert source.lines == ["def test_method(self):", " pass"] -def test_source_from_lines(): +def test_source_from_lines() -> None: lines = ["a \n", "b\n", "c"] source = _pytest._code.Source(lines) assert source.lines == ["a ", "b", "c"] -def test_source_from_inner_function(): +def test_source_from_inner_function() -> None: def f(): pass @@ -63,7 +66,7 @@ def test_source_from_inner_function(): assert str(source).startswith("def f():") -def test_source_putaround_simple(): +def test_source_putaround_simple() -> None: source = Source("raise ValueError") source = source.putaround( "try:", @@ -85,7 +88,7 @@ else: ) -def test_source_putaround(): +def test_source_putaround() -> None: source = Source() source = source.putaround( """ @@ -96,28 +99,29 @@ def test_source_putaround(): assert str(source).strip() == "if 1:\n x=1" -def test_source_strips(): +def test_source_strips() -> None: source = Source("") assert source == Source() assert str(source) == "" assert source.strip() == source -def test_source_strip_multiline(): +def test_source_strip_multiline() -> None: source = Source() source.lines = ["", " hello", " "] source2 = source.strip() assert source2.lines == [" hello"] -def test_syntaxerror_rerepresentation(): +def test_syntaxerror_rerepresentation() -> None: ex = pytest.raises(SyntaxError, _pytest._code.compile, "xyz xyz") + assert ex is not None assert ex.value.lineno == 1 assert ex.value.offset in {5, 7} # cpython: 7, pypy3.6 7.1.1: 5 - assert ex.value.text.strip(), "x x" + assert ex.value.text == "xyz xyz\n" -def test_isparseable(): +def test_isparseable() -> None: assert Source("hello").isparseable() assert Source("if 1:\n pass").isparseable() assert Source(" \nif 1:\n pass").isparseable() @@ -127,7 +131,7 @@ def test_isparseable(): class TestAccesses: - def setup_class(self): + def setup_class(self) -> None: self.source = Source( """\ def f(x): @@ -137,26 +141,26 @@ class TestAccesses: """ ) - def test_getrange(self): + def test_getrange(self) -> None: x = self.source[0:2] assert x.isparseable() assert len(x.lines) == 2 assert str(x) == "def f(x):\n pass" - def test_getline(self): + def test_getline(self) -> None: x = self.source[0] assert x == "def f(x):" - def test_len(self): + def test_len(self) -> None: assert len(self.source) == 4 - def test_iter(self): + def test_iter(self) -> None: values = [x for x in self.source] assert len(values) == 4 class TestSourceParsingAndCompiling: - def setup_class(self): + def setup_class(self) -> None: self.source = Source( """\ def f(x): @@ -166,19 +170,19 @@ class TestSourceParsingAndCompiling: """ ).strip() - def test_compile(self): + def test_compile(self) -> None: co = _pytest._code.compile("x=3") - d = {} + d = {} # type: Dict[str, Any] exec(co, d) assert d["x"] == 3 - def test_compile_and_getsource_simple(self): + def test_compile_and_getsource_simple(self) -> None: co = _pytest._code.compile("x=3") exec(co) source = _pytest._code.Source(co) assert str(source) == "x=3" - def test_compile_and_getsource_through_same_function(self): + def test_compile_and_getsource_through_same_function(self) -> None: def gensource(source): return _pytest._code.compile(source) @@ -199,7 +203,7 @@ class TestSourceParsingAndCompiling: source2 = inspect.getsource(co2) assert "ValueError" in source2 - def test_getstatement(self): + def test_getstatement(self) -> None: # print str(self.source) ass = str(self.source[1:]) for i in range(1, 4): @@ -208,7 +212,7 @@ class TestSourceParsingAndCompiling: # x = s.deindent() assert str(s) == ass - def test_getstatementrange_triple_quoted(self): + def test_getstatementrange_triple_quoted(self) -> None: # print str(self.source) source = Source( """hello(''' @@ -219,7 +223,7 @@ class TestSourceParsingAndCompiling: s = source.getstatement(1) assert s == str(source) - def test_getstatementrange_within_constructs(self): + def test_getstatementrange_within_constructs(self) -> None: source = Source( """\ try: @@ -241,7 +245,7 @@ class TestSourceParsingAndCompiling: # assert source.getstatementrange(5) == (0, 7) assert source.getstatementrange(6) == (6, 7) - def test_getstatementrange_bug(self): + def test_getstatementrange_bug(self) -> None: source = Source( """\ try: @@ -255,7 +259,7 @@ class TestSourceParsingAndCompiling: assert len(source) == 6 assert source.getstatementrange(2) == (1, 4) - def test_getstatementrange_bug2(self): + def test_getstatementrange_bug2(self) -> None: source = Source( """\ assert ( @@ -272,7 +276,7 @@ class TestSourceParsingAndCompiling: assert len(source) == 9 assert source.getstatementrange(5) == (0, 9) - def test_getstatementrange_ast_issue58(self): + def test_getstatementrange_ast_issue58(self) -> None: source = Source( """\ @@ -286,38 +290,44 @@ class TestSourceParsingAndCompiling: assert getstatement(2, source).lines == source.lines[2:3] assert getstatement(3, source).lines == source.lines[3:4] - def test_getstatementrange_out_of_bounds_py3(self): + def test_getstatementrange_out_of_bounds_py3(self) -> None: source = Source("if xxx:\n from .collections import something") r = source.getstatementrange(1) assert r == (1, 2) - def test_getstatementrange_with_syntaxerror_issue7(self): + def test_getstatementrange_with_syntaxerror_issue7(self) -> None: source = Source(":") pytest.raises(SyntaxError, lambda: source.getstatementrange(0)) - def test_compile_to_ast(self): + def test_compile_to_ast(self) -> None: source = Source("x = 4") mod = source.compile(flag=ast.PyCF_ONLY_AST) assert isinstance(mod, ast.Module) compile(mod, "", "exec") - def test_compile_and_getsource(self): + def test_compile_and_getsource(self) -> None: co = self.source.compile() exec(co, globals()) - f(7) - excinfo = pytest.raises(AssertionError, f, 6) + f(7) # type: ignore + excinfo = pytest.raises(AssertionError, f, 6) # type: ignore + assert excinfo is not None frame = excinfo.traceback[-1].frame + assert isinstance(frame.code.fullsource, Source) stmt = frame.code.fullsource.getstatement(frame.lineno) assert str(stmt).strip().startswith("assert") @pytest.mark.parametrize("name", ["", None, "my"]) - def test_compilefuncs_and_path_sanity(self, name): + def test_compilefuncs_and_path_sanity(self, name: Optional[str]) -> None: def check(comp, name): co = comp(self.source, name) if not name: - expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2) + expected = "codegen %s:%d>" % (mypath, mylineno + 2 + 2) # type: ignore else: - expected = "codegen %r %s:%d>" % (name, mypath, mylineno + 2 + 2) + expected = "codegen %r %s:%d>" % ( + name, + mypath, # type: ignore + mylineno + 2 + 2, # type: ignore + ) # type: ignore fn = co.co_filename assert fn.endswith(expected) @@ -332,9 +342,9 @@ class TestSourceParsingAndCompiling: pytest.raises(SyntaxError, _pytest._code.compile, "lambda a,a: 0", mode="eval") -def test_getstartingblock_singleline(): +def test_getstartingblock_singleline() -> None: class A: - def __init__(self, *args): + def __init__(self, *args) -> None: frame = sys._getframe(1) self.source = _pytest._code.Frame(frame).statement @@ -344,22 +354,22 @@ def test_getstartingblock_singleline(): assert len(values) == 1 -def test_getline_finally(): - def c(): +def test_getline_finally() -> None: + def c() -> None: pass with pytest.raises(TypeError) as excinfo: teardown = None try: - c(1) + c(1) # type: ignore finally: if teardown: teardown() source = excinfo.traceback[-1].statement - assert str(source).strip() == "c(1)" + assert str(source).strip() == "c(1) # type: ignore" -def test_getfuncsource_dynamic(): +def test_getfuncsource_dynamic() -> None: source = """ def f(): raise ValueError @@ -368,11 +378,13 @@ def test_getfuncsource_dynamic(): """ co = _pytest._code.compile(source) exec(co, globals()) - assert str(_pytest._code.Source(f)).strip() == "def f():\n raise ValueError" - assert str(_pytest._code.Source(g)).strip() == "def g(): pass" + f_source = _pytest._code.Source(f) # type: ignore + g_source = _pytest._code.Source(g) # type: ignore + assert str(f_source).strip() == "def f():\n raise ValueError" + assert str(g_source).strip() == "def g(): pass" -def test_getfuncsource_with_multine_string(): +def test_getfuncsource_with_multine_string() -> None: def f(): c = """while True: pass @@ -387,7 +399,7 @@ def test_getfuncsource_with_multine_string(): assert str(_pytest._code.Source(f)) == expected.rstrip() -def test_deindent(): +def test_deindent() -> None: from _pytest._code.source import deindent as deindent assert deindent(["\tfoo", "\tbar"]) == ["foo", "bar"] @@ -401,7 +413,7 @@ def test_deindent(): assert lines == ["def f():", " def g():", " pass"] -def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot): +def test_source_of_class_at_eof_without_newline(tmpdir, _sys_snapshot) -> None: # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. source = _pytest._code.Source( @@ -423,7 +435,7 @@ if True: pass -def test_getsource_fallback(): +def test_getsource_fallback() -> None: from _pytest._code.source import getsource expected = """def x(): @@ -432,7 +444,7 @@ def test_getsource_fallback(): assert src == expected -def test_idem_compile_and_getsource(): +def test_idem_compile_and_getsource() -> None: from _pytest._code.source import getsource expected = "def x(): pass" @@ -441,15 +453,16 @@ def test_idem_compile_and_getsource(): assert src == expected -def test_findsource_fallback(): +def test_findsource_fallback() -> None: from _pytest._code.source import findsource src, lineno = findsource(x) + assert src is not None assert "test_findsource_simple" in str(src) assert src[lineno] == " def x():" -def test_findsource(): +def test_findsource() -> None: from _pytest._code.source import findsource co = _pytest._code.compile( @@ -460,19 +473,21 @@ def test_findsource(): ) src, lineno = findsource(co) + assert src is not None assert "if 1:" in str(src) - d = {} + d = {} # type: Dict[str, Any] eval(co, d) src, lineno = findsource(d["x"]) + assert src is not None assert "if 1:" in str(src) assert src[lineno] == " def x():" -def test_getfslineno(): +def test_getfslineno() -> None: from _pytest._code import getfslineno - def f(x): + def f(x) -> None: pass fspath, lineno = getfslineno(f) @@ -498,40 +513,40 @@ def test_getfslineno(): assert getfslineno(B)[1] == -1 -def test_code_of_object_instance_with_call(): +def test_code_of_object_instance_with_call() -> None: class A: pass pytest.raises(TypeError, lambda: _pytest._code.Source(A())) class WithCall: - def __call__(self): + def __call__(self) -> None: pass code = _pytest._code.Code(WithCall()) assert "pass" in str(code.source()) class Hello: - def __call__(self): + def __call__(self) -> None: pass pytest.raises(TypeError, lambda: _pytest._code.Code(Hello)) -def getstatement(lineno, source): +def getstatement(lineno: int, source) -> Source: from _pytest._code.source import getstatementrange_ast - source = _pytest._code.Source(source, deindent=False) - ast, start, end = getstatementrange_ast(lineno, source) - return source[start:end] + src = _pytest._code.Source(source, deindent=False) + ast, start, end = getstatementrange_ast(lineno, src) + return src[start:end] -def test_oneline(): +def test_oneline() -> None: source = getstatement(0, "raise ValueError") assert str(source) == "raise ValueError" -def test_comment_and_no_newline_at_end(): +def test_comment_and_no_newline_at_end() -> None: from _pytest._code.source import getstatementrange_ast source = Source( @@ -545,12 +560,12 @@ def test_comment_and_no_newline_at_end(): assert end == 2 -def test_oneline_and_comment(): +def test_oneline_and_comment() -> None: source = getstatement(0, "raise ValueError\n#hello") assert str(source) == "raise ValueError" -def test_comments(): +def test_comments() -> None: source = '''def test(): "comment 1" x = 1 @@ -576,7 +591,7 @@ comment 4 assert str(getstatement(line, source)) == '"""\ncomment 4\n"""' -def test_comment_in_statement(): +def test_comment_in_statement() -> None: source = """test(foo=1, # comment 1 bar=2) @@ -588,17 +603,17 @@ def test_comment_in_statement(): ) -def test_single_line_else(): +def test_single_line_else() -> None: source = getstatement(1, "if False: 2\nelse: 3") assert str(source) == "else: 3" -def test_single_line_finally(): +def test_single_line_finally() -> None: source = getstatement(1, "try: 1\nfinally: 3") assert str(source) == "finally: 3" -def test_issue55(): +def test_issue55() -> None: source = ( "def round_trip(dinp):\n assert 1 == dinp\n" 'def test_rt():\n round_trip("""\n""")\n' @@ -607,7 +622,7 @@ def test_issue55(): assert str(s) == ' round_trip("""\n""")' -def test_multiline(): +def test_multiline() -> None: source = getstatement( 0, """\ @@ -621,7 +636,7 @@ x = 3 class TestTry: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ try: raise ValueError @@ -631,25 +646,25 @@ else: raise KeyError() """ - def test_body(self): + def test_body(self) -> None: source = getstatement(1, self.source) assert str(source) == " raise ValueError" - def test_except_line(self): + def test_except_line(self) -> None: source = getstatement(2, self.source) assert str(source) == "except Something:" - def test_except_body(self): + def test_except_body(self) -> None: source = getstatement(3, self.source) assert str(source) == " raise IndexError(1)" - def test_else(self): + def test_else(self) -> None: source = getstatement(5, self.source) assert str(source) == " raise KeyError()" class TestTryFinally: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ try: raise ValueError @@ -657,17 +672,17 @@ finally: raise IndexError(1) """ - def test_body(self): + def test_body(self) -> None: source = getstatement(1, self.source) assert str(source) == " raise ValueError" - def test_finally(self): + def test_finally(self) -> None: source = getstatement(3, self.source) assert str(source) == " raise IndexError(1)" class TestIf: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ if 1: y = 3 @@ -677,24 +692,24 @@ else: y = 7 """ - def test_body(self): + def test_body(self) -> None: source = getstatement(1, self.source) assert str(source) == " y = 3" - def test_elif_clause(self): + def test_elif_clause(self) -> None: source = getstatement(2, self.source) assert str(source) == "elif False:" - def test_elif(self): + def test_elif(self) -> None: source = getstatement(3, self.source) assert str(source) == " y = 5" - def test_else(self): + def test_else(self) -> None: source = getstatement(5, self.source) assert str(source) == " y = 7" -def test_semicolon(): +def test_semicolon() -> None: s = """\ hello ; pytest.skip() """ @@ -702,7 +717,7 @@ hello ; pytest.skip() assert str(source) == s.strip() -def test_def_online(): +def test_def_online() -> None: s = """\ def func(): raise ValueError(42) @@ -713,7 +728,7 @@ def something(): assert str(source) == "def func(): raise ValueError(42)" -def XXX_test_expression_multiline(): +def XXX_test_expression_multiline() -> None: source = """\ something ''' @@ -722,7 +737,7 @@ something assert str(result) == "'''\n'''" -def test_getstartingblock_multiline(): +def test_getstartingblock_multiline() -> None: class A: def __init__(self, *args): frame = sys._getframe(1) From 1b4623a6d16a17d228deda23d4242d944dff2397 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Nov 2019 23:41:12 +0100 Subject: [PATCH 142/153] tests: revisit test_cacheprovider --- testing/test_cacheprovider.py | 203 +++++++++------------------------- 1 file changed, 53 insertions(+), 150 deletions(-) diff --git a/testing/test_cacheprovider.py b/testing/test_cacheprovider.py index ea78358d6..2d91e234b 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -2,7 +2,6 @@ import os import shutil import stat import sys -import textwrap import py @@ -65,13 +64,7 @@ class TestNewAPI: mode = os.stat(cache_dir)[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) try: - testdir.makepyfile( - """ - def test_error(): - raise Exception - - """ - ) + testdir.makepyfile("def test_error(): raise Exception") result = testdir.runpytest("-rw") assert result.ret == 1 # warnings from nodeids, lastfailed, and stepwise @@ -178,12 +171,7 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory): "test_cache_reportheader_external_abspath_abs" ) - testdir.makepyfile( - """ - def test_hello(): - pass - """ - ) + testdir.makepyfile("def test_hello(): pass") testdir.makeini( """ [pytest] @@ -192,7 +180,6 @@ def test_cache_reportheader_external_abspath(testdir, tmpdir_factory): abscache=external_cache ) ) - result = testdir.runpytest("-v") result.stdout.fnmatch_lines( ["cachedir: {abscache}".format(abscache=external_cache)] @@ -256,33 +243,23 @@ class TestLastFailed: monkeypatch.setattr("sys.dont_write_bytecode", True) p = testdir.makepyfile( """ - def test_1(): - assert 0 - def test_2(): - assert 0 - def test_3(): - assert 1 - """ + def test_1(): assert 0 + def test_2(): assert 0 + def test_3(): assert 1 + """ ) - result = testdir.runpytest() + result = testdir.runpytest(str(p)) result.stdout.fnmatch_lines(["*2 failed*"]) - p.write( - textwrap.dedent( - """\ - def test_1(): - assert 1 - - def test_2(): - assert 1 - - def test_3(): - assert 0 - """ - ) + p = testdir.makepyfile( + """ + def test_1(): assert 1 + def test_2(): assert 1 + def test_3(): assert 0 + """ ) - result = testdir.runpytest("--lf") + result = testdir.runpytest(str(p), "--lf") result.stdout.fnmatch_lines(["*2 passed*1 desel*"]) - result = testdir.runpytest("--lf") + result = testdir.runpytest(str(p), "--lf") result.stdout.fnmatch_lines( [ "collected 3 items", @@ -290,7 +267,7 @@ class TestLastFailed: "*1 failed*2 passed*", ] ) - result = testdir.runpytest("--lf", "--cache-clear") + result = testdir.runpytest(str(p), "--lf", "--cache-clear") result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) # Run this again to make sure clear-cache is robust @@ -300,21 +277,9 @@ class TestLastFailed: result.stdout.fnmatch_lines(["*1 failed*2 passed*"]) def test_failedfirst_order(self, testdir): - testdir.tmpdir.join("test_a.py").write( - textwrap.dedent( - """\ - def test_always_passes(): - assert 1 - """ - ) - ) - testdir.tmpdir.join("test_b.py").write( - textwrap.dedent( - """\ - def test_always_fails(): - assert 0 - """ - ) + testdir.makepyfile( + test_a="def test_always_passes(): pass", + test_b="def test_always_fails(): assert 0", ) result = testdir.runpytest() # Test order will be collection order; alphabetical @@ -325,16 +290,8 @@ class TestLastFailed: def test_lastfailed_failedfirst_order(self, testdir): testdir.makepyfile( - **{ - "test_a.py": """\ - def test_always_passes(): - assert 1 - """, - "test_b.py": """\ - def test_always_fails(): - assert 0 - """, - } + test_a="def test_always_passes(): assert 1", + test_b="def test_always_fails(): assert 0", ) result = testdir.runpytest() # Test order will be collection order; alphabetical @@ -347,16 +304,11 @@ class TestLastFailed: def test_lastfailed_difference_invocations(self, testdir, monkeypatch): monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( - test_a="""\ - def test_a1(): - assert 0 - def test_a2(): - assert 1 - """, - test_b="""\ - def test_b1(): - assert 0 + test_a=""" + def test_a1(): assert 0 + def test_a2(): assert 1 """, + test_b="def test_b1(): assert 0", ) p = testdir.tmpdir.join("test_a.py") p2 = testdir.tmpdir.join("test_b.py") @@ -365,14 +317,8 @@ class TestLastFailed: result.stdout.fnmatch_lines(["*2 failed*"]) result = testdir.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 failed*"]) - p2.write( - textwrap.dedent( - """\ - def test_b1(): - assert 1 - """ - ) - ) + + testdir.makepyfile(test_b="def test_b1(): assert 1") result = testdir.runpytest("--lf", p2) result.stdout.fnmatch_lines(["*1 passed*"]) result = testdir.runpytest("--lf", p) @@ -381,20 +327,9 @@ class TestLastFailed: def test_lastfailed_usecase_splice(self, testdir, monkeypatch): monkeypatch.setattr("sys.dont_write_bytecode", True) testdir.makepyfile( - """\ - def test_1(): - assert 0 - """ + "def test_1(): assert 0", test_something="def test_2(): assert 0" ) p2 = testdir.tmpdir.join("test_something.py") - p2.write( - textwrap.dedent( - """\ - def test_2(): - assert 0 - """ - ) - ) result = testdir.runpytest() result.stdout.fnmatch_lines(["*2 failed*"]) result = testdir.runpytest("--lf", p2) @@ -436,18 +371,14 @@ class TestLastFailed: def test_terminal_report_lastfailed(self, testdir): test_a = testdir.makepyfile( test_a=""" - def test_a1(): - pass - def test_a2(): - pass + def test_a1(): pass + def test_a2(): pass """ ) test_b = testdir.makepyfile( test_b=""" - def test_b1(): - assert 0 - def test_b2(): - assert 0 + def test_b1(): assert 0 + def test_b2(): assert 0 """ ) result = testdir.runpytest() @@ -492,10 +423,8 @@ class TestLastFailed: def test_terminal_report_failedfirst(self, testdir): testdir.makepyfile( test_a=""" - def test_a1(): - assert 0 - def test_a2(): - pass + def test_a1(): assert 0 + def test_a2(): pass """ ) result = testdir.runpytest() @@ -542,7 +471,6 @@ class TestLastFailed: assert list(lastfailed) == ["test_maybe.py::test_hello"] def test_lastfailed_failure_subset(self, testdir, monkeypatch): - testdir.makepyfile( test_maybe=""" import os @@ -560,6 +488,7 @@ class TestLastFailed: env = os.environ if '1' == env['FAILIMPORT']: raise ImportError('fail') + def test_hello(): assert '0' == env['FAILTEST'] @@ -613,8 +542,7 @@ class TestLastFailed: """ import pytest @pytest.mark.xfail - def test(): - assert 0 + def test(): assert 0 """ ) result = testdir.runpytest() @@ -626,8 +554,7 @@ class TestLastFailed: """ import pytest @pytest.mark.xfail(strict=True) - def test(): - pass + def test(): pass """ ) result = testdir.runpytest() @@ -641,8 +568,7 @@ class TestLastFailed: testdir.makepyfile( """ import pytest - def test(): - assert 0 + def test(): assert 0 """ ) result = testdir.runpytest() @@ -655,8 +581,7 @@ class TestLastFailed: """ import pytest @pytest.{mark} - def test(): - assert 0 + def test(): assert 0 """.format( mark=mark ) @@ -694,18 +619,14 @@ class TestLastFailed: # 1. initial run test_bar = testdir.makepyfile( test_bar=""" - def test_bar_1(): - pass - def test_bar_2(): - assert 0 + def test_bar_1(): pass + def test_bar_2(): assert 0 """ ) test_foo = testdir.makepyfile( test_foo=""" - def test_foo_3(): - pass - def test_foo_4(): - assert 0 + def test_foo_3(): pass + def test_foo_4(): assert 0 """ ) testdir.runpytest() @@ -717,10 +638,8 @@ class TestLastFailed: # 2. fix test_bar_2, run only test_bar.py testdir.makepyfile( test_bar=""" - def test_bar_1(): - pass - def test_bar_2(): - pass + def test_bar_1(): pass + def test_bar_2(): pass """ ) result = testdir.runpytest(test_bar) @@ -735,10 +654,8 @@ class TestLastFailed: # 3. fix test_foo_4, run only test_foo.py test_foo = testdir.makepyfile( test_foo=""" - def test_foo_3(): - pass - def test_foo_4(): - pass + def test_foo_3(): pass + def test_foo_4(): pass """ ) result = testdir.runpytest(test_foo, "--last-failed") @@ -752,10 +669,8 @@ class TestLastFailed: def test_lastfailed_no_failures_behavior_all_passed(self, testdir): testdir.makepyfile( """ - def test_1(): - assert True - def test_2(): - assert True + def test_1(): pass + def test_2(): pass """ ) result = testdir.runpytest() @@ -777,10 +692,8 @@ class TestLastFailed: def test_lastfailed_no_failures_behavior_empty_cache(self, testdir): testdir.makepyfile( """ - def test_1(): - assert True - def test_2(): - assert False + def test_1(): pass + def test_2(): assert 0 """ ) result = testdir.runpytest("--lf", "--cache-clear") @@ -1022,22 +935,12 @@ class TestReadme: return readme.is_file() def test_readme_passed(self, testdir): - testdir.makepyfile( - """ - def test_always_passes(): - assert 1 - """ - ) + testdir.makepyfile("def test_always_passes(): pass") testdir.runpytest() assert self.check_readme(testdir) is True def test_readme_failed(self, testdir): - testdir.makepyfile( - """ - def test_always_fails(): - assert 0 - """ - ) + testdir.makepyfile("def test_always_fails(): assert 0") testdir.runpytest() assert self.check_readme(testdir) is True From b9a3ba1fe8a02b5093ad72785ab5d908e18e228c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 15 Nov 2019 23:43:54 +0100 Subject: [PATCH 143/153] test_cache_writefail_permissions: ignore any other plugins --- 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 2d91e234b..0e1194b02 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -59,7 +59,8 @@ class TestNewAPI: @pytest.mark.filterwarnings( "ignore:could not create cache path:pytest.PytestWarning" ) - def test_cache_failure_warns(self, testdir): + def test_cache_failure_warns(self, testdir, monkeypatch): + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") cache_dir = str(testdir.tmpdir.ensure_dir(".pytest_cache")) mode = os.stat(cache_dir)[stat.ST_MODE] testdir.tmpdir.ensure_dir(".pytest_cache").chmod(0) From f3a10245d0cfbad21c8c49d1ce7227afbcb0d716 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 16:20:41 +0100 Subject: [PATCH 144/153] Metafunc: remove unused _ids Forgotten in 40b85d7ee. --- src/_pytest/python.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 30853de94..2ce2f3f3a 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -914,7 +914,6 @@ class Metafunc: self.cls = cls self._calls = [] - self._ids = set() self._arg2fixturedefs = fixtureinfo.name2fixturedefs @property From 91dec8e2bf5e4e9c43b040a8b2f286c8275f8141 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 16:35:34 +0100 Subject: [PATCH 145/153] Factor out _validate_parametrize_spelling This makes it easier to read `pytest_generate_tests`. --- src/_pytest/python.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 30853de94..f7436f23b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -119,14 +119,17 @@ def pytest_cmdline_main(config): return 0 -def pytest_generate_tests(metafunc): - # those alternative spellings are common - raise a specific error to alert - # the user - alt_spellings = ["parameterize", "parametrise", "parameterise"] - for mark_name in alt_spellings: +def _validate_parametrize_spelling(metafunc): + """Raise a specific error for common misspellings of "parametrize".""" + for mark_name in ["parameterize", "parametrise", "parameterise"]: if metafunc.definition.get_closest_marker(mark_name): msg = "{0} has '{1}' mark, spelling should be 'parametrize'" fail(msg.format(metafunc.function.__name__, mark_name), pytrace=False) + + +def pytest_generate_tests(metafunc): + _validate_parametrize_spelling(metafunc) + for marker in metafunc.definition.iter_markers(name="parametrize"): metafunc.parametrize(*marker.args, **marker.kwargs) From b461010f32bb60cca1718a0542a4426f73a61758 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 18:12:13 +0100 Subject: [PATCH 146/153] mypy: config: use mypy_path=src MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows for checking files inside of "testing" without having "src/…" as an argument also. --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 0c0cb4861..42d5b9460 100644 --- a/setup.cfg +++ b/setup.cfg @@ -63,6 +63,7 @@ ignore = formats = sdist.tgz,bdist_wheel [mypy] +mypy_path = src ignore_missing_imports = True no_implicit_optional = True strict_equality = True From 2ad2fbc9a222f3ab2cab5379972c40d3f5ad2db1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 17:36:15 +0100 Subject: [PATCH 147/153] Metafunc: remove hack for DefinitionMock Done initially in 99015bfc8. --- src/_pytest/python.py | 16 ++++++++++------ testing/python/metafunc.py | 4 ++-- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ebc6895f2..2e8756289 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -9,6 +9,7 @@ from collections import Counter from collections.abc import Sequence from functools import partial from textwrap import dedent +from typing import List from typing import Tuple import py @@ -894,11 +895,14 @@ class Metafunc: test function is defined. """ - def __init__(self, definition, fixtureinfo, config, cls=None, module=None): - assert ( - isinstance(definition, FunctionDefinition) - or type(definition).__name__ == "DefinitionMock" - ) + def __init__( + self, + definition: "FunctionDefinition", + fixtureinfo, + config, + cls=None, + module=None, + ) -> None: self.definition = definition #: access to the :class:`_pytest.config.Config` object for the test session @@ -916,7 +920,7 @@ class Metafunc: #: class object where the test function is defined in or ``None``. self.cls = cls - self._calls = [] + self._calls = [] # type: List[CallSpec2] self._arg2fixturedefs = fixtureinfo.name2fixturedefs @property diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 0c3c2aed3..1c396c4a7 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -12,7 +12,7 @@ from _pytest import python class TestMetafunc: - def Metafunc(self, func, config=None): + def Metafunc(self, func, config=None) -> python.Metafunc: # the unit tests of this class check if things work correctly # on the funcarg level, so we don't need a full blown # initialization @@ -23,7 +23,7 @@ class TestMetafunc: self.names_closure = names @attr.s - class DefinitionMock: + class DefinitionMock(python.FunctionDefinition): obj = attr.ib() names = fixtures.getfuncargnames(func) From f9feef6808c250b0c98d7d35580e3fad75d17439 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 23:13:21 +0100 Subject: [PATCH 148/153] Revert "ci: use tox -vv" `tox -vv` is too verbose, and was only used as a hack to get the output of durations. As for information in logs `-v` could be used maybe still, but I've decided to revert it for now. This reverts commit 56cec5fa79106c0e8c02eb34bd8e5768ec52044d. --- .travis.yml | 2 +- azure-pipelines.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 310d7093b..e3edbfe9b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -108,7 +108,7 @@ before_script: export _PYTEST_TOX_EXTRA_DEP=coverage-enable-subprocess fi -script: tox -vv +script: tox after_success: - | diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 2ee1604a7..f18ce0887 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -57,7 +57,7 @@ jobs: export COVERAGE_FILE="$PWD/.coverage" export COVERAGE_PROCESS_START="$PWD/.coveragerc" fi - python -m tox -e $(tox.env) -vv + python -m tox -e $(tox.env) displayName: 'Run tests' - task: PublishTestResults@2 From f38f2d402e183130222993f501d92eefe0d398dc Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Mon, 18 Nov 2019 20:26:31 +0100 Subject: [PATCH 149/153] minor: visit_Assert: move setting of `negation` out of branches --- src/_pytest/assertion/rewrite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 2f9ca6de0..51ea1801b 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -807,8 +807,9 @@ class AssertionRewriter(ast.NodeVisitor): ) ) + negation = ast.UnaryOp(ast.Not(), top_condition) + if self.enable_assertion_pass_hook: # Experimental pytest_assertion_pass hook - negation = ast.UnaryOp(ast.Not(), top_condition) msg = self.pop_format_context(ast.Str(explanation)) # Failed @@ -860,7 +861,6 @@ class AssertionRewriter(ast.NodeVisitor): else: # Original assertion rewriting # Create failure message. body = self.expl_stmts - negation = ast.UnaryOp(ast.Not(), top_condition) self.statements.append(ast.If(negation, body, [])) if assert_.msg: assertmsg = self.helper("_format_assertmsg", assert_.msg) From 4804d4bc9824f7a6fb0153a25e896627dec37b3b Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 19 Nov 2019 02:27:04 +0100 Subject: [PATCH 150/153] python: remove unused pytest_make_parametrize_id hookimpl Added in 79927428d initially, but never used. --- src/_pytest/python.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2e8756289..4702e0659 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -241,10 +241,6 @@ def pytest_pycollect_makeitem(collector, name, obj): outcome.force_result(res) -def pytest_make_parametrize_id(config, val, argname=None): - return None - - class PyobjContext: module = pyobj_property("Module") cls = pyobj_property("Class") From 4ad61cbcf6063d5bc414a9a37a5fbb29a3083e73 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 19 Nov 2019 15:58:46 +0100 Subject: [PATCH 151/153] Improve check for misspelling of parametrize - there is no need to do this with `--strict-markers` - it can be done when looking up marks, instead of for every generated test --- changelog/6231.improvement.rst | 1 + src/_pytest/mark/structures.py | 19 ++++++++++++------- src/_pytest/python.py | 10 ---------- testing/python/metafunc.py | 20 ++++++++++++-------- 4 files changed, 25 insertions(+), 25 deletions(-) create mode 100644 changelog/6231.improvement.rst diff --git a/changelog/6231.improvement.rst b/changelog/6231.improvement.rst new file mode 100644 index 000000000..1554a229b --- /dev/null +++ b/changelog/6231.improvement.rst @@ -0,0 +1 @@ +Improve check for misspelling of ``pytest.mark.parametrize``. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 18ebc506a..3002f8abc 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -314,13 +314,18 @@ class MarkGenerator: "{!r} not found in `markers` configuration option".format(name), pytrace=False, ) - else: - warnings.warn( - "Unknown pytest.mark.%s - is this a typo? You can register " - "custom marks to avoid this warning - for details, see " - "https://docs.pytest.org/en/latest/mark.html" % name, - PytestUnknownMarkWarning, - ) + + # Raise a specific error for common misspellings of "parametrize". + if name in ["parameterize", "parametrise", "parameterise"]: + __tracebackhide__ = True + fail("Unknown '{}' mark, did you mean 'parametrize'?".format(name)) + + warnings.warn( + "Unknown pytest.mark.%s - is this a typo? You can register " + "custom marks to avoid this warning - for details, see " + "https://docs.pytest.org/en/latest/mark.html" % name, + PytestUnknownMarkWarning, + ) return MarkDecorator(Mark(name, (), {})) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 2e8756289..cb6c93159 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -120,17 +120,7 @@ def pytest_cmdline_main(config): return 0 -def _validate_parametrize_spelling(metafunc): - """Raise a specific error for common misspellings of "parametrize".""" - for mark_name in ["parameterize", "parametrise", "parameterise"]: - if metafunc.definition.get_closest_marker(mark_name): - msg = "{0} has '{1}' mark, spelling should be 'parametrize'" - fail(msg.format(metafunc.function.__name__, mark_name), pytrace=False) - - def pytest_generate_tests(metafunc): - _validate_parametrize_spelling(metafunc) - for marker in metafunc.definition.iter_markers(name="parametrize"): metafunc.parametrize(*marker.args, **marker.kwargs) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 1c396c4a7..65855f724 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1323,25 +1323,29 @@ class TestMetafuncFunctional: reprec = testdir.runpytest() reprec.assert_outcomes(passed=4) - @pytest.mark.parametrize("attr", ["parametrise", "parameterize", "parameterise"]) - def test_parametrize_misspelling(self, testdir, attr): + def test_parametrize_misspelling(self, testdir): """#463""" testdir.makepyfile( """ import pytest - @pytest.mark.{}("x", range(2)) + @pytest.mark.parametrise("x", range(2)) def test_foo(x): pass - """.format( - attr - ) + """ ) result = testdir.runpytest("--collectonly") result.stdout.fnmatch_lines( [ - "test_foo has '{}' mark, spelling should be 'parametrize'".format(attr), - "*1 error in*", + "collected 0 items / 1 error", + "", + "*= ERRORS =*", + "*_ ERROR collecting test_parametrize_misspelling.py _*", + "test_parametrize_misspelling.py:3: in ", + ' @pytest.mark.parametrise("x", range(2))', + "E Failed: Unknown 'parametrise' mark, did you mean 'parametrize'?", + "*! Interrupted: 1 error during collection !*", + "*= 1 error in *", ] ) From 4b16b93cf57bb58d6dd55fb4b4ffa2a0a2a344db Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 19 Nov 2019 12:43:51 -0300 Subject: [PATCH 152/153] Preparing release version 5.3.0 --- CHANGELOG.rst | 202 ++++++++++++++++++++++++++++++ changelog/2049.bugfix.rst | 1 - changelog/2548.bugfix.rst | 1 - changelog/4488.feature.rst | 10 -- changelog/4730.feature.rst | 3 - changelog/4901.trivial.rst | 2 - changelog/5061.improvement.rst | 1 - changelog/5515.feature.rst | 11 -- changelog/5630.improvement.rst | 1 - changelog/5914.feature.rst | 19 --- changelog/5924.improvement.rst | 34 ----- changelog/5936.improvement.rst | 1 - changelog/5990.improvement.rst | 1 - changelog/6008.improvement.rst | 2 - changelog/6023.improvement.rst | 1 - changelog/6026.improvement.rst | 1 - changelog/6039.bugfix.rst | 3 - changelog/6047.bugfix.rst | 1 - changelog/6057.feature.rst | 3 - changelog/6059.improvement.rst | 1 - changelog/6061.feature.rst | 4 - changelog/6069.improvement.rst | 1 - changelog/6074.bugfix.rst | 1 - changelog/6097.improvement.rst | 1 - changelog/6116.improvement.rst | 1 - changelog/6148.improvement.rst | 1 - changelog/6152.improvement.rst | 1 - changelog/6176.improvement.rst | 1 - changelog/6179.deprecation.rst | 7 -- changelog/6181.improvement.rst | 1 - changelog/6189.bugfix.rst | 1 - changelog/6206.improvement.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-5.3.0.rst | 45 +++++++ doc/en/example/markers.rst | 2 +- doc/en/example/parametrize.rst | 9 +- doc/en/example/simple.rst | 2 +- doc/en/usage.rst | 4 +- doc/en/warnings.rst | 4 +- doc/en/writing_plugins.rst | 2 +- 40 files changed, 260 insertions(+), 129 deletions(-) delete mode 100644 changelog/2049.bugfix.rst delete mode 100644 changelog/2548.bugfix.rst delete mode 100644 changelog/4488.feature.rst delete mode 100644 changelog/4730.feature.rst delete mode 100644 changelog/4901.trivial.rst delete mode 100644 changelog/5061.improvement.rst delete mode 100644 changelog/5515.feature.rst delete mode 100644 changelog/5630.improvement.rst delete mode 100644 changelog/5914.feature.rst delete mode 100644 changelog/5924.improvement.rst delete mode 100644 changelog/5936.improvement.rst delete mode 100644 changelog/5990.improvement.rst delete mode 100644 changelog/6008.improvement.rst delete mode 100644 changelog/6023.improvement.rst delete mode 100644 changelog/6026.improvement.rst delete mode 100644 changelog/6039.bugfix.rst delete mode 100644 changelog/6047.bugfix.rst delete mode 100644 changelog/6057.feature.rst delete mode 100644 changelog/6059.improvement.rst delete mode 100644 changelog/6061.feature.rst delete mode 100644 changelog/6069.improvement.rst delete mode 100644 changelog/6074.bugfix.rst delete mode 100644 changelog/6097.improvement.rst delete mode 100644 changelog/6116.improvement.rst delete mode 100644 changelog/6148.improvement.rst delete mode 100644 changelog/6152.improvement.rst delete mode 100644 changelog/6176.improvement.rst delete mode 100644 changelog/6179.deprecation.rst delete mode 100644 changelog/6181.improvement.rst delete mode 100644 changelog/6189.bugfix.rst delete mode 100644 changelog/6206.improvement.rst create mode 100644 doc/en/announce/release-5.3.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e9ac09c8e..76b6caf1b 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,208 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 5.3.0 (2019-11-19) +========================= + +Deprecations +------------ + +- `#6179 `_: The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given + that this is the version supported by default in modern tools that manipulate this type of file. + + In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option + is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``. + + For more information, `see the docs `__. + + + +Features +-------- + +- `#4488 `_: The pytest team has created the `pytest-reportlog `__ + plugin, which provides a new ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. + + Each line of the report log contains a self contained JSON object corresponding to a testing event, + such as a collection or a test result report. The file is guaranteed to be flushed after writing + each line, so systems can read and process events in real-time. + + The plugin is meant to replace the ``--resultlog`` option, which is deprecated and meant to be removed + in a future release. If you use ``--resultlog``, please try out ``pytest-reportlog`` and + provide feedback. + + +- `#4730 `_: When ``sys.pycache_prefix`` (Python 3.8+) is set, it will be used by pytest to cache test files changed by the assertion rewriting mechanism. + + This makes it easier to benefit of cached ``.pyc`` files even on file systems without permissions. + + +- `#5515 `_: Allow selective auto-indentation of multiline log messages. + + Adds command line option ``--log-auto-indent``, config option + ``log_auto_indent`` and support for per-entry configuration of + indentation behavior on calls to ``logging.log()``. + + Alters the default for auto-indention from ``on`` to ``off``. This + restores the older behavior that existed prior to v4.6.0. This + reversion to earlier behavior was done because it is better to + activate new features that may lead to broken tests explicitly + rather than implicitly. + + +- `#5914 `_: ``pytester`` learned two new functions, `no_fnmatch_line `_ and + `no_re_match_line `_. + + The functions are used to ensure the captured text *does not* match the given + pattern. + + The previous idiom was to use ``re.match``: + + .. code-block:: python + + assert re.match(pat, result.stdout.str()) is None + + Or the ``in`` operator: + + .. code-block:: python + + assert text in result.stdout.str() + + But the new functions produce best output on failure. + + +- `#6057 `_: Add tolerances to complex values when printing ``pytest.approx``. + + For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. + + +- `#6061 `_: Adding the pluginmanager as an option ``pytest_addoption`` + so that hooks can be invoked when setting up command line options. This is + useful for having one plugin communicate things to another plugin, + such as default values or which set of command line options to add. + + + +Improvements +------------ + +- `#5061 `_: Use multiple colors with terminal summary statistics. + + +- `#5630 `_: Quitting from debuggers is now properly handled in ``doctest`` items. + + +- `#5924 `_: Improve verbose diff output with sequences. + + Before: + + .. code-block:: + + E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] + E Right contains 3 more items, first extra item: ' ' + E Full diff: + E - ['version', 'version_info', 'sys.version', 'sys.version_info'] + E + ['version', + E + 'version_info', + E + 'sys.version', + E + 'sys.version_info', + E + ' ', + E + 'sys.version', + E + 'sys.version_info'] + + After: + + .. code-block:: + + E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] + E Right contains 3 more items, first extra item: ' ' + E Full diff: + E [ + E 'version', + E 'version_info', + E 'sys.version', + E 'sys.version_info', + E + ' ', + E + 'sys.version', + E + 'sys.version_info', + E ] + + +- `#5936 `_: Display untruncated assertion message with ``-vv``. + + +- `#5990 `_: Fix plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). + + +- `#6008 `_: ``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be + immutable and avoid accidental modifications. + + +- `#6023 `_: ``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). + + +- `#6026 `_: Align prefixes in output of pytester's ``LineMatcher``. + + +- `#6059 `_: Collection errors are reported as errors (and not failures like before) in the terminal's short test summary. + + +- `#6069 `_: ``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. + + +- `#6097 `_: The "[XXX%]" indicator in the test summary is colored according to the final (new) multi-colored line's main color. + + +- `#6116 `_: Add ``--co`` as a synonym to ``--collect-only``. + + +- `#6148 `_: ``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix. + + +- `#6152 `_: Now parametrization will use the ``__name__`` attribute of any object for the id, if present. Previously it would only use ``__name__`` for functions and classes. + + +- `#6176 `_: Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``. + + +- `#6181 `_: The reason for a stopped session, e.g. with ``--maxfail`` / ``-x`` gets reported. + + +- `#6206 `_: cacheprovider: improved robustness and performance with ``cache.set``. + + + +Bug Fixes +--------- + +- `#2049 `_: Fix ``-setup-plan`` showing inaccurate information about fixture lifetimes. + + +- `#2548 `_: Fix line offset mismatch with skipped tests in terminal summary. + + +- `#6039 `_: The ``PytestDoctestRunner`` is properly invalidated when unconfiguring the doctest plugin. + + This is important when used with ``pytester``'s ``runpytest_inprocess``. + + +- `#6047 `_: BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. + + +- `#6074 `_: pytester: fix order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. + + +- `#6189 `_: Fix incorrect result of ``getmodpath`` method. + + + +Trivial/Internal Changes +------------------------ + +- `#4901 `_: ``RunResult`` from ``pytester`` now displays the mnemonic of the ``ret`` attribute when it is a + valid ``pytest.ExitCode`` value. + + pytest 5.2.4 (2019-11-15) ========================= diff --git a/changelog/2049.bugfix.rst b/changelog/2049.bugfix.rst deleted file mode 100644 index 395396bd3..000000000 --- a/changelog/2049.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``-setup-plan`` showing inaccurate information about fixture lifetimes. diff --git a/changelog/2548.bugfix.rst b/changelog/2548.bugfix.rst deleted file mode 100644 index 8ee3b6462..000000000 --- a/changelog/2548.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix line offset mismatch with skipped tests in terminal summary. diff --git a/changelog/4488.feature.rst b/changelog/4488.feature.rst deleted file mode 100644 index 1e0387f44..000000000 --- a/changelog/4488.feature.rst +++ /dev/null @@ -1,10 +0,0 @@ -The pytest team has created the `pytest-reportlog `__ -plugin, which provides a new ``--report-log=FILE`` option that writes *report logs* into a file as the test session executes. - -Each line of the report log contains a self contained JSON object corresponding to a testing event, -such as a collection or a test result report. The file is guaranteed to be flushed after writing -each line, so systems can read and process events in real-time. - -The plugin is meant to replace the ``--resultlog`` option, which is deprecated and meant to be removed -in a future release. If you use ``--resultlog``, please try out ``pytest-reportlog`` and -provide feedback. diff --git a/changelog/4730.feature.rst b/changelog/4730.feature.rst deleted file mode 100644 index 80d1c4a38..000000000 --- a/changelog/4730.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -When ``sys.pycache_prefix`` (Python 3.8+) is set, it will be used by pytest to cache test files changed by the assertion rewriting mechanism. - -This makes it easier to benefit of cached ``.pyc`` files even on file systems without permissions. diff --git a/changelog/4901.trivial.rst b/changelog/4901.trivial.rst deleted file mode 100644 index f6609ddf1..000000000 --- a/changelog/4901.trivial.rst +++ /dev/null @@ -1,2 +0,0 @@ -``RunResult`` from ``pytester`` now displays the mnemonic of the ``ret`` attribute when it is a -valid ``pytest.ExitCode`` value. diff --git a/changelog/5061.improvement.rst b/changelog/5061.improvement.rst deleted file mode 100644 index 9eb0c1cd3..000000000 --- a/changelog/5061.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Use multiple colors with terminal summary statistics. diff --git a/changelog/5515.feature.rst b/changelog/5515.feature.rst deleted file mode 100644 index b53097c43..000000000 --- a/changelog/5515.feature.rst +++ /dev/null @@ -1,11 +0,0 @@ -Allow selective auto-indentation of multiline log messages. - -Adds command line option ``--log-auto-indent``, config option -``log_auto_indent`` and support for per-entry configuration of -indentation behavior on calls to ``logging.log()``. - -Alters the default for auto-indention from ``on`` to ``off``. This -restores the older behavior that existed prior to v4.6.0. This -reversion to earlier behavior was done because it is better to -activate new features that may lead to broken tests explicitly -rather than implicitly. diff --git a/changelog/5630.improvement.rst b/changelog/5630.improvement.rst deleted file mode 100644 index 45d49bdae..000000000 --- a/changelog/5630.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Quitting from debuggers is now properly handled in ``doctest`` items. diff --git a/changelog/5914.feature.rst b/changelog/5914.feature.rst deleted file mode 100644 index 68cd66f99..000000000 --- a/changelog/5914.feature.rst +++ /dev/null @@ -1,19 +0,0 @@ -``pytester`` learned two new functions, `no_fnmatch_line `_ and -`no_re_match_line `_. - -The functions are used to ensure the captured text *does not* match the given -pattern. - -The previous idiom was to use ``re.match``: - -.. code-block:: python - - assert re.match(pat, result.stdout.str()) is None - -Or the ``in`` operator: - -.. code-block:: python - - assert text in result.stdout.str() - -But the new functions produce best output on failure. diff --git a/changelog/5924.improvement.rst b/changelog/5924.improvement.rst deleted file mode 100644 index a03eb4704..000000000 --- a/changelog/5924.improvement.rst +++ /dev/null @@ -1,34 +0,0 @@ -Improve verbose diff output with sequences. - -Before: - -.. code-block:: - - E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] - E Right contains 3 more items, first extra item: ' ' - E Full diff: - E - ['version', 'version_info', 'sys.version', 'sys.version_info'] - E + ['version', - E + 'version_info', - E + 'sys.version', - E + 'sys.version_info', - E + ' ', - E + 'sys.version', - E + 'sys.version_info'] - -After: - -.. code-block:: - - E AssertionError: assert ['version', '...version_info'] == ['version', '...version', ...] - E Right contains 3 more items, first extra item: ' ' - E Full diff: - E [ - E 'version', - E 'version_info', - E 'sys.version', - E 'sys.version_info', - E + ' ', - E + 'sys.version', - E + 'sys.version_info', - E ] diff --git a/changelog/5936.improvement.rst b/changelog/5936.improvement.rst deleted file mode 100644 index c5cd924bb..000000000 --- a/changelog/5936.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Display untruncated assertion message with ``-vv``. diff --git a/changelog/5990.improvement.rst b/changelog/5990.improvement.rst deleted file mode 100644 index 6f5ad648e..000000000 --- a/changelog/5990.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Fix plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). diff --git a/changelog/6008.improvement.rst b/changelog/6008.improvement.rst deleted file mode 100644 index 22ef35cc8..000000000 --- a/changelog/6008.improvement.rst +++ /dev/null @@ -1,2 +0,0 @@ -``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be -immutable and avoid accidental modifications. diff --git a/changelog/6023.improvement.rst b/changelog/6023.improvement.rst deleted file mode 100644 index 6cf81002e..000000000 --- a/changelog/6023.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). diff --git a/changelog/6026.improvement.rst b/changelog/6026.improvement.rst deleted file mode 100644 index 34dfb278d..000000000 --- a/changelog/6026.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Align prefixes in output of pytester's ``LineMatcher``. diff --git a/changelog/6039.bugfix.rst b/changelog/6039.bugfix.rst deleted file mode 100644 index b13a677c8..000000000 --- a/changelog/6039.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -The ``PytestDoctestRunner`` is properly invalidated when unconfiguring the doctest plugin. - -This is important when used with ``pytester``'s ``runpytest_inprocess``. diff --git a/changelog/6047.bugfix.rst b/changelog/6047.bugfix.rst deleted file mode 100644 index 11a997f71..000000000 --- a/changelog/6047.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. diff --git a/changelog/6057.feature.rst b/changelog/6057.feature.rst deleted file mode 100644 index b7334e7fe..000000000 --- a/changelog/6057.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Add tolerances to complex values when printing ``pytest.approx``. - -For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. diff --git a/changelog/6059.improvement.rst b/changelog/6059.improvement.rst deleted file mode 100644 index 39ffff99b..000000000 --- a/changelog/6059.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Collection errors are reported as errors (and not failures like before) in the terminal's short test summary. diff --git a/changelog/6061.feature.rst b/changelog/6061.feature.rst deleted file mode 100644 index 11f548625..000000000 --- a/changelog/6061.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Adding the pluginmanager as an option ``pytest_addoption`` -so that hooks can be invoked when setting up command line options. This is -useful for having one plugin communicate things to another plugin, -such as default values or which set of command line options to add. diff --git a/changelog/6069.improvement.rst b/changelog/6069.improvement.rst deleted file mode 100644 index e60d154bb..000000000 --- a/changelog/6069.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. diff --git a/changelog/6074.bugfix.rst b/changelog/6074.bugfix.rst deleted file mode 100644 index 624cf5d1c..000000000 --- a/changelog/6074.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -pytester: fix order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. diff --git a/changelog/6097.improvement.rst b/changelog/6097.improvement.rst deleted file mode 100644 index 32eb84906..000000000 --- a/changelog/6097.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -The "[XXX%]" indicator in the test summary is colored according to the final (new) multi-colored line's main color. diff --git a/changelog/6116.improvement.rst b/changelog/6116.improvement.rst deleted file mode 100644 index 4fc96ec77..000000000 --- a/changelog/6116.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``--co`` as a synonym to ``--collect-only``. diff --git a/changelog/6148.improvement.rst b/changelog/6148.improvement.rst deleted file mode 100644 index 3d77ab528..000000000 --- a/changelog/6148.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix. diff --git a/changelog/6152.improvement.rst b/changelog/6152.improvement.rst deleted file mode 100644 index 8e5f4d52a..000000000 --- a/changelog/6152.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Now parametrization will use the ``__name__`` attribute of any object for the id, if present. Previously it would only use ``__name__`` for functions and classes. diff --git a/changelog/6176.improvement.rst b/changelog/6176.improvement.rst deleted file mode 100644 index 39787da2e..000000000 --- a/changelog/6176.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``. diff --git a/changelog/6179.deprecation.rst b/changelog/6179.deprecation.rst deleted file mode 100644 index 97f7ec74b..000000000 --- a/changelog/6179.deprecation.rst +++ /dev/null @@ -1,7 +0,0 @@ -The default value of ``junit_family`` option will change to ``xunit2`` in pytest 6.0, given -that this is the version supported by default in modern tools that manipulate this type of file. - -In order to smooth the transition, pytest will issue a warning in case the ``--junitxml`` option -is given in the command line but ``junit_family`` is not explicitly configured in ``pytest.ini``. - -For more information, `see the docs `__. diff --git a/changelog/6181.improvement.rst b/changelog/6181.improvement.rst deleted file mode 100644 index 0960f6203..000000000 --- a/changelog/6181.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -The reason for a stopped session, e.g. with ``--maxfail`` / ``-x`` gets reported. diff --git a/changelog/6189.bugfix.rst b/changelog/6189.bugfix.rst deleted file mode 100644 index 060a2260a..000000000 --- a/changelog/6189.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix incorrect result of ``getmodpath`` method. diff --git a/changelog/6206.improvement.rst b/changelog/6206.improvement.rst deleted file mode 100644 index 67d8363b3..000000000 --- a/changelog/6206.improvement.rst +++ /dev/null @@ -1 +0,0 @@ -cacheprovider: improved robustness and performance with ``cache.set``. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index e7c011411..6e6914f2d 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-5.3.0 release-5.2.4 release-5.2.3 release-5.2.2 diff --git a/doc/en/announce/release-5.3.0.rst b/doc/en/announce/release-5.3.0.rst new file mode 100644 index 000000000..9855a7a2d --- /dev/null +++ b/doc/en/announce/release-5.3.0.rst @@ -0,0 +1,45 @@ +pytest-5.3.0 +======================================= + +The pytest team is proud to announce the 5.3.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: + +* AnjoMan +* Anthony Sottile +* Anton Lodder +* Bruno Oliveira +* Daniel Hahler +* Gregory Lee +* Josh Karpel +* JoshKarpel +* Joshua Storck +* Kale Kundert +* MarcoGorelli +* Michael Krebs +* NNRepos +* Ran Benita +* TH3CHARLie +* Tibor Arpas +* Zac Hatfield-Dodds +* 林玮 + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/example/markers.rst b/doc/en/example/markers.rst index ccddb1f66..8143b3fd4 100644 --- a/doc/en/example/markers.rst +++ b/doc/en/example/markers.rst @@ -622,7 +622,7 @@ then you will see two tests skipped and two executed tests as expected: test_plat.py s.s. [100%] ========================= short test summary info ========================== - SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:13: cannot run on platform linux + SKIPPED [2] $REGENDOC_TMPDIR/conftest.py:12: cannot run on platform linux ======================= 2 passed, 2 skipped in 0.12s ======================= Note that if you specify a platform via the marker-command line option like this: diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index 0e131dace..c420761a4 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -475,10 +475,11 @@ Running it results in some skips if we don't have all the python interpreters in .. code-block:: pytest . $ pytest -rs -q multipython.py - ssssssssssss......sss...... [100%] + ssssssssssss...ssssssssssss [100%] ========================= short test summary info ========================== - SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.5' not found - 12 passed, 15 skipped in 0.12s + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.5' not found + SKIPPED [12] $REGENDOC_TMPDIR/CWD/multipython.py:29: 'python3.7' not found + 3 passed, 24 skipped in 0.12s Indirect parametrization of optional implementations/imports -------------------------------------------------------------------- @@ -546,7 +547,7 @@ If you run this with reporting for skips enabled: test_module.py .s [100%] ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:13: could not import 'opt2': No module named 'opt2' + SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:12: could not import 'opt2': No module named 'opt2' ======================= 1 passed, 1 skipped in 0.12s ======================= You'll see that we don't have an ``opt2`` module and thus the second test run diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 05ccbc9b2..1570850fc 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -443,7 +443,7 @@ Now we can profile which test functions execute the slowest: ========================= slowest 3 test durations ========================= 0.30s call test_some_are_slow.py::test_funcslow2 0.20s call test_some_are_slow.py::test_funcslow1 - 0.10s call test_some_are_slow.py::test_funcfast + 0.11s call test_some_are_slow.py::test_funcfast ============================ 3 passed in 0.12s ============================= incremental testing - test steps diff --git a/doc/en/usage.rst b/doc/en/usage.rst index ea849c1a7..245a67b68 100644 --- a/doc/en/usage.rst +++ b/doc/en/usage.rst @@ -241,7 +241,7 @@ Example: test_example.py:14: AssertionError ========================= short test summary info ========================== - SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test XFAIL test_example.py::test_xfail reason: xfailing this test XPASS test_example.py::test_xpass always xfail @@ -296,7 +296,7 @@ More than one character can be used, so for example to only see failed and skipp test_example.py:14: AssertionError ========================= short test summary info ========================== FAILED test_example.py::test_fail - assert 0 - SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:23: skipping this test + SKIPPED [1] $REGENDOC_TMPDIR/test_example.py:22: skipping this test == 1 failed, 1 passed, 1 skipped, 1 xfailed, 1 xpassed, 1 error in 0.12s === Using ``p`` lists the passing tests, whilst ``P`` adds an extra section "PASSES" with those tests that passed but had diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index 54bb60da1..4b8be4469 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -41,7 +41,7 @@ Running pytest now produces this output: warnings.warn(UserWarning("api v1, should use functions from v2")) -- Docs: https://docs.pytest.org/en/latest/warnings.html - ====================== 1 passed, 1 warnings in 0.12s ======================= + ======================= 1 passed, 1 warning in 0.12s ======================= The ``-W`` flag can be passed to control which warnings will be displayed or even turn them into errors: @@ -407,7 +407,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta class Test: -- Docs: https://docs.pytest.org/en/latest/warnings.html - 1 warnings in 0.12s + 1 warning in 0.12s These warnings might be filtered using the same builtin mechanisms used to filter other types of warnings. diff --git a/doc/en/writing_plugins.rst b/doc/en/writing_plugins.rst index 8660746bd..2f7283791 100644 --- a/doc/en/writing_plugins.rst +++ b/doc/en/writing_plugins.rst @@ -442,7 +442,7 @@ additionally it is possible to copy examples for an example folder before runnin testdir.copy_example("test_example.py") -- Docs: https://docs.pytest.org/en/latest/warnings.html - ====================== 2 passed, 1 warnings in 0.12s ======================= + ======================= 2 passed, 1 warning in 0.12s ======================= For more information about the result object that ``runpytest()`` returns, and the methods that it provides please check out the :py:class:`RunResult From be59827216612fd416818702be7e2b8448f4089d Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Tue, 19 Nov 2019 13:56:22 -0300 Subject: [PATCH 153/153] Small fixes in the CHANGELOG for 5.3.0 --- CHANGELOG.rst | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 76b6caf1b..a63e6f838 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -88,12 +88,12 @@ Features But the new functions produce best output on failure. -- `#6057 `_: Add tolerances to complex values when printing ``pytest.approx``. +- `#6057 `_: Added tolerances to complex values when printing ``pytest.approx``. For example, ``repr(pytest.approx(3+4j))`` returns ``(3+4j) ± 5e-06 ∠ ±180°``. This is polar notation indicating a circle around the expected value, with a radius of 5e-06. For ``approx`` comparisons to return ``True``, the actual value should fall within this circle. -- `#6061 `_: Adding the pluginmanager as an option ``pytest_addoption`` +- `#6061 `_: Added the pluginmanager as an argument to ``pytest_addoption`` so that hooks can be invoked when setting up command line options. This is useful for having one plugin communicate things to another plugin, such as default values or which set of command line options to add. @@ -109,7 +109,7 @@ Improvements - `#5630 `_: Quitting from debuggers is now properly handled in ``doctest`` items. -- `#5924 `_: Improve verbose diff output with sequences. +- `#5924 `_: Improved verbose diff output with sequences. Before: @@ -148,14 +148,14 @@ Improvements - `#5936 `_: Display untruncated assertion message with ``-vv``. -- `#5990 `_: Fix plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). +- `#5990 `_: Fixed plurality mismatch in test summary (e.g. display "1 error" instead of "1 errors"). - `#6008 `_: ``Config.InvocationParams.args`` is now always a ``tuple`` to better convey that it should be immutable and avoid accidental modifications. -- `#6023 `_: ``pytest.main`` returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). +- `#6023 `_: ``pytest.main`` now returns a ``pytest.ExitCode`` instance now, except for when custom exit codes are used (where it returns ``int`` then still). - `#6026 `_: Align prefixes in output of pytester's ``LineMatcher``. @@ -167,10 +167,10 @@ Improvements - `#6069 `_: ``pytester.spawn`` does not skip/xfail tests on FreeBSD anymore unconditionally. -- `#6097 `_: The "[XXX%]" indicator in the test summary is colored according to the final (new) multi-colored line's main color. +- `#6097 `_: The "[XXX%]" indicator in the test summary is now colored according to the final (new) multi-colored line's main color. -- `#6116 `_: Add ``--co`` as a synonym to ``--collect-only``. +- `#6116 `_: Added ``--co`` as a synonym to ``--collect-only``. - `#6148 `_: ``atomicwrites`` is now only used on Windows, fixing a performance regression with assertion rewriting on Unix. @@ -182,34 +182,34 @@ Improvements - `#6176 `_: Improved failure reporting with pytester's ``Hookrecorder.assertoutcome``. -- `#6181 `_: The reason for a stopped session, e.g. with ``--maxfail`` / ``-x`` gets reported. +- `#6181 `_: The reason for a stopped session, e.g. with ``--maxfail`` / ``-x``, now gets reported in the test summary. -- `#6206 `_: cacheprovider: improved robustness and performance with ``cache.set``. +- `#6206 `_: Improved ``cache.set`` robustness and performance. Bug Fixes --------- -- `#2049 `_: Fix ``-setup-plan`` showing inaccurate information about fixture lifetimes. +- `#2049 `_: Fixed ``--setup-plan`` showing inaccurate information about fixture lifetimes. -- `#2548 `_: Fix line offset mismatch with skipped tests in terminal summary. +- `#2548 `_: Fixed line offset mismatch of skipped tests in terminal summary. -- `#6039 `_: The ``PytestDoctestRunner`` is properly invalidated when unconfiguring the doctest plugin. +- `#6039 `_: The ``PytestDoctestRunner`` is now properly invalidated when unconfiguring the doctest plugin. This is important when used with ``pytester``'s ``runpytest_inprocess``. -- `#6047 `_: BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. +- `#6047 `_: BaseExceptions are now handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. -- `#6074 `_: pytester: fix order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. +- `#6074 `_: pytester: fixed order of arguments in ``rm_rf`` warning when cleaning up temporary directories, and do not emit warnings for errors with ``os.open``. -- `#6189 `_: Fix incorrect result of ``getmodpath`` method. +- `#6189 `_: Fixed result of ``getmodpath`` method.