From da3f836ee38991024ccb4e6140980389840a6e36 Mon Sep 17 00:00:00 2001 From: Jeffrey Rackauckas Date: Thu, 4 Apr 2019 20:26:48 -0700 Subject: [PATCH 01/37] Added the junit_log_passing_tests ini flag. --- changelog/4559.feature.rst | 1 + src/_pytest/junitxml.py | 12 ++++++++++++ testing/test_junitxml.py | 27 +++++++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 changelog/4559.feature.rst diff --git a/changelog/4559.feature.rst b/changelog/4559.feature.rst new file mode 100644 index 000000000..ff076aff5 --- /dev/null +++ b/changelog/4559.feature.rst @@ -0,0 +1 @@ +Added the ``junit_log_passing_tests`` ini value which can be used to enable and disable logging passing test output in the Junit XML file. diff --git a/src/_pytest/junitxml.py b/src/_pytest/junitxml.py index 122e0c7ce..faaa94d73 100644 --- a/src/_pytest/junitxml.py +++ b/src/_pytest/junitxml.py @@ -166,6 +166,9 @@ class _NodeReporter(object): self.append(node) def write_captured_output(self, report): + if not self.xml.log_passing_tests and report.passed: + return + content_out = report.capstdout content_log = report.caplog content_err = report.capstderr @@ -354,6 +357,12 @@ def pytest_addoption(parser): "one of no|system-out|system-err", default="no", ) # choices=['no', 'stdout', 'stderr']) + parser.addini( + "junit_log_passing_tests", + "Capture log information for passing tests to JUnit report: ", + type="bool", + default=True, + ) parser.addini( "junit_duration_report", "Duration time to report: one of total|call", @@ -377,6 +386,7 @@ def pytest_configure(config): config.getini("junit_logging"), config.getini("junit_duration_report"), config.getini("junit_family"), + config.getini("junit_log_passing_tests"), ) config.pluginmanager.register(config._xml) @@ -412,12 +422,14 @@ class LogXML(object): logging="no", report_duration="total", family="xunit1", + log_passing_tests=True, ): logfile = os.path.expanduser(os.path.expandvars(logfile)) self.logfile = os.path.normpath(os.path.abspath(logfile)) self.prefix = prefix self.suite_name = suite_name self.logging = logging + self.log_passing_tests = log_passing_tests self.report_duration = report_duration self.family = family self.stats = dict.fromkeys(["error", "passed", "failure", "skipped"], 0) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index 769e8e8a7..07423eeb6 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -1245,3 +1245,30 @@ def test_escaped_skipreason_issue3533(testdir): snode = node.find_first_by_tag("skipped") assert "1 <> 2" in snode.text snode.assert_attr(message="1 <> 2") + + +def test_logging_passing_tests_disabled_does_not_log_test_output(testdir): + testdir.makeini( + """ + [pytest] + junit_log_passing_tests=False + junit_logging=system-out + """ + ) + testdir.makepyfile( + """ + import pytest + import logging + import sys + + def test_func(): + sys.stdout.write('This is stdout') + sys.stderr.write('This is stderr') + logging.warning('hello') + """ + ) + result, dom = runandparse(testdir) + assert result.ret == 0 + node = dom.find_first_by_tag("testcase") + assert len(node.find_by_tag("system-err")) == 0 + assert len(node.find_by_tag("system-out")) == 0 From 0b8b006db49371fe70e51828454d95a0aaa016e3 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 9 May 2019 15:36:49 +0200 Subject: [PATCH 02/37] minor: improve formatting --- src/_pytest/fixtures.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 902904457..53df79d35 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -853,11 +853,9 @@ class FixtureDef(object): exceptions.append(sys.exc_info()) if exceptions: e = exceptions[0] - del ( - exceptions - ) # ensure we don't keep all frames alive because of the traceback + # Ensure to not keep frame references through traceback. + del exceptions six.reraise(*e) - finally: hook = self._fixturemanager.session.gethookproxy(request.node.fspath) hook.pytest_fixture_post_finalizer(fixturedef=self, request=request) From dda21935a73fea602ba128310585e8f9c5934fcb Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 11 May 2019 14:30:54 +0200 Subject: [PATCH 03/37] tests: fix test_trace_after_runpytest It was not really testing what it was supposed to test (e.g. the inner test was not run in the first place). --- testing/test_pdb.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 3b21bacd9..c0ba7159b 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1027,24 +1027,28 @@ def test_trace_after_runpytest(testdir): from _pytest.debugging import pytestPDB def test_outer(testdir): - from _pytest.debugging import pytestPDB - assert len(pytestPDB._saved) == 1 - testdir.runpytest("-k test_inner") + testdir.makepyfile( + \""" + from _pytest.debugging import pytestPDB - __import__('pdb').set_trace() + def test_inner(): + assert len(pytestPDB._saved) == 2 + print() + print("test_inner_" + "end") + \""" + ) - def test_inner(testdir): - assert len(pytestPDB._saved) == 2 + result = testdir.runpytest("-s", "-k", "test_inner") + assert result.ret == 0 + + assert len(pytestPDB._saved) == 1 """ ) - child = testdir.spawn_pytest("-p pytester %s -k test_outer" % p1) - child.expect(r"\(Pdb") - child.sendline("c") - rest = child.read().decode("utf8") - TestPDB.flush(child) - assert child.exitstatus == 0, rest + result = testdir.runpytest_subprocess("-s", "-p", "pytester", str(p1)) + result.stdout.fnmatch_lines(["test_inner_end"]) + assert result.ret == 0 def test_quit_with_swallowed_SystemExit(testdir): From f8e1d58e8f3552b5e0473a735f501f6b147c8b85 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 14 May 2019 06:51:30 +0200 Subject: [PATCH 04/37] minor: settrace != set_trace --- testing/test_pdb.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 3b21bacd9..b9c216eaf 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -817,7 +817,7 @@ class TestPDB(object): result.stdout.fnmatch_lines(["*NameError*xxx*", "*1 error*"]) assert custom_pdb_calls == [] - def test_pdb_custom_cls_with_settrace(self, testdir, monkeypatch): + def test_pdb_custom_cls_with_set_trace(self, testdir, monkeypatch): testdir.makepyfile( custom_pdb=""" class CustomPdb(object): @@ -1129,14 +1129,14 @@ def test_pdbcls_via_local_module(testdir): p1 = testdir.makepyfile( """ def test(): - print("before_settrace") + print("before_set_trace") __import__("pdb").set_trace() """, mypdb=""" class Wrapped: class MyPdb: def set_trace(self, *args): - print("settrace_called", args) + print("set_trace_called", args) def runcall(self, *args, **kwds): print("runcall_called", args, kwds) @@ -1157,7 +1157,7 @@ def test_pdbcls_via_local_module(testdir): str(p1), "--pdbcls=mypdb:Wrapped.MyPdb", syspathinsert=True ) assert result.ret == 0 - result.stdout.fnmatch_lines(["*settrace_called*", "* 1 passed in *"]) + result.stdout.fnmatch_lines(["*set_trace_called*", "* 1 passed in *"]) # Ensure that it also works with --trace. result = testdir.runpytest( From c081c01eb1b4191e1bc241b331b644ca91b900b1 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 14 May 2019 06:51:49 +0200 Subject: [PATCH 05/37] minor: s/no covers/no cover/ --- testing/test_session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_session.py b/testing/test_session.py index 377b28937..03f30f32b 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -171,7 +171,7 @@ class SessionTests(object): ) try: reprec = testdir.inline_run(testdir.tmpdir) - except pytest.skip.Exception: # pragma: no covers + except pytest.skip.Exception: # pragma: no cover pytest.fail("wrong skipped caught") reports = reprec.getreports("pytest_collectreport") assert len(reports) == 1 From ff428bfee1795508cc0515a43b2a1afd1fa0a739 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 15 May 2019 12:48:40 +0200 Subject: [PATCH 06/37] tests: harden test_nonascii_text --- testing/test_assertion.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 8a59b7e8d..e1aff3805 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -605,7 +605,10 @@ class TestAssert_reprcompare(object): return "\xff" expl = callequal(A(), "1") - assert expl + if PY3: + assert expl == ["ÿ == '1'", "+ 1"] + else: + assert expl == [u"\ufffd == '1'", u"+ 1"] def test_format_nonascii_explanation(self): assert util.format_explanation("λ") From d19df5efa24038267c9a3601d99152770fcfe9fd Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 15 May 2019 15:53:23 +0200 Subject: [PATCH 07/37] importorskip: display/include ImportError This can provide useful information, e.g. > could not import 'pyrepl.readline': curses library not found --- changelog/5269.feature.rst | 1 + src/_pytest/outcomes.py | 10 +++++----- testing/test_skipping.py | 8 ++++++++ 3 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 changelog/5269.feature.rst diff --git a/changelog/5269.feature.rst b/changelog/5269.feature.rst new file mode 100644 index 000000000..6247bfd5c --- /dev/null +++ b/changelog/5269.feature.rst @@ -0,0 +1 @@ +``pytest.importorskip`` includes the ``ImportError`` now in the default ``reason``. diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index f57983918..e2a21bb67 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -154,7 +154,7 @@ def importorskip(modname, minversion=None, reason=None): __tracebackhide__ = True compile(modname, "", "eval") # to catch syntaxerrors - should_skip = False + import_exc = None with warnings.catch_warnings(): # make sure to ignore ImportWarnings that might happen because @@ -163,12 +163,12 @@ def importorskip(modname, minversion=None, reason=None): warnings.simplefilter("ignore") try: __import__(modname) - except ImportError: + except ImportError as exc: # Do not raise chained exception here(#1485) - should_skip = True - if should_skip: + import_exc = exc + if import_exc: if reason is None: - reason = "could not import %r" % (modname,) + reason = "could not import %r: %s" % (modname, import_exc) raise Skipped(reason, allow_module_level=True) mod = sys.modules[modname] if minversion is None: diff --git a/testing/test_skipping.py b/testing/test_skipping.py index fb0822f8f..f1b494777 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1177,3 +1177,11 @@ def test_summary_list_after_errors(testdir): "FAILED test_summary_list_after_errors.py::test_fail - assert 0", ] ) + + +def test_importorskip(): + with pytest.raises( + pytest.skip.Exception, + match="^could not import 'doesnotexist': No module named .*", + ): + pytest.importorskip("doesnotexist") From b2ce6f32003a1483ae62a03557baac7478d38726 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 11 Apr 2019 09:29:17 +0200 Subject: [PATCH 08/37] Improve output of ini options in --help Do not cut long help texts, but wrap them the same way as argparse wraps the other help items. --- changelog/5091.feature.rst | 1 + src/_pytest/helpconfig.py | 32 ++++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) create mode 100644 changelog/5091.feature.rst diff --git a/changelog/5091.feature.rst b/changelog/5091.feature.rst new file mode 100644 index 000000000..122c91f53 --- /dev/null +++ b/changelog/5091.feature.rst @@ -0,0 +1 @@ +The output for ini options in ``--help`` has been improved. diff --git a/src/_pytest/helpconfig.py b/src/_pytest/helpconfig.py index 2b383d264..756ad49cb 100644 --- a/src/_pytest/helpconfig.py +++ b/src/_pytest/helpconfig.py @@ -141,24 +141,48 @@ def pytest_cmdline_main(config): def showhelp(config): + import textwrap + reporter = config.pluginmanager.get_plugin("terminalreporter") tw = reporter._tw tw.write(config._parser.optparser.format_help()) tw.line() - tw.line() tw.line( "[pytest] ini-options in the first pytest.ini|tox.ini|setup.cfg file found:" ) tw.line() columns = tw.fullwidth # costly call + indent_len = 24 # based on argparse's max_help_position=24 + indent = " " * indent_len for name in config._parser._ininames: help, type, default = config._parser._inidict[name] if type is None: type = "string" - spec = "%s (%s)" % (name, type) - line = " %-24s %s" % (spec, help) - tw.line(line[:columns]) + spec = "%s (%s):" % (name, type) + tw.write(" %s" % spec) + spec_len = len(spec) + if spec_len > (indent_len - 3): + # Display help starting at a new line. + tw.line() + helplines = textwrap.wrap( + help, + columns, + initial_indent=indent, + subsequent_indent=indent, + break_on_hyphens=False, + ) + + for line in helplines: + tw.line(line) + else: + # Display help starting after the spec, following lines indented. + tw.write(" " * (indent_len - spec_len - 2)) + wrapped = textwrap.wrap(help, columns - indent_len, break_on_hyphens=False) + + tw.line(wrapped[0]) + for line in wrapped[1:]: + tw.line(indent + line) tw.line() tw.line("environment variables:") From f9f41e69a8aa2577dab4b5473c39734b9f7bfb2c Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sun, 14 Apr 2019 23:47:24 +0200 Subject: [PATCH 09/37] reportopts: A: put "Pp" in front --- src/_pytest/terminal.py | 2 +- testing/test_terminal.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 1780792b4..46d694c18 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -170,7 +170,7 @@ def getreportopt(config): if char == "a": reportopts = "sxXwEf" elif char == "A": - reportopts = "sxXwEfpP" + reportopts = "PpsxXwEf" break elif char not in reportopts: reportopts += char diff --git a/testing/test_terminal.py b/testing/test_terminal.py index f269fccd2..77a191a97 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -893,7 +893,7 @@ def test_getreportopt(): assert getreportopt(config) == "sxXwEf" # NOTE: "w" included! config.option.reportchars = "A" - assert getreportopt(config) == "sxXwEfpP" + assert getreportopt(config) == "PpsxXwEf" def test_terminalreporter_reportopt_addopts(testdir): From 6b5152ae131ea2bd15f6c998de223b72b0c9e65d Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Sat, 13 Apr 2019 18:58:38 +0300 Subject: [PATCH 10/37] Sanity tests for loop unrolling --- testing/test_assertrewrite.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 72bfbcc55..c22ae7c6e 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -656,6 +656,12 @@ class TestAssertionRewrite(object): else: assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] + def test_all_unroll(self): + def f(): + assert all(x == 1 for x in range(10)) + + assert "0 != 1" in getmsg(f) + def test_custom_repr_non_ascii(self): def f(): class A(object): From 765f75a8f1d38413fa85ae51f61e6be969aac9d5 Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Sat, 13 Apr 2019 19:13:29 +0300 Subject: [PATCH 11/37] Replace asserts of `any` with an assert in a for --- src/_pytest/assertion/rewrite.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 18506d2e1..68ff2b3ef 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -964,6 +964,8 @@ warn_explicit( """ visit `ast.Call` nodes on Python3.5 and after """ + if call.func.id == "all": + return self.visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] new_args = [] @@ -987,6 +989,22 @@ warn_explicit( outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) return res, outer_expl + def visit_all(self, call): + """Special rewrite for the builtin all function, see #5602""" + if not isinstance(call.args[0], ast.GeneratorExp): + return + gen_exp = call.args[0] + for_loop = ast.For( + iter=gen_exp.generators[0].iter, + target=gen_exp.generators[0].target, + body=[ + self.visit(ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)) + ], + ) + ast.fix_missing_locations(for_loop) + for_loop = ast.copy_location(for_loop, call) + return for_loop, "" + def visit_Starred(self, starred): # From Python 3.5, a Starred node can appear in a function call res, expl = self.visit(starred.value) From 470e686a70df788ddc76ed45607f7cea1e85ec71 Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Sat, 13 Apr 2019 20:11:11 +0300 Subject: [PATCH 12/37] Rewrite unrolled assertion with a new rewriter,correctly append the unrolled for loop --- src/_pytest/assertion/rewrite.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 68ff2b3ef..adf252009 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -994,16 +994,21 @@ warn_explicit( if not isinstance(call.args[0], ast.GeneratorExp): return gen_exp = call.args[0] + assertion_module = ast.Module( + body=[ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)] + ) + AssertionRewriter(None, None).run(assertion_module) for_loop = ast.For( iter=gen_exp.generators[0].iter, target=gen_exp.generators[0].target, - body=[ - self.visit(ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)) - ], + body=assertion_module.body, + orelse=[], ) - ast.fix_missing_locations(for_loop) - for_loop = ast.copy_location(for_loop, call) - return for_loop, "" + self.statements.append(for_loop) + return ( + ast.Num(n=1), + "", + ) # Return an empty expression, all the asserts are in the for_loop def visit_Starred(self, starred): # From Python 3.5, a Starred node can appear in a function call From ddbe733666d56b7c997bd901071f1608a0a90873 Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Sat, 13 Apr 2019 20:57:05 +0300 Subject: [PATCH 13/37] Add changelog entry for 5062 --- changelog/5062.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/5062.feature.rst diff --git a/changelog/5062.feature.rst b/changelog/5062.feature.rst new file mode 100644 index 000000000..e311d161d --- /dev/null +++ b/changelog/5062.feature.rst @@ -0,0 +1 @@ +Unroll calls to ``all`` to full for-loops for better failure messages, especially when using Generator Expressions. From a0dbf2ab995780140ce335fb96e0f797c661764b Mon Sep 17 00:00:00 2001 From: danielx123 Date: Mon, 22 Apr 2019 17:58:07 -0400 Subject: [PATCH 14/37] Adding test cases for unrolling an iterable #5062 --- testing/test_assertrewrite.py | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index c22ae7c6e..8add65e0a 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -677,6 +677,53 @@ class TestAssertionRewrite(object): assert "UnicodeDecodeError" not in msg assert "UnicodeEncodeError" not in msg + def test_generator(self, testdir): + testdir.makepyfile( + """ + def check_even(num): + if num % 2 == 0: + return True + return False + + def test_generator(): + odd_list = list(range(1,9,2)) + assert all(check_even(num) for num in odd_list)""" + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) + + def test_list_comprehension(self, testdir): + testdir.makepyfile( + """ + def check_even(num): + if num % 2 == 0: + return True + return False + + def test_list_comprehension(): + odd_list = list(range(1,9,2)) + assert all([check_even(num) for num in odd_list])""" + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) + + def test_for_loop(self, testdir): + testdir.makepyfile( + """ + def check_even(num): + if num % 2 == 0: + return True + return False + + def test_for_loop(): + odd_list = list(range(1,9,2)) + for num in odd_list: + assert check_even(num) + """ + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) + class TestRewriteOnImport(object): def test_pycache_is_a_file(self, testdir): From 0996f3dbc54d5b0d096e7341cb6718ffd16da100 Mon Sep 17 00:00:00 2001 From: danielx123 Date: Mon, 22 Apr 2019 18:23:51 -0400 Subject: [PATCH 15/37] Displaying pip list command's packages and versions #5062 --- src/_pytest/terminal.py | 6 ++++++ testing/test_terminal.py | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 2d7132259..98cbaa571 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -9,6 +9,7 @@ from __future__ import print_function import argparse import collections import platform +import subprocess import sys import time from functools import partial @@ -581,6 +582,11 @@ class TerminalReporter(object): ): msg += " -- " + str(sys.executable) self.write_line(msg) + pipe = subprocess.Popen("pip list", shell=True, stdout=subprocess.PIPE).stdout + package_msg = pipe.read() + package_msg = package_msg[:-2] + if package_msg: + self.write_line(package_msg) lines = self.config.hook.pytest_report_header( config=self.config, startdir=self.startdir ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index feacc242d..bfa928d74 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -279,6 +279,15 @@ class TestTerminal(object): tr.rewrite("hey", erase=True) assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ") + def test_packages_display(self, testdir): + testdir.makepyfile( + """ + def test_pass(num): + assert 1 == 1""" + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*Package*", "*Version*"]) + class TestCollectonly(object): def test_collectonly_basic(self, testdir): From c607697400affcc0e50e51baa329aec81ba3cf7b Mon Sep 17 00:00:00 2001 From: danielx123 Date: Mon, 22 Apr 2019 21:29:08 -0400 Subject: [PATCH 16/37] Fixed test case --- testing/test_terminal.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index bfa928d74..0b57114e5 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -286,7 +286,7 @@ class TestTerminal(object): assert 1 == 1""" ) result = testdir.runpytest() - result.stdout.fnmatch_lines(["*Package*", "*Version*"]) + result.stdout.fnmatch_lines(["*Package Version *"]) class TestCollectonly(object): From ecd2de25a122cd79f2e9b37b267fe883272383c6 Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Thu, 9 May 2019 17:39:24 +0300 Subject: [PATCH 17/37] Revert "Displaying pip list command's packages and versions #5062" This reverts commit 043fdb7c4065e5eb54f3fb5776077bb8fd651ce6. These tests were part of the PR #5155 but weren't relevant to #5602 --- src/_pytest/terminal.py | 6 ------ testing/test_terminal.py | 9 --------- 2 files changed, 15 deletions(-) diff --git a/src/_pytest/terminal.py b/src/_pytest/terminal.py index 98cbaa571..2d7132259 100644 --- a/src/_pytest/terminal.py +++ b/src/_pytest/terminal.py @@ -9,7 +9,6 @@ from __future__ import print_function import argparse import collections import platform -import subprocess import sys import time from functools import partial @@ -582,11 +581,6 @@ class TerminalReporter(object): ): msg += " -- " + str(sys.executable) self.write_line(msg) - pipe = subprocess.Popen("pip list", shell=True, stdout=subprocess.PIPE).stdout - package_msg = pipe.read() - package_msg = package_msg[:-2] - if package_msg: - self.write_line(package_msg) lines = self.config.hook.pytest_report_header( config=self.config, startdir=self.startdir ) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 0b57114e5..feacc242d 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -279,15 +279,6 @@ class TestTerminal(object): tr.rewrite("hey", erase=True) assert f.getvalue() == "hello" + "\r" + "hey" + (6 * " ") - def test_packages_display(self, testdir): - testdir.makepyfile( - """ - def test_pass(num): - assert 1 == 1""" - ) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["*Package Version *"]) - class TestCollectonly(object): def test_collectonly_basic(self, testdir): From e37ff3042e711dd930ed8cd315b3b93340546a25 Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Thu, 9 May 2019 17:50:41 +0300 Subject: [PATCH 18/37] Check calls to all only if it's a name and not an attribute --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index adf252009..0a3713285 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -964,7 +964,7 @@ warn_explicit( """ visit `ast.Call` nodes on Python3.5 and after """ - if call.func.id == "all": + if isinstance(call.func, ast.Name) and call.func.id == "all": return self.visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] From 437d6452c1a37ef1412adb3fd79bc844f9d05c3a Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Thu, 9 May 2019 18:51:03 +0300 Subject: [PATCH 19/37] Expand list comprehensions as well --- src/_pytest/assertion/rewrite.py | 4 ++-- testing/test_assertrewrite.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 0a3713285..0e988a8f4 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -991,13 +991,13 @@ warn_explicit( def visit_all(self, call): """Special rewrite for the builtin all function, see #5602""" - if not isinstance(call.args[0], ast.GeneratorExp): + if not isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)): return gen_exp = call.args[0] assertion_module = ast.Module( body=[ast.Assert(test=gen_exp.elt, lineno=1, msg="", col_offset=1)] ) - AssertionRewriter(None, None).run(assertion_module) + AssertionRewriter(module_path=None, config=None).run(assertion_module) for_loop = ast.For( iter=gen_exp.generators[0].iter, target=gen_exp.generators[0].target, diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 8add65e0a..c453fe57d 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -677,7 +677,7 @@ class TestAssertionRewrite(object): assert "UnicodeDecodeError" not in msg assert "UnicodeEncodeError" not in msg - def test_generator(self, testdir): + def test_unroll_generator(self, testdir): testdir.makepyfile( """ def check_even(num): @@ -692,7 +692,7 @@ class TestAssertionRewrite(object): result = testdir.runpytest() result.stdout.fnmatch_lines(["*assert False*", "*where False = check_even(1)*"]) - def test_list_comprehension(self, testdir): + def test_unroll_list_comprehension(self, testdir): testdir.makepyfile( """ def check_even(num): From 852fb6a4ae93c68bd03f36ca4daf7e9642d8e2d2 Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Thu, 9 May 2019 19:02:50 +0300 Subject: [PATCH 20/37] Change basic test case to be consistent with existing assertion rewriting The code ``` x = 0 assert x == 1 ``` will give the failure message 0 == 1, so it shouldn't be different as part of an unroll --- testing/test_assertrewrite.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index c453fe57d..e819075b5 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -656,11 +656,11 @@ class TestAssertionRewrite(object): else: assert lines == ["assert 0 == 1\n + where 1 = \\n{ \\n~ \\n}.a"] - def test_all_unroll(self): + def test_unroll_expression(self): def f(): assert all(x == 1 for x in range(10)) - assert "0 != 1" in getmsg(f) + assert "0 == 1" in getmsg(f) def test_custom_repr_non_ascii(self): def f(): From 58149459a59c7b7d2f34e7291482d74e39d8ad69 Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Thu, 9 May 2019 19:08:10 +0300 Subject: [PATCH 21/37] Mark visit_all as a private method --- 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 0e988a8f4..bb3d36a9c 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -965,7 +965,7 @@ warn_explicit( visit `ast.Call` nodes on Python3.5 and after """ if isinstance(call.func, ast.Name) and call.func.id == "all": - return self.visit_all(call) + return self._visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] new_args = [] @@ -989,7 +989,7 @@ warn_explicit( outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl) return res, outer_expl - def visit_all(self, call): + def _visit_all(self, call): """Special rewrite for the builtin all function, see #5602""" if not isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)): return From 322a0f0a331494746c20a56f7776aec1f08d9240 Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Thu, 9 May 2019 19:12:58 +0300 Subject: [PATCH 22/37] Fix mention of issue #5062 in docstrings --- src/_pytest/assertion/rewrite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index bb3d36a9c..5690e0e33 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -990,7 +990,7 @@ warn_explicit( return res, outer_expl def _visit_all(self, call): - """Special rewrite for the builtin all function, see #5602""" + """Special rewrite for the builtin all function, see #5062""" if not isinstance(call.args[0], (ast.GeneratorExp, ast.ListComp)): return gen_exp = call.args[0] From 22d91a3c3a248761506a38fdf26c859b341e82be Mon Sep 17 00:00:00 2001 From: Tomer Keren Date: Sat, 18 May 2019 14:27:47 +0300 Subject: [PATCH 23/37] Unroll calls to all on python 2 --- src/_pytest/assertion/rewrite.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 5690e0e33..e3870d435 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -1020,6 +1020,8 @@ warn_explicit( """ visit `ast.Call nodes on 3.4 and below` """ + if isinstance(call.func, ast.Name) and call.func.id == "all": + return self._visit_all(call) new_func, func_expl = self.visit(call.func) arg_expls = [] new_args = [] From 13f02af97d676bdf7143aa1834c8898fbf753d87 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Sat, 6 Apr 2019 17:32:47 -0700 Subject: [PATCH 24/37] Switch to importlib-metadata --- changelog/5063.feature.rst | 1 + setup.py | 5 +- src/_pytest/assertion/rewrite.py | 19 ----- src/_pytest/config/__init__.py | 21 ++---- src/_pytest/outcomes.py | 12 +-- testing/acceptance_test.py | 31 +++----- testing/test_assertion.py | 56 +++++--------- testing/test_config.py | 121 ++++++++++++------------------- testing/test_entry_points.py | 14 +--- 9 files changed, 95 insertions(+), 185 deletions(-) create mode 100644 changelog/5063.feature.rst diff --git a/changelog/5063.feature.rst b/changelog/5063.feature.rst new file mode 100644 index 000000000..21379ef0a --- /dev/null +++ b/changelog/5063.feature.rst @@ -0,0 +1 @@ +Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time. diff --git a/setup.py b/setup.py index 0fb5a58a2..172703cee 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ from setuptools import setup INSTALL_REQUIRES = [ "py>=1.5.0", "six>=1.10.0", - "setuptools", + "packaging", "attrs>=17.4.0", 'more-itertools>=4.0.0,<6.0.0;python_version<="2.7"', 'more-itertools>=4.0.0;python_version>"2.7"', @@ -13,7 +13,8 @@ INSTALL_REQUIRES = [ 'funcsigs>=1.0;python_version<"3.0"', 'pathlib2>=2.2.0;python_version<"3.6"', 'colorama;sys_platform=="win32"', - "pluggy>=0.9,!=0.10,<1.0", + "pluggy>=0.12,<1.0", + "importlib-metadata>=0.12", "wcwidth", ] diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 2903b8995..cae7f86a8 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -64,7 +64,6 @@ class AssertionRewritingHook(object): self.session = None self.modules = {} self._rewritten_names = set() - self._register_with_pkg_resources() self._must_rewrite = set() # flag to guard against trying to rewrite a pyc file while we are already writing another pyc file, # which might result in infinite recursion (#3506) @@ -315,24 +314,6 @@ class AssertionRewritingHook(object): tp = desc[2] return tp == imp.PKG_DIRECTORY - @classmethod - def _register_with_pkg_resources(cls): - """ - Ensure package resources can be loaded from this loader. May be called - multiple times, as the operation is idempotent. - """ - try: - import pkg_resources - - # access an attribute in case a deferred importer is present - pkg_resources.__name__ - except ImportError: - return - - # Since pytest tests are always located in the file system, the - # DefaultProvider is appropriate. - pkg_resources.register_loader_type(cls, pkg_resources.DefaultProvider) - def get_data(self, pathname): """Optional PEP302 get_data API. """ diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 03769b815..25d0cd745 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -12,8 +12,10 @@ import sys import types import warnings +import importlib_metadata import py import six +from packaging.version import Version from pluggy import HookimplMarker from pluggy import HookspecMarker from pluggy import PluginManager @@ -787,25 +789,17 @@ class Config(object): modules or packages in the distribution package for all pytest plugins. """ - import pkg_resources - self.pluginmanager.rewrite_hook = hook if os.environ.get("PYTEST_DISABLE_PLUGIN_AUTOLOAD"): # We don't autoload from setuptools entry points, no need to continue. return - # 'RECORD' available for plugins installed normally (pip install) - # 'SOURCES.txt' available for plugins installed in dev mode (pip install -e) - # for installed plugins 'SOURCES.txt' returns an empty list, and vice-versa - # so it shouldn't be an issue - metadata_files = "RECORD", "SOURCES.txt" - package_files = ( - entry.split(",")[0] - for entrypoint in pkg_resources.iter_entry_points("pytest11") - for metadata in metadata_files - for entry in entrypoint.dist._get_metadata(metadata) + str(file) + for dist in importlib_metadata.distributions() + if any(ep.group == "pytest11" for ep in dist.entry_points) + for file in dist.files ) for name in _iter_rewritable_modules(package_files): @@ -874,11 +868,10 @@ class Config(object): def _checkversion(self): import pytest - from pkg_resources import parse_version minver = self.inicfg.get("minversion", None) if minver: - if parse_version(minver) > parse_version(pytest.__version__): + if Version(minver) > Version(pytest.__version__): raise pytest.UsageError( "%s:%d: requires pytest-%s, actual pytest-%s'" % ( diff --git a/src/_pytest/outcomes.py b/src/_pytest/outcomes.py index e2a21bb67..7ee1ce7c9 100644 --- a/src/_pytest/outcomes.py +++ b/src/_pytest/outcomes.py @@ -8,6 +8,8 @@ from __future__ import print_function import sys +from packaging.version import Version + class OutcomeException(BaseException): """ OutcomeException and its subclass instances indicate and @@ -175,15 +177,7 @@ def importorskip(modname, minversion=None, reason=None): return mod verattr = getattr(mod, "__version__", None) if minversion is not None: - try: - from pkg_resources import parse_version as pv - except ImportError: - raise Skipped( - "we have a required version for %r but can not import " - "pkg_resources to parse version strings." % (modname,), - allow_module_level=True, - ) - if verattr is None or pv(verattr) < pv(minversion): + if verattr is None or Version(verattr) < Version(minversion): raise Skipped( "module %r has __version__ %r, required is: %r" % (modname, verattr, minversion), diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b0c682900..7016cf13b 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -9,6 +9,7 @@ import textwrap import types import attr +import importlib_metadata import py import six @@ -111,8 +112,6 @@ class TestGeneralUsage(object): @pytest.mark.parametrize("load_cov_early", [True, False]) def test_early_load_setuptools_name(self, testdir, monkeypatch, load_cov_early): - pkg_resources = pytest.importorskip("pkg_resources") - testdir.makepyfile(mytestplugin1_module="") testdir.makepyfile(mytestplugin2_module="") testdir.makepyfile(mycov_module="") @@ -124,38 +123,28 @@ class TestGeneralUsage(object): class DummyEntryPoint(object): name = attr.ib() module = attr.ib() - version = "1.0" - - @property - def project_name(self): - return self.name + group = "pytest11" def load(self): __import__(self.module) loaded.append(self.name) return sys.modules[self.module] - @property - def dist(self): - return self - - def _get_metadata(self, *args): - return [] - entry_points = [ DummyEntryPoint("myplugin1", "mytestplugin1_module"), DummyEntryPoint("myplugin2", "mytestplugin2_module"), DummyEntryPoint("mycov", "mycov_module"), ] - def my_iter(group, name=None): - assert group == "pytest11" - for ep in entry_points: - if name is not None and ep.name != name: - continue - yield ep + @attr.s + class DummyDist(object): + entry_points = attr.ib() + files = () - monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) + def my_dists(): + return (DummyDist(entry_points),) + + monkeypatch.setattr(importlib_metadata, "distributions", my_dists) params = ("-p", "mycov") if load_cov_early else () testdir.runpytest_inprocess(*params) if load_cov_early: diff --git a/testing/test_assertion.py b/testing/test_assertion.py index e1aff3805..2085ffd8b 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -137,12 +137,12 @@ class TestImportHookInstallation(object): def test_pytest_plugins_rewrite_module_names_correctly(self, testdir): """Test that we match files correctly when they are marked for rewriting (#2939).""" contents = { - "conftest.py": """ + "conftest.py": """\ pytest_plugins = "ham" """, "ham.py": "", "hamster.py": "", - "test_foo.py": """ + "test_foo.py": """\ def test_foo(pytestconfig): assert pytestconfig.pluginmanager.rewrite_hook.find_module('ham') is not None assert pytestconfig.pluginmanager.rewrite_hook.find_module('hamster') is None @@ -153,14 +153,13 @@ class TestImportHookInstallation(object): assert result.ret == 0 @pytest.mark.parametrize("mode", ["plain", "rewrite"]) - @pytest.mark.parametrize("plugin_state", ["development", "installed"]) - def test_installed_plugin_rewrite(self, testdir, mode, plugin_state, monkeypatch): + def test_installed_plugin_rewrite(self, testdir, mode, monkeypatch): monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) # Make sure the hook is installed early enough so that plugins # installed via setuptools are rewritten. testdir.tmpdir.join("hampkg").ensure(dir=1) contents = { - "hampkg/__init__.py": """ + "hampkg/__init__.py": """\ import pytest @pytest.fixture @@ -169,7 +168,7 @@ class TestImportHookInstallation(object): assert values.pop(0) == value return check """, - "spamplugin.py": """ + "spamplugin.py": """\ import pytest from hampkg import check_first2 @@ -179,46 +178,31 @@ class TestImportHookInstallation(object): assert values.pop(0) == value return check """, - "mainwrapper.py": """ - import pytest, pkg_resources - - plugin_state = "{plugin_state}" - - class DummyDistInfo(object): - project_name = 'spam' - version = '1.0' - - def _get_metadata(self, name): - # 'RECORD' meta-data only available in installed plugins - if name == 'RECORD' and plugin_state == "installed": - return ['spamplugin.py,sha256=abc,123', - 'hampkg/__init__.py,sha256=abc,123'] - # 'SOURCES.txt' meta-data only available for plugins in development mode - elif name == 'SOURCES.txt' and plugin_state == "development": - return ['spamplugin.py', - 'hampkg/__init__.py'] - return [] + "mainwrapper.py": """\ + import pytest, importlib_metadata class DummyEntryPoint(object): name = 'spam' module_name = 'spam.py' - attrs = () - extras = None - dist = DummyDistInfo() + group = 'pytest11' - def load(self, require=True, *args, **kwargs): + def load(self): import spamplugin return spamplugin - def iter_entry_points(group, name=None): - yield DummyEntryPoint() + class DummyDistInfo(object): + version = '1.0' + files = ('spamplugin.py', 'hampkg/__init__.py') + entry_points = (DummyEntryPoint(),) + metadata = {'name': 'foo'} - pkg_resources.iter_entry_points = iter_entry_points + def distributions(): + return (DummyDistInfo(),) + + importlib_metadata.distributions = distributions pytest.main() - """.format( - plugin_state=plugin_state - ), - "test_foo.py": """ + """, + "test_foo.py": """\ def test(check_first): check_first([10, 30], 30) diff --git a/testing/test_config.py b/testing/test_config.py index ecb8fd403..c8590ca37 100644 --- a/testing/test_config.py +++ b/testing/test_config.py @@ -5,7 +5,7 @@ from __future__ import print_function import sys import textwrap -import attr +import importlib_metadata import _pytest._code import pytest @@ -531,32 +531,26 @@ def test_options_on_small_file_do_not_blow_up(testdir): def test_preparse_ordering_with_setuptools(testdir, monkeypatch): - pkg_resources = pytest.importorskip("pkg_resources") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) - def my_iter(group, name=None): - assert group == "pytest11" + class EntryPoint(object): + name = "mytestplugin" + group = "pytest11" - class Dist(object): - project_name = "spam" - version = "1.0" + def load(self): + class PseudoPlugin(object): + x = 42 - def _get_metadata(self, name): - return ["foo.txt,sha256=abc,123"] + return PseudoPlugin() - class EntryPoint(object): - name = "mytestplugin" - dist = Dist() + class Dist(object): + files = () + entry_points = (EntryPoint(),) - def load(self): - class PseudoPlugin(object): - x = 42 + def my_dists(): + return (Dist,) - return PseudoPlugin() - - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) + monkeypatch.setattr(importlib_metadata, "distributions", my_dists) testdir.makeconftest( """ pytest_plugins = "mytestplugin", @@ -569,60 +563,50 @@ def test_preparse_ordering_with_setuptools(testdir, monkeypatch): def test_setuptools_importerror_issue1479(testdir, monkeypatch): - pkg_resources = pytest.importorskip("pkg_resources") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) - def my_iter(group, name=None): - assert group == "pytest11" + class DummyEntryPoint(object): + name = "mytestplugin" + group = "pytest11" - class Dist(object): - project_name = "spam" - version = "1.0" + def load(self): + raise ImportError("Don't hide me!") - def _get_metadata(self, name): - return ["foo.txt,sha256=abc,123"] + class Distribution(object): + version = "1.0" + files = ("foo.txt",) + entry_points = (DummyEntryPoint(),) - class EntryPoint(object): - name = "mytestplugin" - dist = Dist() + def distributions(): + return (Distribution(),) - def load(self): - raise ImportError("Don't hide me!") - - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) + monkeypatch.setattr(importlib_metadata, "distributions", distributions) with pytest.raises(ImportError): testdir.parseconfig() @pytest.mark.parametrize("block_it", [True, False]) def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block_it): - pkg_resources = pytest.importorskip("pkg_resources") monkeypatch.delenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", raising=False) plugin_module_placeholder = object() - def my_iter(group, name=None): - assert group == "pytest11" + class DummyEntryPoint(object): + name = "mytestplugin" + group = "pytest11" - class Dist(object): - project_name = "spam" - version = "1.0" + def load(self): + return plugin_module_placeholder - def _get_metadata(self, name): - return ["foo.txt,sha256=abc,123"] + class Distribution(object): + version = "1.0" + files = ("foo.txt",) + entry_points = (DummyEntryPoint(),) - class EntryPoint(object): - name = "mytestplugin" - dist = Dist() + def distributions(): + return (Distribution(),) - def load(self): - return plugin_module_placeholder - - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) + monkeypatch.setattr(importlib_metadata, "distributions", distributions) args = ("-p", "no:mytestplugin") if block_it else () config = testdir.parseconfig(*args) config.pluginmanager.import_plugin("mytestplugin") @@ -639,37 +623,26 @@ def test_plugin_preparse_prevents_setuptools_loading(testdir, monkeypatch, block "parse_args,should_load", [(("-p", "mytestplugin"), True), ((), False)] ) def test_disable_plugin_autoload(testdir, monkeypatch, parse_args, should_load): - pkg_resources = pytest.importorskip("pkg_resources") - - def my_iter(group, name=None): - assert group == "pytest11" - assert name == "mytestplugin" - return iter([DummyEntryPoint()]) - - @attr.s class DummyEntryPoint(object): - name = "mytestplugin" + project_name = name = "mytestplugin" + group = "pytest11" version = "1.0" - @property - def project_name(self): - return self.name - def load(self): return sys.modules[self.name] - @property - def dist(self): - return self - - def _get_metadata(self, *args): - return [] + class Distribution(object): + entry_points = (DummyEntryPoint(),) + files = () class PseudoPlugin(object): x = 42 + def distributions(): + return (Distribution(),) + monkeypatch.setenv("PYTEST_DISABLE_PLUGIN_AUTOLOAD", "1") - monkeypatch.setattr(pkg_resources, "iter_entry_points", my_iter) + monkeypatch.setattr(importlib_metadata, "distributions", distributions) monkeypatch.setitem(sys.modules, "mytestplugin", PseudoPlugin()) config = testdir.parseconfig(*parse_args) has_loaded = config.pluginmanager.get_plugin("mytestplugin") is not None diff --git a/testing/test_entry_points.py b/testing/test_entry_points.py index dcb9dd525..0adae0b1a 100644 --- a/testing/test_entry_points.py +++ b/testing/test_entry_points.py @@ -2,16 +2,10 @@ from __future__ import absolute_import from __future__ import division from __future__ import print_function -import pkg_resources - -import pytest - - -@pytest.mark.parametrize("entrypoint", ["py.test", "pytest"]) -def test_entry_point_exist(entrypoint): - assert entrypoint in pkg_resources.get_entry_map("pytest")["console_scripts"] +import importlib_metadata def test_pytest_entry_points_are_identical(): - entryMap = pkg_resources.get_entry_map("pytest")["console_scripts"] - assert entryMap["pytest"].module_name == entryMap["py.test"].module_name + dist = importlib_metadata.distribution("pytest") + entry_map = {ep.name: ep for ep in dist.entry_points} + assert entry_map["pytest"].value == entry_map["py.test"].value From 220a2a1bc9b2e92a38207a165c0706cb7d690916 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 28 May 2019 18:16:00 +0200 Subject: [PATCH 25/37] Remove _pytest.compat.NoneType --- src/_pytest/compat.py | 1 - src/_pytest/python.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 19863dd83..7668c3a94 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -37,7 +37,6 @@ if _PY3: else: from funcsigs import signature, Parameter as Parameter -NoneType = type(None) NOTSET = object() PY35 = sys.version_info[:2] >= (3, 5) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 035369a59..961a3af5d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -31,7 +31,6 @@ from _pytest.compat import getlocation from _pytest.compat import is_generator from _pytest.compat import isclass from _pytest.compat import isfunction -from _pytest.compat import NoneType from _pytest.compat import NOTSET from _pytest.compat import REGEX_TYPE from _pytest.compat import safe_getattr @@ -1192,7 +1191,7 @@ def _idval(val, argname, idx, idfn, item, config): if isinstance(val, STRING_TYPES): return _ascii_escaped_by_config(val, config) - elif isinstance(val, (float, int, bool, NoneType)): + elif val is None or isinstance(val, (float, int, bool)): return str(val) elif isinstance(val, REGEX_TYPE): return ascii_escaped(val.pattern) From 97d8e9fbecc3a2f496522928e24f75889a3d1544 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 28 May 2019 18:25:45 +0200 Subject: [PATCH 26/37] minor: getbasetemp: dedent, improve assert --- src/_pytest/tmpdir.py | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/_pytest/tmpdir.py b/src/_pytest/tmpdir.py index f1b16fb5c..a8a703771 100644 --- a/src/_pytest/tmpdir.py +++ b/src/_pytest/tmpdir.py @@ -60,29 +60,29 @@ class TempPathFactory(object): def getbasetemp(self): """ return base temporary directory. """ - if self._basetemp is None: - if self._given_basetemp is not None: - basetemp = self._given_basetemp - ensure_reset_dir(basetemp) - basetemp = basetemp.resolve() - else: - from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") - temproot = Path(from_env or tempfile.gettempdir()).resolve() - user = get_user() or "unknown" - # use a sub-directory in the temproot to speed-up - # make_numbered_dir() call - rootdir = temproot.joinpath("pytest-of-{}".format(user)) - rootdir.mkdir(exist_ok=True) - basetemp = make_numbered_dir_with_cleanup( - prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT - ) - assert basetemp is not None - self._basetemp = t = basetemp - self._trace("new basetemp", t) - return t - else: + if self._basetemp is not None: return self._basetemp + if self._given_basetemp is not None: + basetemp = self._given_basetemp + ensure_reset_dir(basetemp) + basetemp = basetemp.resolve() + else: + from_env = os.environ.get("PYTEST_DEBUG_TEMPROOT") + temproot = Path(from_env or tempfile.gettempdir()).resolve() + user = get_user() or "unknown" + # use a sub-directory in the temproot to speed-up + # make_numbered_dir() call + rootdir = temproot.joinpath("pytest-of-{}".format(user)) + rootdir.mkdir(exist_ok=True) + basetemp = make_numbered_dir_with_cleanup( + prefix="pytest-", root=rootdir, keep=3, lock_timeout=LOCK_TIMEOUT + ) + assert basetemp is not None, basetemp + self._basetemp = t = basetemp + self._trace("new basetemp", t) + return t + @attr.s class TempdirFactory(object): From d4b85da8c776f265fdbdada6bb6d3e986d34029a Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Sat, 25 May 2019 10:13:29 +0200 Subject: [PATCH 27/37] Use same code for setting up cli/non-cli formatter A method _create_formatter was introduced that is used for both the log_cli_formatter and the log_formatter. Consequences of this commit are: * Captured logs that are output for each failing test are formatted using the ColoredLevelFromatter. * The formatter used for writing to a file still uses the non-colored logging.Formatter class. --- changelog/5311.feature.rst | 2 ++ src/_pytest/logging.py | 45 ++++++++++++++++++------------- testing/logging/test_reporting.py | 45 +++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 19 deletions(-) create mode 100644 changelog/5311.feature.rst diff --git a/changelog/5311.feature.rst b/changelog/5311.feature.rst new file mode 100644 index 000000000..e96be0f9e --- /dev/null +++ b/changelog/5311.feature.rst @@ -0,0 +1,2 @@ +Captured logs that are output for each failing test are formatted using the +ColoredLevelFromatter. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 08670d2b2..cb59d36da 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -17,6 +17,11 @@ from _pytest.pathlib import Path DEFAULT_LOG_FORMAT = "%(levelname)-8s %(name)s:%(filename)s:%(lineno)d %(message)s" DEFAULT_LOG_DATE_FORMAT = "%H:%M:%S" +_ANSI_ESCAPE_SEQ = re.compile(r"\x1b\[[\d;]+m") + + +def _remove_ansi_escape_sequences(text): + return _ANSI_ESCAPE_SEQ.sub("", text) class ColoredLevelFormatter(logging.Formatter): @@ -256,8 +261,8 @@ class LogCaptureFixture(object): @property def text(self): - """Returns the log text.""" - return self.handler.stream.getvalue() + """Returns the formatted log text.""" + return _remove_ansi_escape_sequences(self.handler.stream.getvalue()) @property def records(self): @@ -393,7 +398,7 @@ class LoggingPlugin(object): config.option.verbose = 1 self.print_logs = get_option_ini(config, "log_print") - self.formatter = logging.Formatter( + self.formatter = self._create_formatter( get_option_ini(config, "log_format"), get_option_ini(config, "log_date_format"), ) @@ -427,6 +432,19 @@ class LoggingPlugin(object): if self._log_cli_enabled(): self._setup_cli_logging() + def _create_formatter(self, log_format, log_date_format): + # 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( + log_format + ): + formatter = ColoredLevelFormatter( + create_terminal_writer(self._config), log_format, log_date_format + ) + else: + formatter = logging.Formatter(log_format, log_date_format) + return formatter + def _setup_cli_logging(self): config = self._config terminal_reporter = config.pluginmanager.get_plugin("terminalreporter") @@ -437,23 +455,12 @@ class LoggingPlugin(object): capture_manager = config.pluginmanager.get_plugin("capturemanager") # if capturemanager plugin is disabled, live logging still works. log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager) - log_cli_format = get_option_ini(config, "log_cli_format", "log_format") - log_cli_date_format = get_option_ini( - config, "log_cli_date_format", "log_date_format" + + 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"), ) - if ( - config.option.color != "no" - and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format) - ): - log_cli_formatter = ColoredLevelFormatter( - create_terminal_writer(config), - log_cli_format, - datefmt=log_cli_date_format, - ) - else: - log_cli_formatter = logging.Formatter( - log_cli_format, datefmt=log_cli_date_format - ) + log_cli_level = get_actual_log_level(config, "log_cli_level", "log_level") self.log_cli_handler = log_cli_handler self.live_logs_context = lambda: catching_logs( diff --git a/testing/logging/test_reporting.py b/testing/logging/test_reporting.py index 77cf71b43..e7a3a80eb 100644 --- a/testing/logging/test_reporting.py +++ b/testing/logging/test_reporting.py @@ -1084,3 +1084,48 @@ def test_log_set_path(testdir): with open(os.path.join(report_dir_base, "test_second"), "r") as rfh: content = rfh.read() assert "message from test 2" in content + + +def test_colored_captured_log(testdir): + """ + Test that the level names of captured log messages of a failing test are + colored. + """ + testdir.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(): + logger.info('text going to logger from call') + assert False + """ + ) + result = testdir.runpytest("--log-level=INFO", "--color=yes") + assert result.ret == 1 + result.stdout.fnmatch_lines( + [ + "*-- Captured log call --*", + "\x1b[32mINFO \x1b[0m*text going to logger from call", + ] + ) + + +def test_colored_ansi_esc_caplogtext(testdir): + """ + Make sure that caplog.text does not contain ANSI escape sequences. + """ + testdir.makepyfile( + """ + import logging + + logger = logging.getLogger(__name__) + + def test_foo(caplog): + logger.info('text going to logger from call') + assert '\x1b' not in caplog.text + """ + ) + result = testdir.runpytest("--log-level=INFO", "--color=yes") + assert result.ret == 0 From 31b1c4ca0c07f7d1653134f79dc028e0e9516914 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Wed, 29 May 2019 22:00:34 +0200 Subject: [PATCH 28/37] Update changelog/5311.feature.rst Co-Authored-By: Daniel Hahler --- changelog/5311.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog/5311.feature.rst b/changelog/5311.feature.rst index e96be0f9e..ae7ef99ac 100644 --- a/changelog/5311.feature.rst +++ b/changelog/5311.feature.rst @@ -1,2 +1,2 @@ Captured logs that are output for each failing test are formatted using the -ColoredLevelFromatter. +ColoredLevelFormatter. From ea3ebec117b6f06b0a584f39ecaa8e75976a6e37 Mon Sep 17 00:00:00 2001 From: Thomas Hisch Date: Fri, 24 May 2019 04:32:22 +0200 Subject: [PATCH 29/37] logging: Improve formatting of multiline message --- changelog/5312.feature.rst | 1 + src/_pytest/logging.py | 33 +++++++++++++++++++++++++++++++ testing/logging/test_formatter.py | 30 ++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 changelog/5312.feature.rst diff --git a/changelog/5312.feature.rst b/changelog/5312.feature.rst new file mode 100644 index 000000000..bcd5a736b --- /dev/null +++ b/changelog/5312.feature.rst @@ -0,0 +1 @@ +Improved formatting of multiline log messages in python3. diff --git a/src/_pytest/logging.py b/src/_pytest/logging.py index 6555710bf..577a5407b 100644 --- a/src/_pytest/logging.py +++ b/src/_pytest/logging.py @@ -77,6 +77,36 @@ class ColoredLevelFormatter(logging.Formatter): return super(ColoredLevelFormatter, self).format(record) +if not six.PY2: + # Formatter classes don't support format styles in PY2 + + class PercentStyleMultiline(logging.PercentStyle): + """A logging style with special support for multiline messages. + + If the message of a record consists of multiple lines, this style + formats the message as if each line were logged separately. + """ + + @staticmethod + def _update_message(record_dict, message): + tmp = record_dict.copy() + tmp["message"] = message + return tmp + + 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__ + + def get_option_ini(config, *names): for name in names: ret = config.getoption(name) # 'default' arg won't work as expected @@ -444,6 +474,9 @@ class LoggingPlugin(object): ) else: formatter = logging.Formatter(log_format, log_date_format) + + if not six.PY2: + formatter._style = PercentStyleMultiline(formatter._style._fmt) return formatter def _setup_cli_logging(self): diff --git a/testing/logging/test_formatter.py b/testing/logging/test_formatter.py index 1610da845..c851c34d7 100644 --- a/testing/logging/test_formatter.py +++ b/testing/logging/test_formatter.py @@ -2,7 +2,9 @@ import logging import py.io +import six +import pytest from _pytest.logging import ColoredLevelFormatter @@ -35,3 +37,31 @@ def test_coloredlogformatter(): formatter = ColoredLevelFormatter(tw, logfmt) output = formatter.format(record) assert output == ("dummypath 10 INFO Test Message") + + +@pytest.mark.skipif( + six.PY2, reason="Formatter classes don't support format styles in PY2" +) +def test_multiline_message(): + from _pytest.logging import PercentStyleMultiline + + logfmt = "%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s" + + record = logging.LogRecord( + name="dummy", + level=logging.INFO, + pathname="dummypath", + lineno=10, + msg="Test Message line1\nline2", + args=(), + exc_info=False, + ) + # this is called by logging.Formatter.format + record.message = record.getMessage() + + style = PercentStyleMultiline(logfmt) + output = style.format(record) + assert output == ( + "dummypath 10 INFO Test Message line1\n" + " line2" + ) From f9cafd1c94d8592a5114c91184f7322aa5d5c143 Mon Sep 17 00:00:00 2001 From: Bruno Oliveira Date: Wed, 29 May 2019 21:13:16 -0300 Subject: [PATCH 30/37] Add missing junitxml ini options to the reference docs --- doc/en/reference.rst | 45 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/doc/en/reference.rst b/doc/en/reference.rst index f76fc7765..cc5fe8d8b 100644 --- a/doc/en/reference.rst +++ b/doc/en/reference.rst @@ -1087,6 +1087,22 @@ passed multiple times. The expected format is ``name=value``. For example:: This tells pytest to ignore deprecation warnings and turn all other warnings into errors. For more information please refer to :ref:`warnings`. + +.. confval:: junit_duration_report + + .. versionadded:: 4.1 + + Configures how durations are recorded into the JUnit XML report: + + * ``total`` (the default): duration times reported include setup, call, and teardown times. + * ``call``: duration times reported include only call times, excluding setup and teardown. + + .. code-block:: ini + + [pytest] + junit_duration_report = call + + .. confval:: junit_family .. versionadded:: 4.2 @@ -1102,10 +1118,35 @@ passed multiple times. The expected format is ``name=value``. For example:: [pytest] junit_family = xunit2 + +.. confval:: junit_logging + + .. versionadded:: 3.5 + + Configures if stdout/stderr should be written to the JUnit XML file. Valid values are + ``system-out``, ``system-err``, and ``no`` (the default). + + .. code-block:: ini + + [pytest] + junit_logging = system-out + + +.. confval:: junit_log_passing_tests + + .. versionadded:: 4.6 + + If ``junit_logging != "no"``, configures if the captured output should be written + to the JUnit XML file for **passing** tests. Default is ``True``. + + .. code-block:: ini + + [pytest] + junit_log_passing_tests = False + + .. confval:: junit_suite_name - - To set the name of the root test suite xml item, you can configure the ``junit_suite_name`` option in your config file: .. code-block:: ini From 61dfd0a94f8d9170cc29cfbfa07fae6c14d781ad Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Tue, 28 May 2019 14:31:35 +0200 Subject: [PATCH 31/37] pdb: move/refactor initialization of PytestPdbWrapper --- src/_pytest/debugging.py | 248 ++++++++++++++++++++------------------- testing/test_pdb.py | 49 ++++++-- 2 files changed, 169 insertions(+), 128 deletions(-) diff --git a/src/_pytest/debugging.py b/src/_pytest/debugging.py index 8912477db..99d35a5ab 100644 --- a/src/_pytest/debugging.py +++ b/src/_pytest/debugging.py @@ -81,6 +81,7 @@ class pytestPDB(object): _config = None _saved = [] _recursive_debug = 0 + _wrapped_pdb_cls = None @classmethod def _is_capturing(cls, capman): @@ -89,43 +90,138 @@ class pytestPDB(object): return False @classmethod - def _import_pdb_cls(cls): + def _import_pdb_cls(cls, capman): if not cls._config: # Happens when using pytest.set_trace outside of a test. return pdb.Pdb - pdb_cls = cls._config.getvalue("usepdb_cls") - if not pdb_cls: - return pdb.Pdb + usepdb_cls = cls._config.getvalue("usepdb_cls") - modname, classname = pdb_cls + if cls._wrapped_pdb_cls and cls._wrapped_pdb_cls[0] == usepdb_cls: + return cls._wrapped_pdb_cls[1] - try: - __import__(modname) - mod = sys.modules[modname] + if usepdb_cls: + modname, classname = usepdb_cls - # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). - parts = classname.split(".") - pdb_cls = getattr(mod, parts[0]) - for part in parts[1:]: - pdb_cls = getattr(pdb_cls, part) + try: + __import__(modname) + mod = sys.modules[modname] - return pdb_cls - except Exception as exc: - value = ":".join((modname, classname)) - raise UsageError("--pdbcls: could not import {!r}: {}".format(value, exc)) + # Handle --pdbcls=pdb:pdb.Pdb (useful e.g. with pdbpp). + parts = classname.split(".") + pdb_cls = getattr(mod, parts[0]) + for part in parts[1:]: + pdb_cls = getattr(pdb_cls, part) + except Exception as exc: + value = ":".join((modname, classname)) + raise UsageError( + "--pdbcls: could not import {!r}: {}".format(value, exc) + ) + else: + pdb_cls = pdb.Pdb + + wrapped_cls = cls._get_pdb_wrapper_class(pdb_cls, capman) + cls._wrapped_pdb_cls = (usepdb_cls, wrapped_cls) + return wrapped_cls @classmethod - def _init_pdb(cls, *args, **kwargs): + def _get_pdb_wrapper_class(cls, pdb_cls, capman): + import _pytest.config + + class PytestPdbWrapper(pdb_cls, object): + _pytest_capman = capman + _continued = False + + def do_debug(self, arg): + cls._recursive_debug += 1 + ret = super(PytestPdbWrapper, self).do_debug(arg) + cls._recursive_debug -= 1 + return ret + + def do_continue(self, arg): + ret = super(PytestPdbWrapper, self).do_continue(arg) + if cls._recursive_debug == 0: + tw = _pytest.config.create_terminal_writer(cls._config) + tw.line() + + capman = self._pytest_capman + capturing = pytestPDB._is_capturing(capman) + if capturing: + if capturing == "global": + tw.sep(">", "PDB continue (IO-capturing resumed)") + else: + tw.sep( + ">", + "PDB continue (IO-capturing resumed for %s)" + % capturing, + ) + capman.resume() + else: + tw.sep(">", "PDB continue") + cls._pluginmanager.hook.pytest_leave_pdb(config=cls._config, pdb=self) + self._continued = True + return ret + + do_c = do_cont = do_continue + + def do_quit(self, arg): + """Raise Exit outcome when quit command is used in pdb. + + This is a bit of a hack - it would be better if BdbQuit + could be handled, but this would require to wrap the + whole pytest run, and adjust the report etc. + """ + ret = super(PytestPdbWrapper, self).do_quit(arg) + + if cls._recursive_debug == 0: + outcomes.exit("Quitting debugger") + + return ret + + do_q = do_quit + do_exit = do_quit + + def setup(self, f, tb): + """Suspend on setup(). + + Needed after do_continue resumed, and entering another + breakpoint again. + """ + ret = super(PytestPdbWrapper, self).setup(f, tb) + if not ret and self._continued: + # pdb.setup() returns True if the command wants to exit + # from the interaction: do not suspend capturing then. + if self._pytest_capman: + self._pytest_capman.suspend_global_capture(in_=True) + return ret + + def get_stack(self, f, t): + stack, i = super(PytestPdbWrapper, self).get_stack(f, t) + if f is None: + # Find last non-hidden frame. + i = max(0, len(stack) - 1) + while i and stack[i][0].f_locals.get("__tracebackhide__", False): + i -= 1 + return stack, i + + return PytestPdbWrapper + + @classmethod + def _init_pdb(cls, method, *args, **kwargs): """ Initialize PDB debugging, dropping any IO capturing. """ import _pytest.config if cls._pluginmanager is not None: capman = cls._pluginmanager.getplugin("capturemanager") - if capman: - capman.suspend(in_=True) + else: + capman = None + if capman: + capman.suspend(in_=True) + + if cls._config: tw = _pytest.config.create_terminal_writer(cls._config) tw.line() + if cls._recursive_debug == 0: # Handle header similar to pdb.set_trace in py37+. header = kwargs.pop("header", None) @@ -133,112 +229,28 @@ class pytestPDB(object): tw.sep(">", header) else: capturing = cls._is_capturing(capman) - if capturing: - if capturing == "global": - tw.sep(">", "PDB set_trace (IO-capturing turned off)") - else: - tw.sep( - ">", - "PDB set_trace (IO-capturing turned off for %s)" - % capturing, - ) + if capturing == "global": + tw.sep(">", "PDB %s (IO-capturing turned off)" % (method,)) + elif capturing: + tw.sep( + ">", + "PDB %s (IO-capturing turned off for %s)" + % (method, capturing), + ) else: - tw.sep(">", "PDB set_trace") + tw.sep(">", "PDB %s" % (method,)) - pdb_cls = cls._import_pdb_cls() + _pdb = cls._import_pdb_cls(capman)(**kwargs) - class PytestPdbWrapper(pdb_cls, object): - _pytest_capman = capman - _continued = False - - def do_debug(self, arg): - cls._recursive_debug += 1 - ret = super(PytestPdbWrapper, self).do_debug(arg) - cls._recursive_debug -= 1 - return ret - - def do_continue(self, arg): - ret = super(PytestPdbWrapper, self).do_continue(arg) - if cls._recursive_debug == 0: - tw = _pytest.config.create_terminal_writer(cls._config) - tw.line() - - capman = self._pytest_capman - capturing = pytestPDB._is_capturing(capman) - if capturing: - if capturing == "global": - tw.sep(">", "PDB continue (IO-capturing resumed)") - else: - tw.sep( - ">", - "PDB continue (IO-capturing resumed for %s)" - % capturing, - ) - capman.resume() - else: - tw.sep(">", "PDB continue") - cls._pluginmanager.hook.pytest_leave_pdb( - config=cls._config, pdb=self - ) - self._continued = True - return ret - - do_c = do_cont = do_continue - - def do_quit(self, arg): - """Raise Exit outcome when quit command is used in pdb. - - This is a bit of a hack - it would be better if BdbQuit - could be handled, but this would require to wrap the - whole pytest run, and adjust the report etc. - """ - ret = super(PytestPdbWrapper, self).do_quit(arg) - - if cls._recursive_debug == 0: - outcomes.exit("Quitting debugger") - - return ret - - do_q = do_quit - do_exit = do_quit - - def setup(self, f, tb): - """Suspend on setup(). - - Needed after do_continue resumed, and entering another - breakpoint again. - """ - ret = super(PytestPdbWrapper, self).setup(f, tb) - if not ret and self._continued: - # pdb.setup() returns True if the command wants to exit - # from the interaction: do not suspend capturing then. - if self._pytest_capman: - self._pytest_capman.suspend_global_capture(in_=True) - return ret - - def get_stack(self, f, t): - stack, i = super(PytestPdbWrapper, self).get_stack(f, t) - if f is None: - # Find last non-hidden frame. - i = max(0, len(stack) - 1) - while i and stack[i][0].f_locals.get( - "__tracebackhide__", False - ): - i -= 1 - return stack, i - - _pdb = PytestPdbWrapper(**kwargs) + if cls._pluginmanager: cls._pluginmanager.hook.pytest_enter_pdb(config=cls._config, pdb=_pdb) - else: - pdb_cls = cls._import_pdb_cls() - _pdb = pdb_cls(**kwargs) return _pdb @classmethod def set_trace(cls, *args, **kwargs): """Invoke debugging via ``Pdb.set_trace``, dropping any IO capturing.""" frame = sys._getframe().f_back - _pdb = cls._init_pdb(*args, **kwargs) + _pdb = cls._init_pdb("set_trace", *args, **kwargs) _pdb.set_trace(frame) @@ -265,7 +277,7 @@ class PdbTrace(object): def _test_pytest_function(pyfuncitem): - _pdb = pytestPDB._init_pdb() + _pdb = pytestPDB._init_pdb("runcall") testfunction = pyfuncitem.obj pyfuncitem.obj = _pdb.runcall if "func" in pyfuncitem._fixtureinfo.argnames: # pragma: no branch @@ -315,7 +327,7 @@ def _postmortem_traceback(excinfo): def post_mortem(t): - p = pytestPDB._init_pdb() + p = pytestPDB._init_pdb("post_mortem") p.reset() p.interaction(None, t) if p.quitting: diff --git a/testing/test_pdb.py b/testing/test_pdb.py index 8c4534109..267b1e528 100644 --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -638,36 +638,35 @@ class TestPDB(object): class pytestPDBTest(_pytest.debugging.pytestPDB): @classmethod def set_trace(cls, *args, **kwargs): - # Init _PdbWrapper to handle capturing. - _pdb = cls._init_pdb(*args, **kwargs) + # Init PytestPdbWrapper to handle capturing. + _pdb = cls._init_pdb("set_trace", *args, **kwargs) # Mock out pdb.Pdb.do_continue. import pdb pdb.Pdb.do_continue = lambda self, arg: None - print("=== SET_TRACE ===") + print("===" + " SET_TRACE ===") assert input() == "debug set_trace()" - # Simulate _PdbWrapper.do_debug + # Simulate PytestPdbWrapper.do_debug cls._recursive_debug += 1 print("ENTERING RECURSIVE DEBUGGER") - print("=== SET_TRACE_2 ===") + print("===" + " SET_TRACE_2 ===") assert input() == "c" _pdb.do_continue("") - print("=== SET_TRACE_3 ===") + print("===" + " SET_TRACE_3 ===") - # Simulate _PdbWrapper.do_debug + # Simulate PytestPdbWrapper.do_debug print("LEAVING RECURSIVE DEBUGGER") cls._recursive_debug -= 1 - print("=== SET_TRACE_4 ===") + print("===" + " SET_TRACE_4 ===") assert input() == "c" _pdb.do_continue("") def do_continue(self, arg): print("=== do_continue") - # _PdbWrapper.do_continue("") monkeypatch.setattr(_pytest.debugging, "pytestPDB", pytestPDBTest) @@ -677,7 +676,7 @@ class TestPDB(object): set_trace() """ ) - child = testdir.spawn_pytest("%s %s" % (p1, capture_arg)) + child = testdir.spawn_pytest("--tb=short %s %s" % (p1, capture_arg)) child.expect("=== SET_TRACE ===") before = child.before.decode("utf8") if not capture_arg: @@ -1207,3 +1206,33 @@ def test_raises_bdbquit_with_eoferror(testdir): result = testdir.runpytest(str(p1)) result.stdout.fnmatch_lines(["E *BdbQuit", "*= 1 failed in*"]) assert result.ret == 1 + + +def test_pdb_wrapper_class_is_reused(testdir): + p1 = testdir.makepyfile( + """ + def test(): + __import__("pdb").set_trace() + __import__("pdb").set_trace() + + import mypdb + instances = mypdb.instances + assert len(instances) == 2 + assert instances[0].__class__ is instances[1].__class__ + """, + mypdb=""" + instances = [] + + class MyPdb: + def __init__(self, *args, **kwargs): + instances.append(self) + + def set_trace(self, *args): + print("set_trace_called", args) + """, + ) + result = testdir.runpytest(str(p1), "--pdbcls=mypdb:MyPdb", syspathinsert=True) + assert result.ret == 0 + result.stdout.fnmatch_lines( + ["*set_trace_called*", "*set_trace_called*", "* 1 passed in *"] + ) From 5ac498ea9634729d3281d67a8cd27d20a34cf684 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 30 May 2019 06:37:53 +0200 Subject: [PATCH 32/37] ci: Travis: add pypy3 to allowed failures temporarily Ref: https://github.com/pytest-dev/pytest/pull/5334 Ref: https://github.com/pytest-dev/pytest/issues/5317 --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index ecbba1255..442a2761f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -115,6 +115,9 @@ matrix: allow_failures: - python: '3.8-dev' env: TOXENV=py38-xdist + # Temporary (https://github.com/pytest-dev/pytest/pull/5334). + - env: TOXENV=pypy3-xdist + python: 'pypy3' before_script: - | From 28bf3816e7218acdc11316fb1f9e69507576b92a Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 30 May 2019 06:55:38 +0200 Subject: [PATCH 33/37] tests: conftest: auto-add slow marker --- testing/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/testing/conftest.py b/testing/conftest.py index 4badf3016..6e01d710d 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -30,6 +30,7 @@ def pytest_collection_modifyitems(config, items): slowest_items.append(item) else: slow_items.append(item) + item.add_marker(pytest.mark.slow) else: marker = item.get_closest_marker("slow") if marker: From da23aa341927550621706ab25111f7ffa6638248 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 30 May 2019 08:09:49 +0200 Subject: [PATCH 34/37] pytester: remove unused winpymap Follow-up to c86d2daf8. --- src/_pytest/pytester.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 21f5b9f24..1360d8d2f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -153,15 +153,6 @@ class LsofFdLeakChecker(object): item.warn(pytest.PytestWarning("\n".join(error))) -# XXX copied from execnet's conftest.py - needs to be merged -winpymap = { - "python2.7": r"C:\Python27\python.exe", - "python3.4": r"C:\Python34\python.exe", - "python3.5": r"C:\Python35\python.exe", - "python3.6": r"C:\Python36\python.exe", -} - - # used at least by pytest-xdist plugin From f013a5e8c1d375f2a5187fa2570627b8b05c45de Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 May 2019 16:46:50 +0200 Subject: [PATCH 35/37] pytester: use temporary HOME with spawn Followup to https://github.com/pytest-dev/pytest/issues/4956. --- changelog/4956.feature.rst | 1 + src/_pytest/pytester.py | 8 +++++++- testing/test_pytester.py | 23 +++++++++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 changelog/4956.feature.rst diff --git a/changelog/4956.feature.rst b/changelog/4956.feature.rst new file mode 100644 index 000000000..ab8357cda --- /dev/null +++ b/changelog/4956.feature.rst @@ -0,0 +1 @@ +pytester's ``testdir.spawn`` uses ``tmpdir`` as HOME/USERPROFILE directory. diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 1360d8d2f..e76eaa075 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -1233,7 +1233,13 @@ class Testdir(object): if sys.platform.startswith("freebsd"): pytest.xfail("pexpect does not work reliably on freebsd") logfile = self.tmpdir.join("spawn.out").open("wb") - child = pexpect.spawn(cmd, logfile=logfile) + + # Do not load user config. + env = os.environ.copy() + env["HOME"] = str(self.tmpdir) + env["USERPROFILE"] = env["HOME"] + + child = pexpect.spawn(cmd, logfile=logfile, env=env) self.request.addfinalizer(logfile.close) child.timeout = expect_timeout return child diff --git a/testing/test_pytester.py b/testing/test_pytester.py index b76d413b7..4d3833ba0 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -559,3 +559,26 @@ def test_popen_default_stdin_stderr_and_stdin_None(testdir): assert stdout.splitlines() == [b"", b"stdout"] assert stderr.splitlines() == [b"stderr"] assert proc.returncode == 0 + + +def test_spawn_uses_tmphome(testdir): + import os + + tmphome = str(testdir.tmpdir) + + # Does use HOME only during run. + assert os.environ.get("HOME") != tmphome + + p1 = testdir.makepyfile( + """ + import os + + def test(): + assert os.environ["HOME"] == {tmphome!r} + """.format( + tmphome=tmphome + ) + ) + child = testdir.spawn_pytest(str(p1)) + out = child.read() + assert child.wait() == 0, out.decode("utf8") From ace3a02cd485d1024a07d93dc8d58708a882587e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Fri, 24 May 2019 17:49:14 +0200 Subject: [PATCH 36/37] pytester: factor out testdir._env_run_update --- src/_pytest/pytester.py | 15 ++++++++------- testing/test_pytester.py | 3 +++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index e76eaa075..605451630 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -508,6 +508,10 @@ class Testdir(object): # Discard outer pytest options. mp.delenv("PYTEST_ADDOPTS", raising=False) + # Environment (updates) for inner runs. + tmphome = str(self.tmpdir) + self._env_run_update = {"HOME": tmphome, "USERPROFILE": tmphome} + def __repr__(self): return "" % (self.tmpdir,) @@ -804,8 +808,8 @@ class Testdir(object): try: # Do not load user config (during runs only). mp_run = MonkeyPatch() - mp_run.setenv("HOME", str(self.tmpdir)) - mp_run.setenv("USERPROFILE", str(self.tmpdir)) + for k, v in self._env_run_update.items(): + mp_run.setenv(k, v) finalizers.append(mp_run.undo) # When running pytest inline any plugins active in the main test @@ -1045,9 +1049,7 @@ class Testdir(object): env["PYTHONPATH"] = os.pathsep.join( filter(None, [os.getcwd(), env.get("PYTHONPATH", "")]) ) - # Do not load user config. - env["HOME"] = str(self.tmpdir) - env["USERPROFILE"] = env["HOME"] + env.update(self._env_run_update) kw["env"] = env if stdin is Testdir.CLOSE_STDIN: @@ -1236,8 +1238,7 @@ class Testdir(object): # Do not load user config. env = os.environ.copy() - env["HOME"] = str(self.tmpdir) - env["USERPROFILE"] = env["HOME"] + env.update(self._env_run_update) child = pexpect.spawn(cmd, logfile=logfile, env=env) self.request.addfinalizer(logfile.close) diff --git a/testing/test_pytester.py b/testing/test_pytester.py index 4d3833ba0..54d364ca1 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -569,12 +569,15 @@ def test_spawn_uses_tmphome(testdir): # Does use HOME only during run. assert os.environ.get("HOME") != tmphome + testdir._env_run_update["CUSTOMENV"] = "42" + p1 = testdir.makepyfile( """ import os def test(): assert os.environ["HOME"] == {tmphome!r} + assert os.environ["CUSTOMENV"] == "42" """.format( tmphome=tmphome ) From e7cd00ac920f3db2855becd75193d2241b84e56d Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Fri, 31 May 2019 09:01:05 -0700 Subject: [PATCH 37/37] Preparing release version 4.6.0 --- CHANGELOG.rst | 78 ++++++++++++++++++++++++ changelog/2064.bugfix.rst | 1 - changelog/4559.feature.rst | 1 - changelog/4908.bugfix.rst | 1 - changelog/4956.feature.rst | 1 - changelog/5036.bugfix.rst | 1 - changelog/5062.feature.rst | 1 - changelog/5063.feature.rst | 1 - changelog/5091.feature.rst | 1 - changelog/5250.doc.rst | 1 - changelog/5256.bugfix.rst | 1 - changelog/5257.bugfix.rst | 1 - changelog/5269.feature.rst | 1 - changelog/5278.bugfix.rst | 1 - changelog/5286.bugfix.rst | 1 - changelog/5311.feature.rst | 2 - changelog/5312.feature.rst | 1 - changelog/5330.bugfix.rst | 2 - changelog/5333.bugfix.rst | 1 - doc/en/announce/index.rst | 1 + doc/en/announce/release-4.6.0.rst | 43 ++++++++++++++ doc/en/example/parametrize.rst | 4 +- doc/en/example/reportingdemo.rst | 98 +++++++++++++++---------------- doc/en/warnings.rst | 2 +- 24 files changed, 174 insertions(+), 72 deletions(-) delete mode 100644 changelog/2064.bugfix.rst delete mode 100644 changelog/4559.feature.rst delete mode 100644 changelog/4908.bugfix.rst delete mode 100644 changelog/4956.feature.rst delete mode 100644 changelog/5036.bugfix.rst delete mode 100644 changelog/5062.feature.rst delete mode 100644 changelog/5063.feature.rst delete mode 100644 changelog/5091.feature.rst delete mode 100644 changelog/5250.doc.rst delete mode 100644 changelog/5256.bugfix.rst delete mode 100644 changelog/5257.bugfix.rst delete mode 100644 changelog/5269.feature.rst delete mode 100644 changelog/5278.bugfix.rst delete mode 100644 changelog/5286.bugfix.rst delete mode 100644 changelog/5311.feature.rst delete mode 100644 changelog/5312.feature.rst delete mode 100644 changelog/5330.bugfix.rst delete mode 100644 changelog/5333.bugfix.rst create mode 100644 doc/en/announce/release-4.6.0.rst diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a1bcd1917..32267f4dd 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -18,6 +18,84 @@ with advance notice in the **Deprecations** section of releases. .. towncrier release notes start +pytest 4.6.0 (2019-05-31) +========================= + +Important +--------- + +The ``4.6.X`` series will be the last series to support **Python 2 and Python 3.4**. + +For more details, see our `Python 2.7 and 3.4 support plan `__. + + +Features +-------- + +- `#4559 `_: Added the ``junit_log_passing_tests`` ini value which can be used to enable or disable logging of passing test output in the Junit XML file. + + +- `#4956 `_: pytester's ``testdir.spawn`` uses ``tmpdir`` as HOME/USERPROFILE directory. + + +- `#5062 `_: Unroll calls to ``all`` to full for-loops with assertion rewriting for better failure messages, especially when using Generator Expressions. + + +- `#5063 `_: Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time. + + +- `#5091 `_: The output for ini options in ``--help`` has been improved. + + +- `#5269 `_: ``pytest.importorskip`` includes the ``ImportError`` now in the default ``reason``. + + +- `#5311 `_: Captured logs that are output for each failing test are formatted using the + ColoredLevelFormatter. + + +- `#5312 `_: Improved formatting of multiline log messages in Python 3. + + + +Bug Fixes +--------- + +- `#2064 `_: The debugging plugin imports the wrapped ``Pdb`` class (``--pdbcls``) on-demand now. + + +- `#4908 `_: The ``pytest_enter_pdb`` hook gets called with post-mortem (``--pdb``). + + +- `#5036 `_: Fix issue where fixtures dependent on other parametrized fixtures would be erroneously parametrized. + + +- `#5256 `_: Handle internal error due to a lone surrogate unicode character not being representable in Jython. + + +- `#5257 `_: Ensure that ``sys.stdout.mode`` does not include ``'b'`` as it is a text stream. + + +- `#5278 `_: Pytest's internal python plugin can be disabled using ``-p no:python`` again. + + +- `#5286 `_: Fix issue with ``disable_test_id_escaping_and_forfeit_all_rights_to_community_support`` option not working when using a list of test IDs in parametrized tests. + + +- `#5330 `_: Show the test module being collected when emitting ``PytestCollectionWarning`` messages for + test classes with ``__init__`` and ``__new__`` methods to make it easier to pin down the problem. + + +- `#5333 `_: Fix regression in 4.5.0 with ``--lf`` not re-running all tests with known failures from non-selected tests. + + + +Improved Documentation +---------------------- + +- `#5250 `_: Expand docs on use of ``setenv`` and ``delenv`` with ``monkeypatch``. + + pytest 4.5.0 (2019-05-11) ========================= diff --git a/changelog/2064.bugfix.rst b/changelog/2064.bugfix.rst deleted file mode 100644 index eba593fc5..000000000 --- a/changelog/2064.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -The debugging plugin imports the wrapped ``Pdb`` class (``--pdbcls``) on-demand now. diff --git a/changelog/4559.feature.rst b/changelog/4559.feature.rst deleted file mode 100644 index ff076aff5..000000000 --- a/changelog/4559.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added the ``junit_log_passing_tests`` ini value which can be used to enable and disable logging passing test output in the Junit XML file. diff --git a/changelog/4908.bugfix.rst b/changelog/4908.bugfix.rst deleted file mode 100644 index 2513618a1..000000000 --- a/changelog/4908.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -The ``pytest_enter_pdb`` hook gets called with post-mortem (``--pdb``). diff --git a/changelog/4956.feature.rst b/changelog/4956.feature.rst deleted file mode 100644 index ab8357cda..000000000 --- a/changelog/4956.feature.rst +++ /dev/null @@ -1 +0,0 @@ -pytester's ``testdir.spawn`` uses ``tmpdir`` as HOME/USERPROFILE directory. diff --git a/changelog/5036.bugfix.rst b/changelog/5036.bugfix.rst deleted file mode 100644 index a9c266538..000000000 --- a/changelog/5036.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix issue where fixtures dependent on other parametrized fixtures would be erroneously parametrized. diff --git a/changelog/5062.feature.rst b/changelog/5062.feature.rst deleted file mode 100644 index e311d161d..000000000 --- a/changelog/5062.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Unroll calls to ``all`` to full for-loops for better failure messages, especially when using Generator Expressions. diff --git a/changelog/5063.feature.rst b/changelog/5063.feature.rst deleted file mode 100644 index 21379ef0a..000000000 --- a/changelog/5063.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time. diff --git a/changelog/5091.feature.rst b/changelog/5091.feature.rst deleted file mode 100644 index 122c91f53..000000000 --- a/changelog/5091.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The output for ini options in ``--help`` has been improved. diff --git a/changelog/5250.doc.rst b/changelog/5250.doc.rst deleted file mode 100644 index 1b4c564a0..000000000 --- a/changelog/5250.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Expand docs on use of ``setenv`` and ``delenv`` with ``monkeypatch``. diff --git a/changelog/5256.bugfix.rst b/changelog/5256.bugfix.rst deleted file mode 100644 index 4df83454e..000000000 --- a/changelog/5256.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Handle internal error due to a lone surrogate unicode character not being representable in Jython. diff --git a/changelog/5257.bugfix.rst b/changelog/5257.bugfix.rst deleted file mode 100644 index 520519088..000000000 --- a/changelog/5257.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Ensure that ``sys.stdout.mode`` does not include ``'b'`` as it is a text stream. diff --git a/changelog/5269.feature.rst b/changelog/5269.feature.rst deleted file mode 100644 index 6247bfd5c..000000000 --- a/changelog/5269.feature.rst +++ /dev/null @@ -1 +0,0 @@ -``pytest.importorskip`` includes the ``ImportError`` now in the default ``reason``. diff --git a/changelog/5278.bugfix.rst b/changelog/5278.bugfix.rst deleted file mode 100644 index 4fe70c7fd..000000000 --- a/changelog/5278.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Pytest's internal python plugin can be disabled using ``-p no:python`` again. diff --git a/changelog/5286.bugfix.rst b/changelog/5286.bugfix.rst deleted file mode 100644 index 1d6974b89..000000000 --- a/changelog/5286.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix issue with ``disable_test_id_escaping_and_forfeit_all_rights_to_community_support`` option doesn't work when using a list of test IDs in parametrized tests. diff --git a/changelog/5311.feature.rst b/changelog/5311.feature.rst deleted file mode 100644 index ae7ef99ac..000000000 --- a/changelog/5311.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Captured logs that are output for each failing test are formatted using the -ColoredLevelFormatter. diff --git a/changelog/5312.feature.rst b/changelog/5312.feature.rst deleted file mode 100644 index bcd5a736b..000000000 --- a/changelog/5312.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Improved formatting of multiline log messages in python3. diff --git a/changelog/5330.bugfix.rst b/changelog/5330.bugfix.rst deleted file mode 100644 index 61c715552..000000000 --- a/changelog/5330.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Show the test module being collected when emitting ``PytestCollectionWarning`` messages for -test classes with ``__init__`` and ``__new__`` methods to make it easier to pin down the problem. diff --git a/changelog/5333.bugfix.rst b/changelog/5333.bugfix.rst deleted file mode 100644 index ac1d79585..000000000 --- a/changelog/5333.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix regression with ``--lf`` not re-running all tests with known failures from non-selected tests. diff --git a/doc/en/announce/index.rst b/doc/en/announce/index.rst index 8b5ca7b0a..fa53441ce 100644 --- a/doc/en/announce/index.rst +++ b/doc/en/announce/index.rst @@ -6,6 +6,7 @@ Release announcements :maxdepth: 2 + release-4.6.0 release-4.5.0 release-4.4.2 release-4.4.1 diff --git a/doc/en/announce/release-4.6.0.rst b/doc/en/announce/release-4.6.0.rst new file mode 100644 index 000000000..373f5d66e --- /dev/null +++ b/doc/en/announce/release-4.6.0.rst @@ -0,0 +1,43 @@ +pytest-4.6.0 +======================================= + +The pytest team is proud to announce the 4.6.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: + +* Akiomi Kamakura +* Anthony Sottile +* Bruno Oliveira +* Daniel Hahler +* David Röthlisberger +* Evan Kepner +* Jeffrey Rackauckas +* MyComputer +* Nikita Krokosh +* Raul Tambre +* Thomas Hisch +* Tim Hoffmann +* Tomer Keren +* Victor Maryama +* danielx123 +* oleg-yegorov + + +Happy testing, +The Pytest Development Team diff --git a/doc/en/example/parametrize.rst b/doc/en/example/parametrize.rst index c3f825297..ef792afe7 100644 --- a/doc/en/example/parametrize.rst +++ b/doc/en/example/parametrize.rst @@ -436,7 +436,7 @@ Running it results in some skips if we don't have all the python interpreters in . $ pytest -rs -q multipython.py ...sss...sssssssss...sss... [100%] ========================= short test summary info ========================== - SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:30: 'python3.4' not found + SKIPPED [15] $REGENDOC_TMPDIR/CWD/multipython.py:31: 'python3.4' not found 12 passed, 15 skipped in 0.12 seconds Indirect parametrization of optional implementations/imports @@ -494,7 +494,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:11: could not import 'opt2' + SKIPPED [1] $REGENDOC_TMPDIR/conftest.py:11: could not import 'opt2': No module named 'opt2' =================== 1 passed, 1 skipped in 0.12 seconds ==================== You'll see that we don't have an ``opt2`` module and thus the second test run diff --git a/doc/en/example/reportingdemo.rst b/doc/en/example/reportingdemo.rst index 1371d9cfa..928c365ca 100644 --- a/doc/en/example/reportingdemo.rst +++ b/doc/en/example/reportingdemo.rst @@ -26,7 +26,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert param1 * 2 < param2 E assert (3 * 2) < 6 - failure_demo.py:20: AssertionError + failure_demo.py:21: AssertionError _________________________ TestFailing.test_simple __________________________ self = @@ -43,7 +43,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 42 = .f at 0xdeadbeef>() E + and 43 = .g at 0xdeadbeef>() - failure_demo.py:31: AssertionError + failure_demo.py:32: AssertionError ____________________ TestFailing.test_simple_multiline _____________________ self = @@ -51,7 +51,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: def test_simple_multiline(self): > otherfunc_multi(42, 6 * 9) - failure_demo.py:34: + failure_demo.py:35: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ a = 42, b = 54 @@ -60,7 +60,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 42 == 54 - failure_demo.py:15: AssertionError + failure_demo.py:16: AssertionError ___________________________ TestFailing.test_not ___________________________ self = @@ -73,7 +73,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert not 42 E + where 42 = .f at 0xdeadbeef>() - failure_demo.py:40: AssertionError + failure_demo.py:41: AssertionError _________________ TestSpecialisedExplanations.test_eq_text _________________ self = @@ -84,7 +84,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E - spam E + eggs - failure_demo.py:45: AssertionError + failure_demo.py:46: AssertionError _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ self = @@ -97,7 +97,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + foo 2 bar E ? ^ - failure_demo.py:48: AssertionError + failure_demo.py:49: AssertionError ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ self = @@ -110,7 +110,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + eggs E bar - failure_demo.py:51: AssertionError + failure_demo.py:52: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ self = @@ -127,7 +127,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + 1111111111b222222222 E ? ^ - failure_demo.py:56: AssertionError + failure_demo.py:57: AssertionError _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ self = @@ -147,7 +147,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (7 lines hidden), use '-vv' to show - failure_demo.py:61: AssertionError + failure_demo.py:62: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ self = @@ -158,7 +158,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 2 diff: 2 != 3 E Use -v to get the full diff - failure_demo.py:64: AssertionError + failure_demo.py:65: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ self = @@ -171,7 +171,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E At index 100 diff: 1 != 2 E Use -v to get the full diff - failure_demo.py:69: AssertionError + failure_demo.py:70: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ self = @@ -189,7 +189,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (2 lines hidden), use '-vv' to show - failure_demo.py:72: AssertionError + failure_demo.py:73: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ self = @@ -207,7 +207,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (2 lines hidden), use '-vv' to show - failure_demo.py:75: AssertionError + failure_demo.py:76: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ self = @@ -218,7 +218,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Right contains one more item: 3 E Use -v to get the full diff - failure_demo.py:78: AssertionError + failure_demo.py:79: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ self = @@ -227,7 +227,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert 1 in [0, 2, 3, 4, 5] E assert 1 in [0, 2, 3, 4, 5] - failure_demo.py:81: AssertionError + failure_demo.py:82: AssertionError __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ self = @@ -246,7 +246,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E E ...Full output truncated (2 lines hidden), use '-vv' to show - failure_demo.py:85: AssertionError + failure_demo.py:86: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ self = @@ -259,7 +259,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E single foo line E ? +++ - failure_demo.py:89: AssertionError + failure_demo.py:90: AssertionError _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ self = @@ -272,7 +272,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head foo tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? +++ - failure_demo.py:93: AssertionError + failure_demo.py:94: AssertionError ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ self = @@ -285,7 +285,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E head head fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffftail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail tail E ? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - failure_demo.py:97: AssertionError + failure_demo.py:98: AssertionError ______________ TestSpecialisedExplanations.test_eq_dataclass _______________ self = @@ -306,7 +306,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Differing attributes: E b: 'b' != 'c' - failure_demo.py:109: AssertionError + failure_demo.py:110: AssertionError ________________ TestSpecialisedExplanations.test_eq_attrs _________________ self = @@ -327,7 +327,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E Differing attributes: E b: 'b' != 'c' - failure_demo.py:121: AssertionError + failure_demo.py:122: AssertionError ______________________________ test_attribute ______________________________ def test_attribute(): @@ -339,7 +339,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .Foo object at 0xdeadbeef>.b - failure_demo.py:129: AssertionError + failure_demo.py:130: AssertionError _________________________ test_attribute_instance __________________________ def test_attribute_instance(): @@ -351,7 +351,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where 1 = .Foo object at 0xdeadbeef>.b E + where .Foo object at 0xdeadbeef> = .Foo'>() - failure_demo.py:136: AssertionError + failure_demo.py:137: AssertionError __________________________ test_attribute_failure __________________________ def test_attribute_failure(): @@ -364,7 +364,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: i = Foo() > assert i.b == 2 - failure_demo.py:147: + failure_demo.py:148: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = .Foo object at 0xdeadbeef> @@ -373,7 +373,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise Exception("Failed to get attrib") E Exception: Failed to get attrib - failure_demo.py:142: Exception + failure_demo.py:143: Exception _________________________ test_attribute_multiple __________________________ def test_attribute_multiple(): @@ -390,7 +390,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + and 2 = .Bar object at 0xdeadbeef>.b E + where .Bar object at 0xdeadbeef> = .Bar'>() - failure_demo.py:157: AssertionError + failure_demo.py:158: AssertionError __________________________ TestRaises.test_raises __________________________ self = @@ -400,7 +400,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(TypeError, int, s) E ValueError: invalid literal for int() with base 10: 'qwe' - failure_demo.py:167: ValueError + failure_demo.py:168: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ self = @@ -409,7 +409,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raises(IOError, int, "3") E Failed: DID NOT RAISE - failure_demo.py:170: Failed + failure_demo.py:171: Failed __________________________ TestRaises.test_raise ___________________________ self = @@ -418,7 +418,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > raise ValueError("demo error") E ValueError: demo error - failure_demo.py:173: ValueError + failure_demo.py:174: ValueError ________________________ TestRaises.test_tupleerror ________________________ self = @@ -427,7 +427,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = [1] # NOQA E ValueError: not enough values to unpack (expected 2, got 1) - failure_demo.py:176: ValueError + failure_demo.py:177: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ self = @@ -438,7 +438,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items.pop() E TypeError: 'int' object is not iterable - failure_demo.py:181: TypeError + failure_demo.py:182: TypeError --------------------------- Captured stdout call --------------------------- items is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ @@ -449,7 +449,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > if namenotexi: # NOQA E NameError: name 'namenotexi' is not defined - failure_demo.py:184: NameError + failure_demo.py:185: NameError ____________________ test_dynamic_compile_shows_nicely _____________________ def test_dynamic_compile_shows_nicely(): @@ -464,14 +464,14 @@ Here is a nice run of several failures and how ``pytest`` presents things: sys.modules[name] = module > module.foo() - failure_demo.py:202: + failure_demo.py:203: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def foo(): > assert 1 == 0 E AssertionError - <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:199>:2: AssertionError + <0-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:200>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ self = @@ -485,9 +485,9 @@ Here is a nice run of several failures and how ``pytest`` presents things: > somefunc(f(), g()) - failure_demo.py:213: + failure_demo.py:214: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - failure_demo.py:11: in somefunc + failure_demo.py:12: in somefunc otherfunc(x, y) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ @@ -497,7 +497,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert a == b E assert 44 == 43 - failure_demo.py:7: AssertionError + failure_demo.py:8: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ self = @@ -507,7 +507,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E ValueError: not enough values to unpack (expected 2, got 0) - failure_demo.py:217: ValueError + failure_demo.py:218: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ self = @@ -517,7 +517,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > a, b = items E TypeError: 'int' object is not iterable - failure_demo.py:221: TypeError + failure_demo.py:222: TypeError ______________________ TestMoreErrors.test_startswith ______________________ self = @@ -530,7 +530,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = ('456') E + where = '123'.startswith - failure_demo.py:226: AssertionError + failure_demo.py:227: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ self = @@ -549,7 +549,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where '123' = .f at 0xdeadbeef>() E + and '456' = .g at 0xdeadbeef>() - failure_demo.py:235: AssertionError + failure_demo.py:236: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ self = @@ -560,7 +560,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E + where False = isinstance(43, float) E + where 43 = globf(42) - failure_demo.py:238: AssertionError + failure_demo.py:239: AssertionError _______________________ TestMoreErrors.test_instance _______________________ self = @@ -571,7 +571,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 42 != 42 E + where 42 = .x - failure_demo.py:242: AssertionError + failure_demo.py:243: AssertionError _______________________ TestMoreErrors.test_compare ________________________ self = @@ -581,7 +581,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 11 < 5 E + where 11 = globf(10) - failure_demo.py:245: AssertionError + failure_demo.py:246: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ self = @@ -592,7 +592,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: > assert x == 0 E assert 1 == 0 - failure_demo.py:250: AssertionError + failure_demo.py:251: AssertionError ___________________ TestCustomAssertMsg.test_single_line ___________________ self = @@ -607,7 +607,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:261: AssertionError + failure_demo.py:262: AssertionError ____________________ TestCustomAssertMsg.test_multiline ____________________ self = @@ -626,7 +626,7 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = .A'>.a - failure_demo.py:268: AssertionError + failure_demo.py:269: AssertionError ___________________ TestCustomAssertMsg.test_custom_repr ___________________ self = @@ -648,5 +648,5 @@ Here is a nice run of several failures and how ``pytest`` presents things: E assert 1 == 2 E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a - failure_demo.py:281: AssertionError + failure_demo.py:282: AssertionError ======================== 44 failed in 0.12 seconds ========================= diff --git a/doc/en/warnings.rst b/doc/en/warnings.rst index dd10d94f4..8f8d6f3e1 100644 --- a/doc/en/warnings.rst +++ b/doc/en/warnings.rst @@ -400,7 +400,7 @@ defines an ``__init__`` constructor, as this prevents the class from being insta ============================= warnings summary ============================= test_pytest_warnings.py:1 - $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor + $REGENDOC_TMPDIR/test_pytest_warnings.py:1: PytestCollectionWarning: cannot collect test class 'Test' because it has a __init__ constructor (from: test_pytest_warnings.py) class Test: -- Docs: https://docs.pytest.org/en/latest/warnings.html