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 cf886f906de..1bd2642aeb0 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 @@ 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 report_failure(self, out, test, example, got): 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 8d327cbb322..c31b6b0b48f 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -458,7 +458,6 @@ def test_2(): def test_pdb_interaction_doctest(self, testdir, monkeypatch): p1 = testdir.makepyfile( """ - import pytest def function_1(): ''' >>> i = 0 @@ -477,9 +476,32 @@ def function_1(): 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 2e3d49c377a..a56ad4b83dd 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 @@ def _is_capturing(cls, capman): @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 @@ def _import_pdb_cls(cls, capman): "--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 ad9c37737af..1b11a8af070 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1238,3 +1238,40 @@ def test_3(): 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 00000000000..68cd66f9902 --- /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 0f346074184..a050dad09e5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1318,8 +1318,7 @@ def fnmatch_lines(self, lines2): 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 @@ def re_match_lines(self, lines2): 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 @@ def _match_lines(self, lines2, match_func, match_nickname): 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 d330ff2532b..f8b0896c5fd 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 ad9c37737af..c8269965169 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -246,7 +246,7 @@ def test_issue93_initialnode_importing_capturing(self, testdir): ) 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 @@ def test_with_failing_collection(self, testdir): 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 main(): 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 5673b811b98..e2f06a0a253 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -399,7 +399,7 @@ def test_division_zero(): 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(tmpdir): ) 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 5d2ff4654d0..c68866beff9 100644 --- a/testing/logging/test_fixture.py +++ b/testing/logging/test_fixture.py @@ -46,7 +46,7 @@ def test2(caplog): ) 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 1ae0bd78333..5b24ef963af 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -109,7 +109,7 @@ def test_foo(): "=* 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(request): "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(request): "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(request): "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(request): "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_simple(): ] ) 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_simple(): ] ) 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_simple(): 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 e6dd3e87088..8fc882f88a0 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1139,7 +1139,7 @@ class Test(object): """ ) 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 f4dbfdf0977..7bacfb6e247 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -455,7 +455,7 @@ def test_lookup_error(unknown): "*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 @@ def test_finish(): *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 @@ def arg1(): *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 7c871a9eead..6343991aeaf 100644 --- a/testing/python/setup_only.py +++ b/testing/python/setup_only.py @@ -27,7 +27,7 @@ def test_arg1(arg1): 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 aff8aa0e5e2..ef841819d09 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_arg1(arg1): " 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 8fce5e279bb..56729d28abd 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1034,7 +1034,7 @@ def test_hello(): 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_hello(): ) 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 89b23a72c7c..470c54145e4 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -914,7 +914,7 @@ def test_rewrite_warning_using_pytest_plugins(self, testdir): 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(): 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_long_repr(): """ ) 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 cbba27e5f5d..e2fd5a4ca56 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -327,7 +327,7 @@ def test_always_fails(): 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 @@ def test_lf_and_ff_prints_no_needless_message(self, quiet, opt, testdir): 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 f5b193597fb..0f7db4b8e94 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -609,12 +609,12 @@ def test_normal(): *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 @@ def test_captured_print(captured_print): ) 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 pytest_runtest_setup(): 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_spam_in_thread(): 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 dee07d5c715..7a5cf795b22 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -139,7 +139,7 @@ def test_ignored_virtualenvs(self, testdir, fname): # 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 @@ def test_ignored_virtualenvs_norecursedirs_precedence(self, testdir, fname): 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 @@ def pytest_configure(config): 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 3f08ee38169..0374db0b351 100644 --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -187,7 +187,7 @@ def pytest_addoption(parser): ) 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 pytest_addoption(parser): ) ) 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 4aac5432d58..755f26286ab 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -239,8 +239,8 @@ def foo(): ] ) # 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 @@ def auto(request): """ ) 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 @@ def auto(request): """ ) 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 d4a1f6cc311..06a03348961 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1216,7 +1216,7 @@ def test_pass(): ) 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_pass(): ) 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_pass(): 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 82e413518f6..9920d2b3e90 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -615,7 +615,7 @@ def teardown_function(function): ) 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 some_internal_function(): ) 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_hello(): ) 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_func(): """ ) 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_foo(): """ ) 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_fix(foo): """ ) 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 34a086551fd..1e63bbf49d3 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -237,7 +237,7 @@ def test_function2(hello): "*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 8bba479f1b0..51b1bbdd6ac 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -949,7 +949,7 @@ def test_func(): 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 88f96f8940a..3bdabc5dee8 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -204,7 +204,7 @@ def test_method(self): 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 @@ def test_pass(): "*= 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_this(i): 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_failure(): "*== 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_failure(): "*== 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 @@ def pytest_collection_modifyitems(items, config): """ ) 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 @@ def test_capture_no(self, many_tests_files, testdir): ) 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 9b1b688ff86..281c852814a 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -270,7 +270,7 @@ def test_method(self): 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_hello(self): % (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_hello(self): """ ) 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 @@ class Test(unittest.TestCase): """ ) 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_hello(self): ) 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 00000000000..a03eb47047b --- /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 c2a4e446ffb..0350b0b075d 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 56729d28abd..999f64a0edd 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -413,6 +413,55 @@ def test_list_different_lengths(self): 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 00000000000..17c0b17378c --- /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 534bfe2a831..1d26d94ab58 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -507,7 +507,9 @@ def traceback(self, value: Traceback) -> None: 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 e2f06a0a253..3f205b131c6 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 999f64a0edd..7b99a65b43a 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -12,13 +12,11 @@ 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 @@ def test_check(list): 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 @@ def test_text_skipping(self): 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 @@ def test_bytes_diff_normal(self): 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 @@ def test_iterable_full_diff(self, left, right, expected): 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 06a03348961..885d259415b 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -477,22 +477,25 @@ def test_fail(): 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 b0394165752..edc9a56673a 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 0d97941597f..2fd81ea2b66 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 1d26d94ab58..694f45fb6d4 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -448,7 +448,7 @@ def from_current( 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 281c852814a..f56284d8510 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -233,7 +233,7 @@ def tearDownClass(self): 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 @@ def f(_): # 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 fce4c1e3f21..29f9658ee14 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -236,16 +236,9 @@ def from_call(cls, func, when, reraise=None): 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 2d6b8476c10..d2ce5d0bcf1 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 00000000000..22ef35cc8dc --- /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 39c8d2cdfa2..cd23281fa73 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 InvocationParams: .. 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 @@ def parse(self, args, addopts=True): 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 71dae5c4cdb..0264b029d01 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -7,6 +7,7 @@ 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 @@ def test_invocation_params_args(self, _sys_snapshot): 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 @@ class DummyPlugin: 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 @@ class DummyPlugin: 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 63d22477cb3..d817a5cfa45 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 70d6ed12f86..13c2886bb72 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 00000000000..c5cd924bb59 --- /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 0350b0b075d..ce29553d57a 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -7,6 +7,7 @@ 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 470c54145e4..3555d825225 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -190,11 +190,12 @@ class X: 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 @@ def test_assertrepr_compare_same_width(self, request): 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 b77c3f59501..92d28cafc06 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 a050dad09e5..2ce3701c0a5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -927,11 +927,9 @@ def parseconfigure(self, *args): 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 e4e58b32cf6..c4099e6b02d 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -693,13 +693,6 @@ def snap(self): 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 00000000000..9eb0c1cd3e5 --- /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 2fd81ea2b66..fd30d85723b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -864,15 +864,41 @@ def _outrep_summary(self, rep): 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)) - - if parts: - line = ", ".join(parts) - else: - line = "no tests ran" + 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 "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" + if not parts: + parts = [("no tests ran", {_color_for_type_default: True})] - return line, color + return parts, main_color def _plugin_nameversions(plugininfo): diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 924c2f4af36..d4c4e8014b4 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -193,7 +193,7 @@ def test_one(self): ) 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 @@ def test_not_called_due_to_quit(): 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 @@ def do_continue(self, arg): 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 @@ def test_3(): 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_inner({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 3bdabc5dee8..a624be3b4ee 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -164,7 +164,7 @@ def test_1(): 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_failure(): # 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 @@ class DummyReport(BaseReport): 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 e4e58b32cf6..2877f2215e0 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 b77c3f59501..be30aa44e8f 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 f18ce08877a..2ee1604a743 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 b77c3f59501..5542fa2ac7c 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 7aa100e6e24..296aa42aaac 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 00000000000..45d49bdae7a --- /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 00000000000..b13a677c8a1 --- /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 1bd2642aeb0..4c17e035873 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 a5187644d99..2bf56cb80f8 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -859,16 +859,21 @@ def test_doctest_id(self, testdir): 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 53475078ec4..fbf34480734 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -506,7 +506,7 @@ def function_1(): 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 00000000000..6cf81002ece --- /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 ea709a26a72..4746fd6c7fc 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -18,6 +18,7 @@ from typing import Sequence from typing import Set from typing import Tuple +from typing import Union import attr import py @@ -56,7 +57,7 @@ def __init__(self, path, excinfo): 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 2ce3701c0a5..2a9ef12b81a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -878,7 +878,9 @@ class reprec: 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 0264b029d01..d4d624348f5 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 d2ce5d0bcf1..86e9bddffdb 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -597,7 +597,7 @@ def test_foo(): 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 pytest_sessionstart(): 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 e9a970ca7f5..8481848f7bf 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 58076d66b9d..4967106d9e4 100644 --- a/src/_pytest/recwarn.py +++ b/src/_pytest/recwarn.py @@ -187,7 +187,7 @@ def __exit__( 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 @@ def __exit__( # manually here for this context manager to become reusable. self._entered = False - return False - class WarningsChecker(WarningsRecorder): def __init__( @@ -232,7 +230,7 @@ def __exit__( 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 @@ def __exit__( [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 1bd2642aeb0..fd65a3cc70a 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -6,6 +6,8 @@ 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 db1de1986a9..7449a56c8c0 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -6,8 +6,12 @@ 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.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 @@ # 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 @@ def __init__(self, failures): 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 report_unexpected_exception(self, out, test, example, exc_info): 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 @@ def setup(self): 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 @@ def _disable_output_capturing_for_darwin(self): 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 @@ def repr_failure(self, excinfo): 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 @@ def reportinfo(self): 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 @@ def collect(self): 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 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): 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 @@ def _find(self, tests, obj, name, module, source_lines, globs, seen): 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 func(): 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 _remove_unwanted_precision(self, want, got): offset += w.end() - w.start() - (g.end() - g.start()) return got - _get_checker.LiteralsOutputChecker = LiteralsOutputChecker - return _get_checker.LiteralsOutputChecker() + return LiteralsOutputChecker + + +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(): +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 755f26286ab..37b3988f786 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -839,7 +839,8 @@ def test_bytes_literal(self, testdir): 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 @@ def test_number_re(self): "-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 054bfc866d4..c72f761188a 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 @@ def reset(self): 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 @@ def _create_formatter(self, log_format, log_date_format): ): 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 d817a5cfa45..8fdb61c2b7a 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 7a5deaa39a7..dad76f13f5e 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -7,6 +7,7 @@ import json import os from collections import OrderedDict +from typing import List import attr import py @@ -15,6 +16,9 @@ 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 @@ def __init__(self, config): 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 fd30d85723b..35f6d324b9c 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -9,6 +9,12 @@ 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 @@ def __init__(self, config, file=None): 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 @@ def _printcollecteditems(self, items): # 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 @@ def getreports(self, name): 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 @@ def summary_warnings(self): 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 @@ def summary_stats(self): 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 @@ def show_simple(stat, lines): 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 @@ def show_xfailed(lines): 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 @@ def show_xpassed(lines): 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 @@ def show_skipped(lines): "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 c4099e6b02d..56707822ddf 100644 --- a/src/_pytest/capture.py +++ b/src/_pytest/capture.py @@ -12,6 +12,7 @@ import pytest from _pytest.compat import CaptureIO +from _pytest.fixtures import FixtureRequest patchsysdict = {0: "stdin", 1: "stdout", 2: "stderr"} @@ -241,13 +242,12 @@ def pytest_internalerror(self, excinfo): 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 0f7db4b8e94..180637db60f 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -6,6 +6,8 @@ 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 c225eff5fb5..4e7db83696d 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -78,7 +78,8 @@ def find_spec(self, name, path=None, target=None): # 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 @@ def visit_Assert(self, assert_): 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 7449a56c8c0..48c934e3a31 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -368,9 +368,7 @@ def collect(self): ) 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 e6dee15470c..71036dc7e97 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -139,8 +139,7 @@ def warn(self, warning): ) ) 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 00000000000..f6609ddf136 --- /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 a050dad09e5..875828fd6cb 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 @@ def __init__(self, ret, outlines, errlines, duration): 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 f8b0896c5fd..1a068c1d007 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -121,17 +121,6 @@ def test_potato(): 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(): 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 00000000000..34dfb278de9 --- /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 a050dad09e5..2974420f58e 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1344,7 +1344,6 @@ def _match_lines(self, lines2, match_func, match_nickname): 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 @@ def _match_lines(self, lines2, match_func, match_nickname): nextline = None extralines = [] __tracebackhide__ = True + wnick = len(match_nickname) + 1 for line in lines2: nomatchprinted = False while lines1: @@ -1361,17 +1361,21 @@ def _match_lines(self, lines2, match_func, match_nickname): 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 @@ def _no_match_line(self, pat, match_func, match_nickname): """ __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 f8b0896c5fd..63710143a40 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 00000000000..b7334e7fe55 --- /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 f03d45ab76c..025a46076a9 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 0575557ae78..5900dee28b2 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -46,7 +46,6 @@ def test_repr_string(self, plus_minus): 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 @@ def test_repr_string(self, plus_minus): ), ) + 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 5900dee28b2..60fde151a27 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -24,37 +24,18 @@ def report_failure(self, out, test, example, got): 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 @@ def test_repr_complex_numbers(self): 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 00000000000..39ffff99bad --- /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 35f6d324b9c..e9d44f2a8b9 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 a624be3b4ee..fdd53d3c6c0 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 9f6ee048d9a..d96e6e71341 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 00000000000..b53097c4330 --- /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 9c3a4c73175..f90efc3a58a 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 2861baefda3..f12ac13d835 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -7,6 +7,7 @@ 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 add_option_ini(option, dest, default=None, type=None, **kwargs): 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 @@ def __init__(self, config): 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 @@ def __init__(self, config): 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 @@ def _create_formatter(self, log_format, log_date_format): 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 @@ def _setup_cli_logging(self): 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 6850a83cdb7..b363e8b03ff 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 025a46076a9..52a91a905ba 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -227,7 +227,7 @@ def __repr__(self): """ # 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 00000000000..80d1c4a386a --- /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 4e7db83696d..6a4b24da1f7 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -13,6 +13,7 @@ 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.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 @@ def create_module(self, spec): 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 @@ def exec_module(self, module): # 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 @@ def exec_module(self, module): 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 @@ def visit_Assert(self, assert_): "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 @@ def warn_about_none_ast(self, node, module_path, lineno): 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 @@ def visit_Compare(self, comp: ast.Compare): 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 3555d825225..f00f25ff089 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -9,6 +9,7 @@ import textwrap import zipfile from functools import partial +from pathlib import Path import py @@ -17,6 +18,8 @@ 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 fake_mkdir(p, *, exc): 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 6a4b24da1f7..9c9d6135b1b 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -28,6 +28,7 @@ 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 @@ def exec_module(self, module): 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 @@ def visit_Assert(self, assert_): "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 @@ def warn_about_none_ast(self, node, module_path, lineno): 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 @@ def visit_Compare(self, comp: ast.Compare): 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 3898fb252a1..83947d3eb55 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -4,6 +4,7 @@ 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 f00f25ff089..6c5de5c0350 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 f45b0bab705..543103fb5b0 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 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 @@ 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 chmod_rw(p: str): 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 cleanup_on_exit(lock_path=lock_path, original_pid=pid): 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 123a583adaa..bd8fb7d8a7e 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 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 @@ def from_config(cls, config): 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 @@ def mktemp(self, basename, numbered=True): 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 @@ def getbasetemp(self): 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 edc9a56673a..e3012d280a0 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 00000000000..e60d154bb97 --- /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 8a218d5e52d..7eb91275156 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1194,8 +1194,6 @@ def spawn(self, cmd, expect_timeout=10.0): 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 eb55093bbfd..dd0a3b9493f 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -605,11 +605,7 @@ def test_normal(): ) 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 e76bb78579d..e21e234e774 100644 --- a/src/_pytest/mark/__init__.py +++ b/src/_pytest/mark/__init__.py @@ -8,6 +8,7 @@ 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 644058f8fa5..37acc495378 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -11,6 +11,7 @@ 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 @@ def __init__(self, request, tmpdir_factory): 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 @@ def pytest_configure(x, config): reprec = rec.pop() else: - class reprec: + class reprec: # type: ignore pass reprec.ret = ret @@ -851,7 +848,7 @@ class reprec: 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 @@ def runpytest_inprocess(self, *args, **kwargs): 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 reprec: 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 @@ def popen( 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 @@ def run(self, *cmdargs, timeout=None, stdin=CLOSE_STDIN): """ __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 @@ def handle_timeout(): 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 @@ def _dump_lines(self, lines, fp): 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 @@ def runpython_c(self, command): """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 7bf36831c89..758e999dc22 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 e11400c1fa0..def641c95d9 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 00000000000..6f5ad648eab --- /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 ad65ed29929..7b3855e6cb4 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 35f6d324b9c..228fc4219e5 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -539,7 +539,7 @@ def report_collect(self, final=False): 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 @@ def _folded_skips(skipped): _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 2bf56cb80f8..82c727fc62d 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -628,7 +628,7 @@ def test_pyargs_importerror(self, testdir, monkeypatch): 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 @@ def test_with_failing_collection(self, testdir): 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 53704711990..30f9841b565 100644 --- a/testing/python/collect.py +++ b/testing/python/collect.py @@ -1167,7 +1167,7 @@ def test_real(): [ "*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 6399863c725..6dca793e051 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -3081,7 +3081,7 @@ def test_3(): *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 6c5de5c0350..3aab3ac2afa 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -122,7 +122,7 @@ def test_dont_rewrite_plugin(self, testdir): } 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 eb55093bbfd..a51dfea3035 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -453,7 +453,7 @@ def test_two(capfd, capsys): "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 7a5cf795b22..2598683570b 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 37b3988f786..79095e3e743 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -334,7 +334,7 @@ def test_doctest_unex_importerror_with_module(self, testdir): [ "*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 c8d5851ac64..93bc77a164d 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -891,7 +891,7 @@ def test(): 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 1e63bbf49d3..0ff508d2c4d 100644 --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -234,7 +234,7 @@ def test_function2(hello): "*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 51b1bbdd6ac..8ba77ba129d 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -886,7 +886,7 @@ def test_func(): " 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 f61425b6b5b..3e4f86f210d 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 c53b9f2ecc8..ba1844fed9f 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -1237,7 +1237,7 @@ def test_failure(): "*= 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_failure(): {"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_failure(): ], {"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 077636c52df..bbcf87e5a6e 100644 --- a/testing/test_warnings.py +++ b/testing/test_warnings.py @@ -142,7 +142,7 @@ def test_func(fix): [ "*== %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_show_warning(): """ ) 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(): """ ) 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_foo(): "*== %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_bar(): [ "*== %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 @@ def test_hidden_by_mark(self, testdir): [ "*== %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 eb55093bbfd..91518c37b22 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 | 39 +++++++++++++++++++++++++++------------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/testing/python/approx.py b/testing/python/approx.py index 60fde151a27..11502c509b6 100644 --- a/testing/python/approx.py +++ b/testing/python/approx.py @@ -11,16 +11,32 @@ inf, nan = float("inf"), float("nan") -class MyDocTestRunner(doctest.DocTestRunner): - def __init__(self): - doctest.DocTestRunner.__init__(self) - - def report_failure(self, out, test, example, got): - raise AssertionError( - "'{}' evaluates to '{}', not '{}'".format( - example.source.strip(), got.strip(), example.want.strip() +@pytest.fixture +def mocked_doctest_runner(monkeypatch): + class MockedPdb: + def __init__(self, out): + pass + + 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 @@ def test_numpy_array_wrong_shape(self): 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 11502c509b6..f720456245e 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 @@ @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 @@ def test_numpy_array_wrong_shape(self): 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 0a7e425b74f..bad99f38076 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 a03efb0cf2c..8b0430f6927 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 a0cf1d8c128..e99206a4d2a 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 eb55093bbfd..4320a7caeb2 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1503,11 +1503,9 @@ def test_fails(): """ ) 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 00000000000..8ee3b64628a --- /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 53737816f5c..9eaa77d1782 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 228fc4219e5..216ad04173b 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -954,7 +954,7 @@ def show_skipped(lines: List[str]) -> None: 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 8ba77ba129d..86f328a931c 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -731,23 +731,37 @@ def test_though(self): 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 67aa0c77e36..a79b4077b56 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 00000000000..ddbca65d6a8 --- /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 c623d0602ab..5d7599f5096 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 9d01e5f233d..5cf3b090303 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 00000000000..61992518079 --- /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 167c7fa9b03..a23cf764ace 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 4746fd6c7fc..2b0f48c07a7 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -154,6 +154,7 @@ def directory_arg(path, optname): "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 00000000000..b12d0a55ded --- /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 49eec612962..b1592f8178b 100644 --- a/src/_pytest/reports.py +++ b/src/_pytest/reports.py @@ -329,18 +329,18 @@ def toterminal(self, out): 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 00000000000..cc2a431ec5a --- /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 9f6c5618660..ff813543c58 100644 --- a/testing/test_reports.py +++ b/testing/test_reports.py @@ -330,7 +330,7 @@ def test_b(): pass 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 @@ def test_b(): pass 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 @@ def test_a(): pass 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 10a9857d7a7..7a21837bd35 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 00000000000..b7804a00874 --- /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 ccddb1f6690..8f81ff4d24a 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 1220cfb4d11..6f3e8031b2c 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 a7cd06d31fc..85000b46dbc 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 5f429c219a7..818974d0b6f 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 1bab9877c61..d09f8004343 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -697,7 +697,9 @@ def __init__(self, pluginmanager, *, invocation_params=None): 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 10a9857d7a7..3340b2153d6 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 97f220ca553..836b458c6e4 100644 --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -135,6 +135,36 @@ def test_hook_proxy(self, testdir): 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 b7804a00874..11f5486257a 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 8f81ff4d24a..ccddb1f6690 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 6f3e8031b2c..1220cfb4d11 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 85000b46dbc..a7cd06d31fc 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 818974d0b6f..8660746bd4f 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 71036dc7e97..e1dc21f8a81 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 e7710266753..bc5ddfbe9a4 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 7704421a22e..908fd2183cf 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 @@ def repr_instance(self, x, level): 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 ce29553d57a..1d9fffd3493 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 1d9fffd3493..46e57818867 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 @@ _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 bbcf87e5a6e..c4af14dac0e 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 bc5ddfbe9a4..d2f43888ac0 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -24,27 +24,19 @@ 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 @@ def test_method(self): 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 @@ def test_interrupt_me(): """ ) - 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 @@ def test_interrupt_me(): "*_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 00000000000..32eb849062c --- /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 c3663e69516..d26df2d2c56 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -15,6 +15,7 @@ from typing import Mapping from typing import Optional from typing import Set +from typing import Tuple import attr import pluggy @@ -458,18 +459,20 @@ def pytest_runtest_logfinish(self, nodeid): 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 @@ def _get_progress_information_message(self): 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 e7710266753..02de45ff9bd 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -21,6 +21,10 @@ 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 @@ def test_normal(self, many_tests_files, testdir): ] ) + 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 18f57633230..1a35521ad80 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -549,7 +549,7 @@ def getrepr( 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 @@ def _truncate_recursive_traceback(self, traceback): 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 @@ def repr_excinfo(self, excinfo): 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 @@ def repr_excinfo(self, excinfo): 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 @@ def __str__(self): 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 @@ def __init__(self) -> None: 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 @@ def __init__(self, chain): 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 @@ def __init__(self, reprtraceback, reprcrash): 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 @@ def __init__(self, reprentries, extraline, style): 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 @@ def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): 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 @@ def __init__(self, path, lineno, message): 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 48c934e3a31..f7d96257e93 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -305,7 +305,7 @@ def repr_failure(self, excinfo): 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 aae1371ec86..fc55ef2cf7e 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -7,13 +7,13 @@ 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 @@ if False: # TYPE_CHECKING from typing import Type + from _pytest import nodes + @attr.s(frozen=True) class PseudoFixtureDef: @@ -689,8 +691,8 @@ def __init__(self, argname, request, msg=None): 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 @@ def __init__(self, filename, firstlineno, tblines, errorstring, argname): 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 @@ def pytest_plugin_registered(self, plugin): 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 @@ def getfixturedefs(self, argname, nodeid): 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 7b3855e6cb4..084d68dab09 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -5,6 +5,7 @@ import importlib import os import sys +from typing import Dict import attr import py @@ -16,6 +17,7 @@ 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 @@ def __missing__(self, path): class Session(nodes.FSCollector): Interrupted = Interrupted Failed = Failed + _setupstate = None # type: SetupState def __init__(self, config): nodes.FSCollector.__init__( @@ -383,7 +386,9 @@ def __init__(self, config): 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 @@ def __repr__(self): 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 71036dc7e97..d72eaeb0a50 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -4,6 +4,7 @@ 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 @@ 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 @@ def __init__( 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 @@ def getparent(self, cls): 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 @@ def _repr_failure_py(self, excinfo, style=None): 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 @@ def add_report_section(self, when: str, key: str, content: str) -> None: 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 913a93bc025..61cbfec8add 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -9,6 +9,7 @@ from collections.abc import Sequence from functools import partial from textwrap import dedent +from typing import Tuple import py @@ -288,7 +289,7 @@ def getmodpath(self, stopatmodule=True, includemodule=False): 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 b1592f8178b..53f28e73f3b 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 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 @@ def from_item_and_call(cls, item, call): 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 deserialize_repr_crash(repr_crash_dict): 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 29f9658ee14..c383146c3c4 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -6,6 +6,7 @@ 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 @@ def result(self): 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 3f205b131c6..b431bb66dfa 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -902,7 +902,7 @@ def test_reprexcinfo_unicode(self): 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 dfc3c532029..6513909bc58 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 @@ def test_cache_writefail_cachfile_silent(self, testdir): ) 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 @@ def test_error(): """ ) 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 edc9a56673a..93889322f46 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 6513909bc58..6a5a5af8b3f 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -48,28 +48,36 @@ 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) + 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 6a5a5af8b3f..3f03b5ff975 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -46,7 +46,8 @@ def test_cache_writefail_cachfile_silent(self, testdir): ) 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 @@ def test_cache_writefail_permissions(self, testdir): "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 46e57818867..dff27590244 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 7b99a65b43a..b7b84528bc5 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -459,6 +459,43 @@ def test_list_wrap_for_width_rewrap_same_length(self): " ]", ] + 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 00000000000..11a997f713a --- /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 908fd2183cf..7fded872def 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -3,14 +3,24 @@ 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 @@ def __init__(self, maxsize: int) -> None: 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 b431bb66dfa..262d1d18422 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -584,7 +584,7 @@ def __repr__(self): 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 @@ def __repr__(self): 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 86897b57c2f..db86ea4d586 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 @@ class BrokenReprException(Exception): 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 dbe0573760b..7b4eb817a14 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -102,15 +102,20 @@ def test_broken_repr(self, testdir): 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 @@ def test_implicit_bad_repr1(self): 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 @@ def test_repr_error(): 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 db86ea4d586..e24d9b470b5 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -41,9 +41,10 @@ class BrokenReprException(Exception): 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 084d68dab09..d10d2d8711b 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -488,22 +488,22 @@ def _perform_collect(self, args, genitems): 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 15e0bf24ade..5e7e1abf5a9 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 @@ def test_iter(self): 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 @@ def test_multiline(): class TestTry: - source = """\ + def setup_class(self): + self.source = """\ try: raise ValueError except Something: @@ -646,7 +649,8 @@ def test_else(self): class TestTryFinally: - source = """\ + def setup_class(self): + self.source = """\ try: raise ValueError finally: @@ -663,7 +667,8 @@ def test_finally(self): 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 718a463ed00..25d2292e9cb 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1086,7 +1086,7 @@ def test_func_kw(myparam, request, func="func_kw"): 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 b7b84528bc5..aac21a0df01 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -358,7 +358,7 @@ def test_list(self): @pytest.mark.parametrize( ["left", "right", "expected"], [ - ( + pytest.param( [0, 1], [0, 2], """ @@ -368,8 +368,9 @@ def test_list(self): + [0, 2] ? ^ """, + id="lists", ), - ( + pytest.param( {0: 1}, {0: 2}, """ @@ -379,8 +380,9 @@ def test_list(self): + {0: 2} ? ^ """, + id="dicts", ), - ( + pytest.param( {0, 1}, {0, 2}, """ @@ -390,6 +392,7 @@ def test_list(self): + {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 b431bb66dfa..3fa561225f0 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -1229,13 +1229,15 @@ def g(): @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 084d68dab09..ed4204b089b 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -476,7 +476,6 @@ def _perform_collect(self, args, genitems): 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 82c727fc62d..578ab45eb03 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -178,8 +178,14 @@ def test_not_collectable_arguments(self, testdir): 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 543103fb5b0..bd76fac6b31 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 0ebed22ac45..2433d614519 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -393,6 +393,13 @@ def test_on_rm_rf_error(self, tmp_path): 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 bd76fac6b31..8d25b21dd7d 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 2433d614519..29b6db947bc 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -388,7 +388,10 @@ def test_on_rm_rf_error(self, tmp_path): 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 00000000000..624cf5d1c8f --- /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 80353ccbc8c..22cb17dbae6 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 db78bbd0d35..1e9dd5031c3 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -7,10 +7,17 @@ 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 @@ def __eq__(self, other): # 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 @@ def __getitem__(self, key): 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 @@ def strip(self): 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 @@ def indent(self, indent=" " * 4): 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 @@ def getstatementrange(self, lineno): 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 @@ def isparseable(self, deindent=True): 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 @@ def compile( # -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 14db8409e2f..6b45e077b48 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 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._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 @@ def pytest_runtest_protocol(self, item): @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 @@ def __repr__(self): 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 @@ def assert_contains(self, entries): 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 @@ def popcall(self, name): 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 @@ def matchreport( ) 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 @@ def listoutcomes(self): 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 @@ def __init__(self, ret: Union[int, ExitCode], outlines, errlines, duration) -> N 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 @@ def parseoutcomes(self): 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 @@ def assert_outcomes( 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 @@ def restore(self): 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 @@ def _match_lines(self, lines2, match_func, match_nickname): :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 00000000000..c60d2c92142 --- /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 dcf63f6fd23..d7f3d7dbf61 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ "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 9c9d6135b1b..b8492993643 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -20,8 +20,6 @@ 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 @@ def get_data(self, pathname): 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 00000000000..4fc96ec77b1 --- /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 843c5b5f7d3..b4261c188dd 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 2598683570b..83345d2c6e4 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -402,7 +402,7 @@ def pytest_collect_file(path, parent): ) 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 @@ def pytest_collect_file(path, parent): 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 3b42b356d5b..34d6701ed03 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 c60d2c92142..3d77ab528f9 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 7521c08e4e4..4af35bd578d 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -28,6 +28,27 @@ _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 aac21a0df01..6c700567a7e 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -462,6 +462,29 @@ def test_list_wrap_for_width_rewrap_same_length(self): " ]", ] + 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 @@ def test_dict_wrap(self): ] 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 d7fb3d78e17..c1654b1c93c 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 b0f9d165195..763d904a4c8 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 2c12c0451c0..d7ba6e23007 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -314,6 +314,21 @@ def test_func(arg): 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 2cab96d675a..924d980f3fd 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -213,9 +213,6 @@ def test_function(): 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 924d980f3fd..18ebc506abb 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -2,7 +2,6 @@ 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 @@ 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 @@ def test_function(): 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 2c12c0451c0..071775aefba 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 00000000000..8e5f4d52aa2 --- /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 50acc2d7d86..21155de2c2f 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -115,9 +115,10 @@ def unset_tracing(): 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 53f28e73f3b..5d445c2f86e 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 @@ def __init__( 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 fc98e30a6de..59f0fe0f3ed 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -9,6 +9,7 @@ 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 @@ 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 mywriter(tags, args): 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 @@ def get_location(self, config): 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 @@ def __init__(self, config, file=None): # 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 @@ def pytest_runtest_logstart(self, nodeid, location): 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 @@ def pytest_runtest_logreport(self, report): 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 @@ def pytest_runtest_logreport(self, report): 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 @@ def pytest_runtest_logfinish(self, nodeid): 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 @@ def _get_progress_information_message(self): 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 @@ def pytest_collection(self): 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 @@ def report_collect(self, final=False): 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 @@ def pytest_sessionstart(self, session): 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 @@ def pytest_collection_finish(self, session): 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 83947d3eb55..5e066c18e02 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -10,7 +10,11 @@ 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.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 @@ def overload(f): # noqa: F811 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 eae783f16b4..737bc11b726 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -15,6 +15,7 @@ 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 @@ def add_report_section(self, when: str, key: str, content: str) -> None: 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 94dac439d4e..04d818b4ec8 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -4,6 +4,7 @@ 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 __class__(self): 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 eae783f16b4..0028e68dd96 100644 --- a/src/_pytest/nodes.py +++ b/src/_pytest/nodes.py @@ -16,6 +16,7 @@ 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 @@ def __init__( 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 @@ def __init__( 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 @@ def listchain(self): """ 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 @@ def addfinalizer(self, fin): 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 6bdc5d73fe6..863a30bb306 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 5becb0f8c9f..15c146e90d4 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1538,27 +1538,6 @@ def test_increment(n, expected): 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 6b45e077b48..9f3b4d8ab7b 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -441,8 +441,9 @@ def assert_outcomes( ) -> 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 00000000000..39787da2e46 --- /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 9f3b4d8ab7b..ca780a9f50a 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -332,10 +332,17 @@ def countoutcomes(self) -> List[int]: 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 6c700567a7e..e4d68ff8c10 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -70,7 +70,14 @@ def test_dummy_failure(testdir): # how meta! """ ) 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 00000000000..0960f62035a --- /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 a23cf764ace..3b5919363c8 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 59f0fe0f3ed..14267b20813 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -676,7 +676,7 @@ def _printcollecteditems(self, items): 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 @@ def pytest_sessionfinish(self, exitstatus): 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 83345d2c6e4..f18d36d2480 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 1bec577b865..d31033197de 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -963,7 +963,31 @@ def test_3(): ) 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 8fdb61c2b7a..8ac1ee22575 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 8e11b56e566..e94bec5d65c 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -974,7 +974,7 @@ def addinivalue_line(self, name, line): 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 9b526ff3e1f..4eec6be056b 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -47,7 +47,7 @@ def getgroup(self, name, description="", after=None): 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 8b45c5f9bef..03e060eb88e 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 59f0fe0f3ed..a84733d4595 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -254,7 +254,7 @@ def __init__(self, config: Config, file=None) -> None: # 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 00000000000..97f7ec74b79 --- /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 5cf3b090303..34a05e1e6fb 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 5186067ef05..442f102d1e9 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -34,3 +34,8 @@ "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 fb951106fc4..9cf22705ef3 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -19,8 +19,10 @@ 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 b8a22428f54..64ec11b7a26 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 ddbca65d6a8..1e0387f4412 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 5d7599f5096..c623d0602ab 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 34a05e1e6fb..748d3ac65a4 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 61992518079..00000000000 --- 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 3b5919363c8..ea849c1a747 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 e94bec5d65c..c5bf32bbfd4 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -154,7 +154,6 @@ def directory_arg(path, optname): "assertion", "junitxml", "resultlog", - "report_log", "doctest", "cacheprovider", "freeze_support", diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 442f102d1e9..5a7066041d4 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -26,7 +26,7 @@ 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 b12d0a55ded..00000000000 --- 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 64ec11b7a26..5390d038d8b 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -16,7 +16,7 @@ def test(): 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 cc2a431ec5a..00000000000 --- 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 b8492993643..af4d00194c6 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -13,7 +13,6 @@ 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.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 e94bec5d65c..06296fd35d7 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -8,7 +8,6 @@ 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.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 e2d6b89c8dd..8490a59e640 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -9,7 +9,6 @@ import textwrap import zipfile from functools import partial -from pathlib import Path import py @@ -23,6 +22,7 @@ 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 d4d624348f5..f146b52a457 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 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 0374db0b351..2918ff04c5f 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 885d259415b..4c2f22a3d6e 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 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 3f03b5ff975..ea78358d6e2 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -253,7 +253,7 @@ def pytest_configure(config): 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 @@ def test_always_fails(): 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 @@ def test_b1(): 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 1e9dd5031c3..a26a70e68be 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -60,7 +60,7 @@ def __getitem__(self, key: int) -> str: 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 af4d00194c6..2f9ca6de0b3 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 5e066c18e02..c115ae98dc6 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -40,7 +40,8 @@ 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 @@ def __get__( 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 52a91a905ba..9f206ce9b49 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 4967106d9e4..5cf32c89400 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 85b0b05aeeb..94af3aef75c 100644 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -92,8 +92,6 @@ def test_init_capturing(self): @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 3c2acfe7f59..19d5efaa6cb 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 199b8716f15..b83ad93e24a 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -59,9 +59,9 @@ def f(): 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 5e7e1abf5a9..519344dd434 100644 --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -478,7 +478,7 @@ def f(x): 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 c115ae98dc6..09e621c5d4b 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -361,21 +361,6 @@ def getvalue(self): 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 fc55ef2cf7e..34ecf2e2125 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -18,7 +18,6 @@ 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 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 @@ def prune_dependency_tree(self): 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 @@ def fixturenames(self): 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 c1654b1c93c..3cee093327c 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -31,6 +31,7 @@ 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 @@ def setmulti2(self, valtypes, argnames, valset, id, marks, scopenum, param_index 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 @@ def __init__(self, definition, fixtureinfo, config, cls=None, module=None): 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 @@ def _pyfuncitem(self): "(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 19d5efaa6cb..3343650425c 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 @@ def __init__(self, tb, excinfo=None): 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 b83ad93e24a..f0880864893 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -502,65 +502,18 @@ def test_repr_many_line_source_not_existing(self): 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 = "?" - - path = "?" - firstlineno = 5 - - 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) + try: + 1 / 0 + except ZeroDivisionError: + excinfo = ExceptionInfo.from_current() - fail = IOError() - repr = pr.repr_excinfo(excinfo) - assert repr.reprtraceback.reprentries[0].lines[0] == "> ???" - assert repr.chain[0][0].reprentries[0].lines[0] == "> ???" + with monkeypatch.context() as m: + m.setattr(_pytest._code.Code, "fullsource", property(lambda self: None)) + repr = pr.repr_excinfo(excinfo) - 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 a26a70e68be..d7cef683d7a 100644 --- a/src/_pytest/_code/source.py +++ b/src/_pytest/_code/source.py @@ -8,6 +8,7 @@ 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 @@ def __getitem__(self, key: Union[int, slice]) -> Union[str, "Source"]: # noqa: 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 09e621c5d4b..fc810b3e510 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -10,11 +10,14 @@ 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 @@ import importlib_metadata # noqa: F401 -def _format_args(func): +def _format_args(func: Callable[..., Any]) -> str: return str(signature(func)) @@ -67,12 +70,12 @@ def fspath(p): 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 syntax, and doesn't contain yield), or a function decorated with 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 @@ def nullcontext(): 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 @@ def get_default_arg_names(function): ) -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 @@ def safe_isclass(obj): ) -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 00000000000..67d8363b39c --- /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 3c60fdb33a6..6e53545d630 100755 --- a/src/_pytest/cacheprovider.py +++ b/src/_pytest/cacheprovider.py @@ -125,13 +125,14 @@ def set(self, key, value): 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 8481848f7bf..8210ef5d595 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 06296fd35d7..d4521adf610 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 4eec6be056b..7cbb676bd70 100644 --- a/src/_pytest/config/argparsing.py +++ b/src/_pytest/config/argparsing.py @@ -395,7 +395,7 @@ def _parse_optional(self, arg_string): 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 ca780a9f50a..02414a29906 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -312,7 +312,7 @@ def getfailedcollections(self) -> List[TestReport]: 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 6dca793e051..52fd32cc441 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -503,7 +503,7 @@ def test_func(something): pass 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 @@ def test_method(self, something): 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 @@ def test_second(): 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 28b0715c01a..1c701796af2 100644 --- a/testing/python/raises.py +++ b/testing/python/raises.py @@ -205,7 +205,7 @@ def test_match_failure_string_quoting(self): 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 f18d36d2480..e4a70b1a728 100644 --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -486,7 +486,7 @@ def test_collect_protocol_single_function(self, testdir): 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 @@ def test_serialization_byid(self, testdir): 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 @@ def test_method(self): 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 ba759980400..0e44220259c 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1011,7 +1011,7 @@ def test_custom_mark_parametrized(obj_type): 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 86f328a931c..67714d030ed 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -115,7 +115,7 @@ def test_func(): ) 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 29b6db947bc..eb1c1f300a7 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -258,7 +258,7 @@ def test_lock_register_cleanup_removal(self, tmp_path): 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 f56284d8510..885178402d3 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -383,7 +383,7 @@ def test_hello(self): 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 3343650425c..a8f11736607 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -7,13 +7,17 @@ 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 _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 @@ def path(self): 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 @@ def fullsource(self): 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 @@ def source(self): 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 @@ def __init__(self, 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 @@ def eval(self, code, **vars): 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 @@ def exec_(self, code, **vars): 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 @@ def repr(self, object): 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 @@ def getargs(self, var=False): 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 @@ def path(self): 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 @@ def ishidden(self): 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 @@ def __str__(self): 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 @@ def cut(self, path=None, lineno=None, firstlineno=None, excludepath=None): 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() + + @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=lambda x: not x.ishidden()): + 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 @@ def filter(self, fn=lambda x: not x.ishidden()): """ 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 @@ def getcrashentry(self): 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 @@ def _getreprcrash(self) -> "ReprFileLocation": 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 @@ def _getindent(self, source): 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 @@ def get_source(self, source, line_index=-1, excinfo=None, short=False) -> List[s 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 @@ def repr_locals(self, locals): # # 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 @@ def repr_traceback_entry(self, entry, excinfo=None): 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 @@ def _makepath(self, path): 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 @@ def repr_traceback(self, excinfo): 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 @@ def _truncate_recursive_traceback(self, traceback): 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 @@ def repr_excinfo(self, excinfo: ExceptionInfo) -> "ExceptionChainRepr": 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 @@ def __str__(self): 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 @@ def toterminal(self, tw) -> None: 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 @@ def toterminal(self, tw) -> None: 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 @@ def toterminal(self, tw) -> None: 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 @@ def toterminal(self, tw) -> None: 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 @@ def toterminal(self, tw) -> None: 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 @@ def __init__(self, lines, reprfuncargs, reprlocals, filelocrepr, style): 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 @@ def toterminal(self, tw) -> None: 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 @@ def toterminal(self, tw) -> None: 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 @@ def toterminal(self, tw) -> None: 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 @@ def toterminal(self, tw) -> None: 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 @@ def getrawcode(obj, trycall=True): _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 2f55720b422..f8e1ce17f21 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 f4(x, *y, **z): 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 f4(x, *y, **z): class TestExceptionInfo: - def test_bad_getsource(self): + def test_bad_getsource(self) -> None: try: if False: pass @@ -145,13 +145,13 @@ def test_bad_getsource(self): 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 @@ def test_getsource(self): 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 f0880864893..997b14e2f68 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -3,6 +3,7 @@ import queue import sys import textwrap +from typing import Union import py @@ -224,23 +225,25 @@ def f(n): 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 @@ def func1(): 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 @@ def unreraise(): @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 519344dd434..bf52dccd791 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_method(self): 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 f(): 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 @@ def test_source_putaround_simple(): ) -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 @@ def g(x): """ ) - 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 @@ def f(x): """ ).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 @@ def f(): 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 @@ def test_getstatement(self): # 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 @@ def test_getstatementrange_triple_quoted(self): 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 @@ def test_getstatementrange_within_constructs(self): # 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 @@ def test_getstatementrange_bug(self): 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 @@ def test_getstatementrange_bug2(self): 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 @@ def test_some(): 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 @@ def test_offsetless_synerr(self): 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 __init__(self, *args): 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 g(): pass """ 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 f(): 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 g(): 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 @@ def x(): 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 x(): ) 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 @@ class B: 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 @@ def test_comments(): 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 @@ def test_multiline(): class TestTry: - def setup_class(self): + def setup_class(self) -> None: self.source = """\ try: raise ValueError @@ -631,25 +646,25 @@ def setup_class(self): 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 @@ def setup_class(self): 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 @@ def setup_class(self): 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 @@ def test_semicolon(): 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 @@ def XXX_test_expression_multiline(): 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 ea78358d6e2..2d91e234b46 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -2,7 +2,6 @@ import shutil import stat import sys -import textwrap import py @@ -65,13 +64,7 @@ def test_cache_failure_warns(self, testdir): 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_hello(): abscache=external_cache ) ) - result = testdir.runpytest("-v") result.stdout.fnmatch_lines( ["cachedir: {abscache}".format(abscache=external_cache)] @@ -256,33 +243,23 @@ def test_lastfailed_usecase(self, testdir, monkeypatch): 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 @@ def test_3(): "*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 @@ def test_3(): 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 @@ def test_always_fails(): 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 @@ def test_always_fails(): 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 @@ def test_b1(): 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 @@ def test_b1(): 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 @@ def test_fail(val): 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 @@ def test_b2(): 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 @@ def rlf(fail_import, fail_run): 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 @@ def test_hello(): env = os.environ if '1' == env['FAILIMPORT']: raise ImportError('fail') + def test_hello(): assert '0' == env['FAILTEST'] @@ -613,8 +542,7 @@ def test_xfail_not_considered_failure(self, testdir): """ import pytest @pytest.mark.xfail - def test(): - assert 0 + def test(): assert 0 """ ) result = testdir.runpytest() @@ -626,8 +554,7 @@ def test_xfail_strict_considered_failure(self, testdir): """ import pytest @pytest.mark.xfail(strict=True) - def test(): - pass + def test(): pass """ ) result = testdir.runpytest() @@ -641,8 +568,7 @@ def test_failed_changed_to_xfail_or_skip(self, testdir, mark): testdir.makepyfile( """ import pytest - def test(): - assert 0 + def test(): assert 0 """ ) result = testdir.runpytest() @@ -655,8 +581,7 @@ def test(): """ import pytest @pytest.{mark} - def test(): - assert 0 + def test(): assert 0 """.format( mark=mark ) @@ -694,18 +619,14 @@ def test_cache_cumulative(self, testdir): # 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 @@ def test_foo_4(): # 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 @@ def test_bar_2(): # 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 @@ def test_foo_4(): 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 @@ def test_2(): 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 @@ def check_readme(self, testdir): 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 2d91e234b46..0e1194b02a0 100644 --- a/testing/test_cacheprovider.py +++ b/testing/test_cacheprovider.py @@ -59,7 +59,8 @@ def test_cache_writefail_permissions(self, testdir): @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 30853de9490..2ce2f3f3a24 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -914,7 +914,6 @@ def __init__(self, definition, fixtureinfo, config, cls=None, module=None): 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 30853de9490..f7436f23b32 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 0c0cb48615e..42d5b94604b 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 ebc6895f2cd..2e875628920 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -9,6 +9,7 @@ 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 @@ def __init__(self, definition, fixtureinfo, config, cls=None, module=None): #: 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 0c3c2aed321..1c396c4a786 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -12,7 +12,7 @@ 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 @@ def __init__(self, names): 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 310d7093bb0..e3edbfe9b7c 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 2ee1604a743..f18ce08877a 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 2f9ca6de0b3..51ea1801b72 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -807,8 +807,9 @@ def visit_Assert(self, assert_): ) ) + 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 @@ def visit_Assert(self, assert_): 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 2e875628920..4702e0659da 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 00000000000..1554a229b7f --- /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 18ebc506abb..3002f8abc41 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -314,13 +314,18 @@ def __getattr__(self, name: str) -> MarkDecorator: "{!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 2e875628920..cb6c93159cf 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 1c396c4a786..65855f724ac 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1323,25 +1323,29 @@ def test_foo(x): 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 e9ac09c8e5d..76b6caf1b86 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 395396bd3e7..00000000000 --- 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 8ee3b64628a..00000000000 --- 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 1e0387f4412..00000000000 --- 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 80d1c4a386a..00000000000 --- 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 f6609ddf136..00000000000 --- 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 9eb0c1cd3e5..00000000000 --- 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 b53097c4330..00000000000 --- 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 45d49bdae7a..00000000000 --- 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 68cd66f9902..00000000000 --- 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 a03eb47047b..00000000000 --- 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 c5cd924bb59..00000000000 --- 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 6f5ad648eab..00000000000 --- 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 22ef35cc8dc..00000000000 --- 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 6cf81002ece..00000000000 --- 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 34dfb278de9..00000000000 --- 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 b13a677c8a1..00000000000 --- 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 11a997f713a..00000000000 --- 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 b7334e7fe55..00000000000 --- 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 39ffff99bad..00000000000 --- 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 11f5486257a..00000000000 --- 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 e60d154bb97..00000000000 --- 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 624cf5d1c8f..00000000000 --- 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 32eb849062c..00000000000 --- 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 4fc96ec77b1..00000000000 --- 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 3d77ab528f9..00000000000 --- 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 8e5f4d52aa2..00000000000 --- 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 39787da2e46..00000000000 --- 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 97f7ec74b79..00000000000 --- 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 0960f62035a..00000000000 --- 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 060a2260a2e..00000000000 --- 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 67d8363b39c..00000000000 --- 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 e7c011411c1..6e6914f2df9 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 00000000000..9855a7a2d07 --- /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 ccddb1f6690..8143b3fd47b 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 0e131dace78..c420761a4d1 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 05ccbc9b23e..1570850fc50 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 ea849c1a747..245a67b68d4 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 54bb60da1da..4b8be4469c8 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 8660746bd4f..2f72837919d 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 76b6caf1b86..a63e6f83847 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.